2 * Copyright (c) 2010-2023 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.marytts.internal;
15 import static javax.sound.sampled.AudioSystem.NOT_SPECIFIED;
17 import java.io.IOException;
18 import java.util.HashSet;
19 import java.util.Locale;
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;
34 import marytts.LocalMaryInterface;
35 import marytts.MaryInterface;
36 import marytts.exceptions.MaryConfigurationException;
37 import marytts.exceptions.SynthesisException;
38 import marytts.modules.synthesis.Voice;
41 * This is a TTS service implementation for using MaryTTS.
43 * @author Kelly Davis - Initial contribution and API
44 * @author Kai Kreuzer - Refactored to updated APIs and moved to openHAB
46 @Component(service = TTSService.class)
47 public class MaryTTSService extends AbstractCachedTTSService {
49 private final Logger logger = LoggerFactory.getLogger(MaryTTSService.class);
51 private MaryInterface marytts;
54 * Set of supported voices
56 private Set<org.openhab.core.voice.Voice> voices;
59 * Set of supported audio formats
61 private Set<AudioFormat> audioFormats;
64 public MaryTTSService(final @Reference TTSCache ttsCache) {
67 marytts = new LocalMaryInterface();
68 voices = initVoices();
69 audioFormats = initAudioFormats();
70 } catch (MaryConfigurationException e) {
71 logger.error("Failed to initialize MaryTTS: {}", e.getMessage(), e);
76 public Set<org.openhab.core.voice.Voice> getAvailableVoices() {
81 public Set<AudioFormat> getSupportedFormats() {
86 public AudioStream synthesizeForCache(String text, org.openhab.core.voice.Voice voice, AudioFormat requestedFormat)
89 if (text == null || text.isEmpty()) {
90 throw new TTSException("The passed text is null or empty");
92 if (!voices.contains(voice)) {
93 throw new TTSException("The passed voice is unsupported");
95 if (audioFormats.stream().noneMatch(f -> f.isCompatible(requestedFormat))) {
96 throw new TTSException("The passed AudioFormat is unsupported");
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.
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.
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");
120 Voice maryTTSVoice = Voice.getVoice(voice.getLabel());
121 AudioFormat maryTTSVoiceAudioFormat = getAudioFormat(maryTTSVoice.dbAudioFormat());
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());
130 return new MaryTTSAudioStream(marytts.generateAudio(text), maryTTSVoiceAudioFormat);
131 } catch (SynthesisException | IOException e) {
132 throw new TTSException("Error generating an AudioStream", e);
140 * @return The voices of this instance
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));
153 * Initializes audioFormats
155 * @return The audio formats of this instance
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()));
166 * Obtains an AudioFormat from a javax.sound.sampled.AudioFormat
168 * @param audioFormat The javax.sound.sampled.AudioFormat
169 * @return The corresponding AudioFormat
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();
176 int frameSize = audioFormat.getFrameSize(); // In bytes
177 int bitsPerFrame = frameSize * 8;
178 Integer bitDepth = NOT_SPECIFIED == frameSize ? null : bitsPerFrame;
180 float frameRate = audioFormat.getFrameRate();
181 Integer bitRate = NOT_SPECIFIED == frameRate ? null : (int) frameRate * bitsPerFrame;
183 float sampleRate = audioFormat.getSampleRate();
184 Long frequency = NOT_SPECIFIED == sampleRate ? null : (long) sampleRate;
186 return new AudioFormat(container, codec, bigEndian, bitDepth, bitRate, frequency);
190 public String getId() {
195 public String getLabel(Locale locale) {