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