]> git.basschouten.com Git - openhab-addons.git/blob
ebecb2d454e94e9909996e6422531ace97f99752
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.voice.marytts.internal;
14
15 import static javax.sound.sampled.AudioSystem.NOT_SPECIFIED;
16
17 import java.io.IOException;
18 import java.util.HashSet;
19 import java.util.Locale;
20 import java.util.Set;
21
22 import org.openhab.core.audio.AudioFormat;
23 import org.openhab.core.audio.AudioStream;
24 import org.openhab.core.voice.TTSException;
25 import org.openhab.core.voice.TTSService;
26 import org.osgi.service.component.annotations.Component;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 import marytts.LocalMaryInterface;
31 import marytts.MaryInterface;
32 import marytts.exceptions.MaryConfigurationException;
33 import marytts.exceptions.SynthesisException;
34 import marytts.modules.synthesis.Voice;
35
36 /**
37  * This is a TTS service implementation for using MaryTTS.
38  *
39  * @author Kelly Davis - Initial contribution and API
40  * @author Kai Kreuzer - Refactored to updated APIs and moved to openHAB
41  */
42 @Component
43 public class MaryTTSService implements TTSService {
44
45     private final Logger logger = LoggerFactory.getLogger(MaryTTSService.class);
46
47     private MaryInterface marytts;
48
49     /**
50      * Set of supported voices
51      */
52     private Set<org.openhab.core.voice.Voice> voices;
53
54     /**
55      * Set of supported audio formats
56      */
57     private Set<AudioFormat> audioFormats;
58
59     protected void activate() {
60         try {
61             marytts = new LocalMaryInterface();
62             voices = initVoices();
63             audioFormats = initAudioFormats();
64         } catch (MaryConfigurationException e) {
65             logger.error("Failed to initialize MaryTTS: {}", e.getMessage(), e);
66         }
67     }
68
69     @Override
70     public Set<org.openhab.core.voice.Voice> getAvailableVoices() {
71         return voices;
72     }
73
74     @Override
75     public Set<AudioFormat> getSupportedFormats() {
76         return audioFormats;
77     }
78
79     @Override
80     public AudioStream synthesize(String text, org.openhab.core.voice.Voice voice, AudioFormat requestedFormat)
81             throws TTSException {
82         // Validate arguments
83         if (text == null || text.isEmpty()) {
84             throw new TTSException("The passed text is null or empty");
85         }
86         if (!voices.contains(voice)) {
87             throw new TTSException("The passed voice is unsupported");
88         }
89         if (audioFormats.stream().noneMatch(f -> f.isCompatible(requestedFormat))) {
90             throw new TTSException("The passed AudioFormat is unsupported");
91         }
92
93         /*
94          * NOTE: For each MaryTTS voice only a single AudioFormat is supported
95          * However, the TTSService interface allows the AudioFormat and
96          * the Voice to vary independently. Thus, an external user does
97          * not know about the requirement that a given voice is paired
98          * with a given AudioFormat. The test below enforces this.
99          *
100          * However, this leads to a problem. The user has no way to
101          * know which AudioFormat is apropos for a give Voice. Thus,
102          * throwing a TTSException for the wrong AudioFormat makes
103          * the user guess the right AudioFormat, a painful process.
104          * Alternatively, we can get the right AudioFormat for the
105          * Voice and ignore what the user requests, also wrong.
106          *
107          * TODO: Decide what to do
108          * Voice maryTTSVoice = Voice.getVoice(voice.getLabel());
109          * AudioFormat maryTTSVoiceAudioFormat = getAudioFormat(maryTTSVoice.dbAudioFormat());
110          * if (!maryTTSVoiceAudioFormat.isCompatible(requestedFormat)) {
111          * throw new TTSException("The passed AudioFormat is incompatable with the voice");
112          * }
113          */
114         Voice maryTTSVoice = Voice.getVoice(voice.getLabel());
115         AudioFormat maryTTSVoiceAudioFormat = getAudioFormat(maryTTSVoice.dbAudioFormat());
116
117         // Synchronize on marytts
118         synchronized (marytts) {
119             // Set voice (Each voice supports only a single AudioFormat)
120             marytts.setLocale(voice.getLocale());
121             marytts.setVoice(voice.getLabel());
122
123             try {
124                 return new MaryTTSAudioStream(marytts.generateAudio(text), maryTTSVoiceAudioFormat);
125             } catch (SynthesisException | IOException e) {
126                 throw new TTSException("Error generating an AudioStream", e);
127             }
128         }
129     }
130
131     /**
132      * Initializes voices
133      *
134      * @return The voices of this instance
135      */
136     private Set<org.openhab.core.voice.Voice> initVoices() {
137         Set<org.openhab.core.voice.Voice> voices = new HashSet<>();
138         for (Locale locale : marytts.getAvailableLocales()) {
139             for (String voiceLabel : marytts.getAvailableVoices(locale)) {
140                 voices.add(new MaryTTSVoice(locale, voiceLabel));
141             }
142         }
143         return voices;
144     }
145
146     /**
147      * Initializes audioFormats
148      *
149      * @return The audio formats of this instance
150      */
151     private Set<AudioFormat> initAudioFormats() {
152         Set<AudioFormat> audioFormats = new HashSet<>();
153         for (String voiceLabel : marytts.getAvailableVoices()) {
154             audioFormats.add(getAudioFormat(Voice.getVoice(voiceLabel).dbAudioFormat()));
155         }
156         return audioFormats;
157     }
158
159     /**
160      * Obtains an AudioFormat from a javax.sound.sampled.AudioFormat
161      *
162      * @param audioFormat The javax.sound.sampled.AudioFormat
163      * @return The corresponding AudioFormat
164      */
165     private AudioFormat getAudioFormat(javax.sound.sampled.AudioFormat audioFormat) {
166         String container = AudioFormat.CONTAINER_WAVE;
167         String codec = audioFormat.getEncoding().toString();
168         Boolean bigEndian = audioFormat.isBigEndian();
169
170         int frameSize = audioFormat.getFrameSize(); // In bytes
171         int bitsPerFrame = frameSize * 8;
172         Integer bitDepth = NOT_SPECIFIED == frameSize ? null : bitsPerFrame;
173
174         float frameRate = audioFormat.getFrameRate();
175         Integer bitRate = NOT_SPECIFIED == frameRate ? null : (int) frameRate * bitsPerFrame;
176
177         float sampleRate = audioFormat.getSampleRate();
178         Long frequency = NOT_SPECIFIED == sampleRate ? null : (long) sampleRate;
179
180         return new AudioFormat(container, codec, bigEndian, bitDepth, bitRate, frequency);
181     }
182
183     @Override
184     public String getId() {
185         return "marytts";
186     }
187
188     @Override
189     public String getLabel(Locale locale) {
190         return "MaryTTS";
191     }
192 }