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.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;
30 import marytts.LocalMaryInterface;
31 import marytts.MaryInterface;
32 import marytts.exceptions.MaryConfigurationException;
33 import marytts.exceptions.SynthesisException;
34 import marytts.modules.synthesis.Voice;
37 * This is a TTS service implementation for using MaryTTS.
39 * @author Kelly Davis - Initial contribution and API
40 * @author Kai Kreuzer - Refactored to updated APIs and moved to openHAB
43 public class MaryTTSService implements TTSService {
45 private final Logger logger = LoggerFactory.getLogger(MaryTTSService.class);
47 private MaryInterface marytts;
50 * Set of supported voices
52 private Set<org.openhab.core.voice.Voice> voices;
55 * Set of supported audio formats
57 private Set<AudioFormat> audioFormats;
59 protected void activate() {
61 marytts = new LocalMaryInterface();
62 voices = initVoices();
63 audioFormats = initAudioFormats();
64 } catch (MaryConfigurationException e) {
65 logger.error("Failed to initialize MaryTTS: {}", e.getMessage(), e);
70 public Set<org.openhab.core.voice.Voice> getAvailableVoices() {
75 public Set<AudioFormat> getSupportedFormats() {
80 public AudioStream synthesize(String text, org.openhab.core.voice.Voice voice, AudioFormat requestedFormat)
83 if (text == null || text.isEmpty()) {
84 throw new TTSException("The passed text is null or empty");
86 if (!voices.contains(voice)) {
87 throw new TTSException("The passed voice is unsupported");
89 if (audioFormats.stream().noneMatch(f -> f.isCompatible(requestedFormat))) {
90 throw new TTSException("The passed AudioFormat is unsupported");
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.
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.
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");
114 Voice maryTTSVoice = Voice.getVoice(voice.getLabel());
115 AudioFormat maryTTSVoiceAudioFormat = getAudioFormat(maryTTSVoice.dbAudioFormat());
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());
124 return new MaryTTSAudioStream(marytts.generateAudio(text), maryTTSVoiceAudioFormat);
125 } catch (SynthesisException | IOException e) {
126 throw new TTSException("Error generating an AudioStream", e);
134 * @return The voices of this instance
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));
147 * Initializes audioFormats
149 * @return The audio formats of this instance
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()));
160 * Obtains an AudioFormat from a javax.sound.sampled.AudioFormat
162 * @param audioFormat The javax.sound.sampled.AudioFormat
163 * @return The corresponding AudioFormat
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();
170 int frameSize = audioFormat.getFrameSize(); // In bytes
171 int bitsPerFrame = frameSize * 8;
172 Integer bitDepth = NOT_SPECIFIED == frameSize ? null : bitsPerFrame;
174 float frameRate = audioFormat.getFrameRate();
175 Integer bitRate = NOT_SPECIFIED == frameRate ? null : (int) frameRate * bitsPerFrame;
177 float sampleRate = audioFormat.getSampleRate();
178 Long frequency = NOT_SPECIFIED == sampleRate ? null : (long) sampleRate;
180 return new AudioFormat(container, codec, bigEndian, bitDepth, bitRate, frequency);
184 public String getId() {
189 public String getLabel(Locale locale) {