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.mactts.internal;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.file.Files;
22 import org.openhab.core.audio.AudioException;
23 import org.openhab.core.audio.AudioFormat;
24 import org.openhab.core.audio.AudioStream;
25 import org.openhab.core.audio.FixedLengthAudioStream;
26 import org.openhab.core.common.Disposable;
27 import org.openhab.core.voice.Voice;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * Implementation of the {@link AudioStream} interface for the {@link MacTTSService}
34 * @author Kelly Davis - Initial contribution and API
35 * @author Kai Kreuzer - Refactored to use AudioStream and fixed audio format to produce
36 * @author Laurent Garnier - Add dispose method to delete the temporary file
38 class MacTTSAudioStream extends FixedLengthAudioStream implements Disposable {
40 private final Logger logger = LoggerFactory.getLogger(MacTTSAudioStream.class);
43 * {@link Voice} this {@link AudioStream} speaks in
45 private final Voice voice;
48 * Text spoken in this {@link AudioStream}
50 private final String text;
53 * {@link AudioFormat} of this {@link AudioStream}
55 private final AudioFormat audioFormat;
58 * The raw input stream
60 private InputStream inputStream;
66 * Constructs an instance with the passed properties.
68 * It is assumed that the passed properties have been validated.
70 * @param text The text spoken in this {@link AudioStream}
71 * @param voice The {@link Voice} used to speak this instance's text
72 * @param audioFormat The {@link AudioFormat} of this {@link AudioStream}
73 * @throws AudioException if stream cannot be created
75 public MacTTSAudioStream(String text, Voice voice, AudioFormat audioFormat) throws AudioException {
78 this.audioFormat = audioFormat;
79 this.inputStream = createInputStream();
83 public AudioFormat getFormat() {
87 private InputStream createInputStream() throws AudioException {
88 String outputFile = generateOutputFilename();
89 String command = getCommand(outputFile);
90 logger.debug("Executing on command line: {}", command);
93 Process process = Runtime.getRuntime().exec(command);
95 file = new File(outputFile);
97 throw new AudioException("Generated file '" + outputFile + "' does not exist.'");
99 this.length = file.length();
100 if (this.length == 0) {
101 throw new AudioException("Generated file '" + outputFile + "' has no content.'");
103 return getFileInputStream(file);
104 } catch (IOException e) {
105 throw new AudioException("Error while executing '" + command + "'", e);
106 } catch (InterruptedException e) {
107 throw new AudioException("The '" + command + "' has been interrupted", e);
111 private InputStream getFileInputStream(File file) throws AudioException {
113 throw new IllegalArgumentException("file must not be null");
117 return new FileInputStream(file);
118 } catch (FileNotFoundException e) {
119 throw new AudioException("Cannot open temporary audio file '" + file.getName() + ".");
122 throw new AudioException("Temporary file '" + file.getName() + "' not found!");
127 * Generates a unique, absolute output filename
129 * @return Unique, absolute output filename
131 private String generateOutputFilename() throws AudioException {
134 tempFile = Files.createTempFile(Integer.toString(text.hashCode()), ".wav").toFile();
135 tempFile.deleteOnExit();
136 } catch (IOException e) {
137 throw new AudioException("Unable to create temp file.", e);
139 return tempFile.getAbsolutePath();
143 * Gets the command used to generate an audio file {@code outputFile}
145 * @param outputFile The absolute filename of the command's output
146 * @return The command used to generate the audio file {@code outputFile}
148 private String getCommand(String outputFile) {
149 StringBuffer stringBuffer = new StringBuffer();
151 stringBuffer.append("say");
153 stringBuffer.append(" --voice=" + this.voice.getLabel());
154 stringBuffer.append(" --output-file=" + outputFile);
155 stringBuffer.append(" --file-format=" + this.audioFormat.getContainer());
156 stringBuffer.append(" --data-format=LEI" + audioFormat.getBitDepth() + "@" + audioFormat.getFrequency());
157 stringBuffer.append(" --channels=1"); // Mono
158 stringBuffer.append(" " + this.text);
160 return stringBuffer.toString();
164 public int read() throws IOException {
165 return inputStream.read();
169 public long length() {
174 public InputStream getClonedStream() throws AudioException {
176 return getFileInputStream(file);
178 throw new AudioException("No temporary audio file available.");
183 public void dispose() throws IOException {
184 if (file != null && file.exists()) {
186 if (!file.delete()) {
187 logger.warn("Failed to delete the file {}", file.getAbsolutePath());
189 } catch (SecurityException e) {
190 logger.warn("Failed to delete the file {}: {}", file.getAbsolutePath(), e.getMessage());