]> git.basschouten.com Git - openhab-addons.git/blob
c6bfeb29e079506dc17ed6b2d96d11b59a5a491a
[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 import java.nio.file.Files;
21
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;
30
31 /**
32  * Implementation of the {@link AudioStream} interface for the {@link MacTTSService}
33  *
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
37  */
38 class MacTTSAudioStream extends FixedLengthAudioStream implements Disposable {
39
40     private final Logger logger = LoggerFactory.getLogger(MacTTSAudioStream.class);
41
42     /**
43      * {@link Voice} this {@link AudioStream} speaks in
44      */
45     private final Voice voice;
46
47     /**
48      * Text spoken in this {@link AudioStream}
49      */
50     private final String text;
51
52     /**
53      * {@link AudioFormat} of this {@link AudioStream}
54      */
55     private final AudioFormat audioFormat;
56
57     /**
58      * The raw input stream
59      */
60     private InputStream inputStream;
61
62     private long length;
63     private File file;
64
65     /**
66      * Constructs an instance with the passed properties.
67      *
68      * It is assumed that the passed properties have been validated.
69      *
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
74      */
75     public MacTTSAudioStream(String text, Voice voice, AudioFormat audioFormat) throws AudioException {
76         this.text = text;
77         this.voice = voice;
78         this.audioFormat = audioFormat;
79         this.inputStream = createInputStream();
80     }
81
82     @Override
83     public AudioFormat getFormat() {
84         return audioFormat;
85     }
86
87     private InputStream createInputStream() throws AudioException {
88         String outputFile = generateOutputFilename();
89         String command = getCommand(outputFile);
90         logger.debug("Executing on command line: {}", command);
91
92         try {
93             Process process = Runtime.getRuntime().exec(command);
94             process.waitFor();
95             file = new File(outputFile);
96             if (!file.exists()) {
97                 throw new AudioException("Generated file '" + outputFile + "' does not exist.'");
98             }
99             this.length = file.length();
100             if (this.length == 0) {
101                 throw new AudioException("Generated file '" + outputFile + "' has no content.'");
102             }
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);
108         }
109     }
110
111     private InputStream getFileInputStream(File file) throws AudioException {
112         if (file == null) {
113             throw new IllegalArgumentException("file must not be null");
114         }
115         if (file.exists()) {
116             try {
117                 return new FileInputStream(file);
118             } catch (FileNotFoundException e) {
119                 throw new AudioException("Cannot open temporary audio file '" + file.getName() + ".");
120             }
121         } else {
122             throw new AudioException("Temporary file '" + file.getName() + "' not found!");
123         }
124     }
125
126     /**
127      * Generates a unique, absolute output filename
128      *
129      * @return Unique, absolute output filename
130      */
131     private String generateOutputFilename() throws AudioException {
132         File tempFile;
133         try {
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);
138         }
139         return tempFile.getAbsolutePath();
140     }
141
142     /**
143      * Gets the command used to generate an audio file {@code outputFile}
144      *
145      * @param outputFile The absolute filename of the command's output
146      * @return The command used to generate the audio file {@code outputFile}
147      */
148     private String getCommand(String outputFile) {
149         StringBuffer stringBuffer = new StringBuffer();
150
151         stringBuffer.append("say");
152
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);
159
160         return stringBuffer.toString();
161     }
162
163     @Override
164     public int read() throws IOException {
165         return inputStream.read();
166     }
167
168     @Override
169     public long length() {
170         return length;
171     }
172
173     @Override
174     public InputStream getClonedStream() throws AudioException {
175         if (file != null) {
176             return getFileInputStream(file);
177         } else {
178             throw new AudioException("No temporary audio file available.");
179         }
180     }
181
182     @Override
183     public void dispose() throws IOException {
184         if (file != null && file.exists()) {
185             try {
186                 if (!file.delete()) {
187                     logger.warn("Failed to delete the file {}", file.getAbsolutePath());
188                 }
189             } catch (SecurityException e) {
190                 logger.warn("Failed to delete the file {}: {}", file.getAbsolutePath(), e.getMessage());
191             }
192         }
193     }
194 }