]> git.basschouten.com Git - openhab-addons.git/blob
b83dcdae4ba150a408a1d08b8a7e763a8e38413d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.mactts.internal;
14
15 import java.io.File;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
20
21 import org.openhab.core.audio.AudioException;
22 import org.openhab.core.audio.AudioFormat;
23 import org.openhab.core.audio.AudioStream;
24 import org.openhab.core.audio.FixedLengthAudioStream;
25 import org.openhab.core.common.Disposable;
26 import org.openhab.core.voice.Voice;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * Implementation of the {@link AudioStream} interface for the {@link MacTTSService}
32  *
33  * @author Kelly Davis - Initial contribution and API
34  * @author Kai Kreuzer - Refactored to use AudioStream and fixed audio format to produce
35  * @author Laurent Garnier - Add dispose method to delete the temporary file
36  */
37 class MacTTSAudioStream extends FixedLengthAudioStream implements Disposable {
38
39     private final Logger logger = LoggerFactory.getLogger(MacTTSAudioStream.class);
40
41     /**
42      * {@link Voice} this {@link AudioStream} speaks in
43      */
44     private final Voice voice;
45
46     /**
47      * Text spoken in this {@link AudioStream}
48      */
49     private final String text;
50
51     /**
52      * {@link AudioFormat} of this {@link AudioStream}
53      */
54     private final AudioFormat audioFormat;
55
56     /**
57      * The raw input stream
58      */
59     private InputStream inputStream;
60
61     private long length;
62     private File file;
63
64     /**
65      * Constructs an instance with the passed properties.
66      *
67      * It is assumed that the passed properties have been validated.
68      *
69      * @param text The text spoken in this {@link AudioStream}
70      * @param voice The {@link Voice} used to speak this instance's text
71      * @param audioFormat The {@link AudioFormat} of this {@link AudioStream}
72      * @throws AudioException if stream cannot be created
73      */
74     public MacTTSAudioStream(String text, Voice voice, AudioFormat audioFormat) throws AudioException {
75         this.text = text;
76         this.voice = voice;
77         this.audioFormat = audioFormat;
78         this.inputStream = createInputStream();
79     }
80
81     @Override
82     public AudioFormat getFormat() {
83         return audioFormat;
84     }
85
86     private InputStream createInputStream() throws AudioException {
87         String outputFile = generateOutputFilename();
88         String command = getCommand(outputFile);
89         logger.debug("Executing on command line: {}", command);
90
91         try {
92             Process process = Runtime.getRuntime().exec(command);
93             process.waitFor();
94             file = new File(outputFile);
95             if (!file.exists()) {
96                 throw new AudioException("Generated file '" + outputFile + "' does not exist.'");
97             }
98             this.length = file.length();
99             if (this.length == 0) {
100                 throw new AudioException("Generated file '" + outputFile + "' has no content.'");
101             }
102             return getFileInputStream(file);
103         } catch (IOException e) {
104             throw new AudioException("Error while executing '" + command + "'", e);
105         } catch (InterruptedException e) {
106             throw new AudioException("The '" + command + "' has been interrupted", e);
107         }
108     }
109
110     private InputStream getFileInputStream(File file) throws AudioException {
111         if (file == null) {
112             throw new IllegalArgumentException("file must not be null");
113         }
114         if (file.exists()) {
115             try {
116                 return new FileInputStream(file);
117             } catch (FileNotFoundException e) {
118                 throw new AudioException("Cannot open temporary audio file '" + file.getName() + ".");
119             }
120         } else {
121             throw new AudioException("Temporary file '" + file.getName() + "' not found!");
122         }
123     }
124
125     /**
126      * Generates a unique, absolute output filename
127      *
128      * @return Unique, absolute output filename
129      */
130     private String generateOutputFilename() throws AudioException {
131         File tempFile;
132         try {
133             tempFile = File.createTempFile(Integer.toString(text.hashCode()), ".wav");
134             tempFile.deleteOnExit();
135         } catch (IOException e) {
136             throw new AudioException("Unable to create temp file.", e);
137         }
138         return tempFile.getAbsolutePath();
139     }
140
141     /**
142      * Gets the command used to generate an audio file {@code outputFile}
143      *
144      * @param outputFile The absolute filename of the command's output
145      * @return The command used to generate the audio file {@code outputFile}
146      */
147     private String getCommand(String outputFile) {
148         StringBuffer stringBuffer = new StringBuffer();
149
150         stringBuffer.append("say");
151
152         stringBuffer.append(" --voice=" + this.voice.getLabel());
153         stringBuffer.append(" --output-file=" + outputFile);
154         stringBuffer.append(" --file-format=" + this.audioFormat.getContainer());
155         stringBuffer.append(" --data-format=LEI" + audioFormat.getBitDepth() + "@" + audioFormat.getFrequency());
156         stringBuffer.append(" --channels=1"); // Mono
157         stringBuffer.append(" " + this.text);
158
159         return stringBuffer.toString();
160     }
161
162     @Override
163     public int read() throws IOException {
164         return inputStream.read();
165     }
166
167     @Override
168     public long length() {
169         return length;
170     }
171
172     @Override
173     public InputStream getClonedStream() throws AudioException {
174         if (file != null) {
175             return getFileInputStream(file);
176         } else {
177             throw new AudioException("No temporary audio file available.");
178         }
179     }
180
181     @Override
182     public void dispose() throws IOException {
183         if (file != null && file.exists()) {
184             try {
185                 if (!file.delete()) {
186                     logger.warn("Failed to delete the file {}", file.getAbsolutePath());
187                 }
188             } catch (SecurityException e) {
189                 logger.warn("Failed to delete the file {}: {}", file.getAbsolutePath(), e.getMessage());
190             }
191         }
192     }
193 }