2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.voice.voicerss.internal;
16 import java.io.IOException;
17 import java.util.Collections;
18 import java.util.HashSet;
19 import java.util.Locale;
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;
39 * This is a TTS service implementation for using VoiceRSS TTS service.
41 * @author Jochen Hiller - Initial contribution and API
42 * @author Laurent Garnier - add support for OGG and AAC audio formats
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 {
50 /** Cache folder name is below userdata/voicerss/cache. */
51 private static final String CACHE_FOLDER_NAME = "voicerss" + File.separator + "cache";
53 // API Key comes from ConfigAdmin
54 private static final String CONFIG_API_KEY = "apiKey";
55 private String apiKey;
57 private final Logger logger = LoggerFactory.getLogger(VoiceRSSTTSService.class);
60 * We need the cached implementation to allow for FixedLengthAudioStream.
62 private CachedVoiceRSSCloudImpl voiceRssImpl;
65 * Set of supported voices
67 private Set<Voice> voices;
70 * Set of supported audio formats
72 private Set<AudioFormat> audioFormats;
75 * DS activate, with access to ConfigAdmin
77 protected void activate(Map<String, Object> config) {
80 voiceRssImpl = initVoiceImplementation();
81 voices = initVoices();
82 audioFormats = initAudioFormats();
84 logger.debug("Using VoiceRSS cache folder {}", getCacheFolderName());
85 } catch (IllegalStateException e) {
86 logger.error("Failed to activate VoiceRSS: {}", e.getMessage(), e);
91 protected void modified(Map<String, Object> config) {
93 apiKey = config.containsKey(CONFIG_API_KEY) ? config.get(CONFIG_API_KEY).toString() : null;
98 public Set<Voice> getAvailableVoices() {
99 return Collections.unmodifiableSet(voices);
103 public Set<AudioFormat> getSupportedFormats() {
104 return Collections.unmodifiableSet(audioFormats);
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");
114 // Validate arguments
116 throw new TTSException("The passed text is null");
119 String trimmedText = text.trim();
120 if (trimmedText.isEmpty()) {
121 throw new TTSException("The passed text is empty");
123 if (!voices.contains(voice)) {
124 throw new TTSException("The passed voice is unsupported");
126 boolean isAudioFormatSupported = false;
127 for (AudioFormat currentAudioFormat : audioFormats) {
128 if (currentAudioFormat.isCompatible(requestedFormat)) {
129 isAudioFormatSupported = true;
133 if (!isAudioFormatSupported) {
134 throw new TTSException("The passed AudioFormat is unsupported");
137 // now create the input stream for given text, locale, format. There is
138 // only a default voice
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");
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);
154 * Initializes voices.
156 * @return The voices of this instance
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));
169 * Initializes audioFormats
171 * @return The audio formats of this instance
173 private Set<AudioFormat> initAudioFormats() {
174 Set<AudioFormat> audioFormats = new HashSet<>();
175 for (String format : voiceRssImpl.getAvailableAudioFormats()) {
176 audioFormats.add(getAudioFormat(format));
181 private AudioFormat getAudioFormat(String apiFormat) {
182 Boolean bigEndian = null;
183 Integer bitDepth = 16;
184 Integer bitRate = null;
185 Long frequency = 44100L;
187 if ("MP3".equals(apiFormat)) {
188 // we use by default: MP3, 44khz_16bit_mono with bitrate 64 kbps
190 return new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, bigEndian, bitDepth, bitRate,
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,
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,
201 throw new IllegalArgumentException("Audio format " + apiFormat + " not yet supported");
205 private String getApiAudioFormat(AudioFormat format) {
206 if (format.getCodec().equals(AudioFormat.CODEC_MP3)) {
208 } else if (format.getCodec().equals(AudioFormat.CODEC_VORBIS)) {
210 } else if (format.getCodec().equals(AudioFormat.CODEC_AAC)) {
213 throw new IllegalArgumentException("Audio format " + format.getCodec() + " not yet supported");
217 private CachedVoiceRSSCloudImpl initVoiceImplementation() {
218 return new CachedVoiceRSSCloudImpl(getCacheFolderName());
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;
227 public String getId() {
232 public String getLabel(Locale locale) {