]> git.basschouten.com Git - openhab-addons.git/blob
8a627aa32f6ea8483cc7513142ac3ac4c5c33c8d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.voicerss.internal;
14
15 import java.io.File;
16 import java.io.IOException;
17 import java.util.Collections;
18 import java.util.HashSet;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.Set;
22
23 import org.openhab.core.OpenHAB;
24 import org.openhab.core.audio.AudioException;
25 import org.openhab.core.audio.AudioFormat;
26 import org.openhab.core.audio.AudioStream;
27 import org.openhab.core.config.core.ConfigurableService;
28 import org.openhab.core.voice.TTSException;
29 import org.openhab.core.voice.TTSService;
30 import org.openhab.core.voice.Voice;
31 import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
32 import org.osgi.framework.Constants;
33 import org.osgi.service.component.annotations.Component;
34 import org.osgi.service.component.annotations.Modified;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * This is a TTS service implementation for using VoiceRSS TTS service.
40  *
41  * @author Jochen Hiller - Initial contribution and API
42  * @author Laurent Garnier - add support for OGG and AAC audio formats
43  */
44 @Component(configurationPid = "org.openhab.voicerss", property = { Constants.SERVICE_PID + "=org.openhab.voicerss",
45         ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=voice:voicerss",
46         ConfigurableService.SERVICE_PROPERTY_LABEL + "=VoiceRSS Text-to-Speech",
47         ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=voice" })
48 public class VoiceRSSTTSService implements TTSService {
49
50     /** Cache folder name is below userdata/voicerss/cache. */
51     private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
52
53     // API Key comes from ConfigAdmin
54     private static final String CONFIG_API_KEY = "apiKey";
55     private String apiKey;
56
57     private final Logger logger = LoggerFactory.getLogger(VoiceRSSTTSService.class);
58
59     /**
60      * We need the cached implementation to allow for FixedLengthAudioStream.
61      */
62     private CachedVoiceRSSCloudImpl voiceRssImpl;
63
64     /**
65      * Set of supported voices
66      */
67     private Set<Voice> voices;
68
69     /**
70      * Set of supported audio formats
71      */
72     private Set<AudioFormat> audioFormats;
73
74     /**
75      * DS activate, with access to ConfigAdmin
76      */
77     protected void activate(Map<String, Object> config) {
78         try {
79             modified(config);
80             voiceRssImpl = initVoiceImplementation();
81             voices = initVoices();
82             audioFormats = initAudioFormats();
83
84             logger.debug("Using VoiceRSS cache folder {}", getCacheFolderName());
85         } catch (IllegalStateException e) {
86             logger.error("Failed to activate VoiceRSS: {}", e.getMessage(), e);
87         }
88     }
89
90     @Modified
91     protected void modified(Map<String, Object> config) {
92         if (config != null) {
93             apiKey = config.containsKey(CONFIG_API_KEY) ? config.get(CONFIG_API_KEY).toString() : null;
94         }
95     }
96
97     @Override
98     public Set<Voice> getAvailableVoices() {
99         return Collections.unmodifiableSet(voices);
100     }
101
102     @Override
103     public Set<AudioFormat> getSupportedFormats() {
104         return Collections.unmodifiableSet(audioFormats);
105     }
106
107     @Override
108     public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException {
109         logger.debug("Synthesize '{}' for voice '{}' in format {}", text, voice.getUID(), requestedFormat);
110         // Validate known api key
111         if (apiKey == null) {
112             throw new TTSException("Missing API key, configure it first before using");
113         }
114         // Validate arguments
115         if (text == null) {
116             throw new TTSException("The passed text is null");
117         }
118         // trim text
119         String trimmedText = text.trim();
120         if (trimmedText.isEmpty()) {
121             throw new TTSException("The passed text is empty");
122         }
123         if (!voices.contains(voice)) {
124             throw new TTSException("The passed voice is unsupported");
125         }
126         boolean isAudioFormatSupported = false;
127         for (AudioFormat currentAudioFormat : audioFormats) {
128             if (currentAudioFormat.isCompatible(requestedFormat)) {
129                 isAudioFormatSupported = true;
130                 break;
131             }
132         }
133         if (!isAudioFormatSupported) {
134             throw new TTSException("The passed AudioFormat is unsupported");
135         }
136
137         // now create the input stream for given text, locale, format. There is
138         // only a default voice
139         try {
140             File cacheAudioFile = voiceRssImpl.getTextToSpeechAsFile(apiKey, trimmedText,
141                     voice.getLocale().toLanguageTag(), getApiAudioFormat(requestedFormat));
142             if (cacheAudioFile == null) {
143                 throw new TTSException("Could not read from VoiceRSS service");
144             }
145             return new VoiceRSSAudioStream(cacheAudioFile, requestedFormat);
146         } catch (AudioException ex) {
147             throw new TTSException("Could not create AudioStream: " + ex.getMessage(), ex);
148         } catch (IOException ex) {
149             throw new TTSException("Could not read from VoiceRSS service: " + ex.getMessage(), ex);
150         }
151     }
152
153     /**
154      * Initializes voices.
155      *
156      * @return The voices of this instance
157      */
158     private Set<Voice> initVoices() {
159         Set<Voice> voices = new HashSet<>();
160         for (Locale locale : voiceRssImpl.getAvailableLocales()) {
161             for (String voiceLabel : voiceRssImpl.getAvailableVoices(locale)) {
162                 voices.add(new VoiceRSSVoice(locale, voiceLabel));
163             }
164         }
165         return voices;
166     }
167
168     /**
169      * Initializes audioFormats
170      *
171      * @return The audio formats of this instance
172      */
173     private Set<AudioFormat> initAudioFormats() {
174         Set<AudioFormat> audioFormats = new HashSet<>();
175         for (String format : voiceRssImpl.getAvailableAudioFormats()) {
176             audioFormats.add(getAudioFormat(format));
177         }
178         return audioFormats;
179     }
180
181     private AudioFormat getAudioFormat(String apiFormat) {
182         Boolean bigEndian = null;
183         Integer bitDepth = 16;
184         Integer bitRate = null;
185         Long frequency = 44100L;
186
187         if ("MP3".equals(apiFormat)) {
188             // we use by default: MP3, 44khz_16bit_mono with bitrate 64 kbps
189             bitRate = 64000;
190             return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, bigEndian, bitDepth, bitRate,
191                     frequency);
192         } else if ("OGG".equals(apiFormat)) {
193             // we use by default: OGG, 44khz_16bit_mono
194             return new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, bigEndian, bitDepth, bitRate,
195                     frequency);
196         } else if ("AAC".equals(apiFormat)) {
197             // we use by default: AAC, 44khz_16bit_mono
198             return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, bigEndian, bitDepth, bitRate,
199                     frequency);
200         } else {
201             throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
202         }
203     }
204
205     private String getApiAudioFormat(AudioFormat format) {
206         if (format.getCodec().equals(AudioFormat.CODEC_MP3)) {
207             return "MP3";
208         } else if (format.getCodec().equals(AudioFormat.CODEC_VORBIS)) {
209             return "OGG";
210         } else if (format.getCodec().equals(AudioFormat.CODEC_AAC)) {
211             return "AAC";
212         } else {
213             throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
214         }
215     }
216
217     private CachedVoiceRSSCloudImpl initVoiceImplementation() {
218         return new CachedVoiceRSSCloudImpl(getCacheFolderName());
219     }
220
221     private String getCacheFolderName() {
222         // we assume that this folder does NOT have a trailing separator
223         return OpenHAB.getUserDataFolder() + File.separator + CACHE_FOLDER_NAME;
224     }
225
226     @Override
227     public String getId() {
228         return "voicerss";
229     }
230
231     @Override
232     public String getLabel(Locale locale) {
233         return "VoiceRSS";
234     }
235 }