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