]> git.basschouten.com Git - openhab-addons.git/blob
6f3756eef9990046fa28fb764056aa3501d2a200
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.binding.pulseaudio.internal;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.net.Socket;
18 import java.util.HashSet;
19 import java.util.Locale;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeUnit;
23 import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
24 import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
25
26 import javax.sound.sampled.AudioInputStream;
27 import javax.sound.sampled.UnsupportedAudioFileException;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
32 import org.openhab.core.audio.AudioFormat;
33 import org.openhab.core.audio.AudioSink;
34 import org.openhab.core.audio.AudioStream;
35 import org.openhab.core.audio.FixedLengthAudioStream;
36 import org.openhab.core.audio.UnsupportedAudioFormatException;
37 import org.openhab.core.audio.UnsupportedAudioStreamException;
38 import org.openhab.core.library.types.PercentType;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * The audio sink for openhab, implemented by a connection to a pulseaudio sink
44  *
45  * @author Gwendal Roulleau - Initial contribution
46  *
47  */
48 @NonNullByDefault
49 public class PulseAudioAudioSink implements AudioSink {
50
51     private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
52
53     private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
54     private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
55
56     private PulseaudioHandler pulseaudioHandler;
57     private ScheduledExecutorService scheduler;
58
59     private @Nullable Socket clientSocket;
60
61     private boolean isIdle = true;
62
63     static {
64         SUPPORTED_FORMATS.add(AudioFormat.WAV);
65         SUPPORTED_FORMATS.add(AudioFormat.MP3);
66         SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
67     }
68
69     public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
70         this.pulseaudioHandler = pulseaudioHandler;
71         this.scheduler = scheduler;
72     }
73
74     @Override
75     public String getId() {
76         return pulseaudioHandler.getThing().getUID().toString();
77     }
78
79     @Override
80     public @Nullable String getLabel(@Nullable Locale locale) {
81         return pulseaudioHandler.getThing().getLabel();
82     }
83
84     /**
85      * Convert MP3 to PCM, as this is the only possible format
86      *
87      * @param input
88      * @return
89      */
90     private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
91         try {
92             MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
93             AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
94             javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
95
96             MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
97             javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
98                     javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
99                     sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
100
101             return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
102
103         } catch (IOException | UnsupportedAudioFileException e) {
104             logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
105         }
106         return null;
107     }
108
109     /**
110      * Connect to pulseaudio with the simple protocol
111      *
112      * @throws IOException
113      * @throws InterruptedException when interrupted during the loading module wait
114      */
115     public void connectIfNeeded() throws IOException, InterruptedException {
116         Socket clientSocketLocal = clientSocket;
117         if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
118             String host = pulseaudioHandler.getHost();
119             int port = pulseaudioHandler.getSimpleTcpPort();
120             clientSocket = new Socket(host, port);
121             clientSocket.setSoTimeout(500);
122         }
123     }
124
125     /**
126      * Disconnect the socket to pulseaudio simple protocol
127      */
128     public void disconnect() {
129         if (clientSocket != null && isIdle) {
130             logger.debug("Disconnecting");
131             try {
132                 clientSocket.close();
133             } catch (IOException e) {
134             }
135         } else {
136             logger.debug("Stream still running or socket not open");
137         }
138     }
139
140     @Override
141     public void process(@Nullable AudioStream audioStream)
142             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
143
144         if (audioStream == null) {
145             return;
146         }
147
148         InputStream audioInputStream = null;
149         try {
150
151             if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
152                 audioInputStream = getPCMStreamFromMp3Stream(audioStream);
153             } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
154                 audioInputStream = audioStream;
155             } else {
156                 throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
157                         audioStream.getFormat());
158             }
159
160             for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
161                 try {
162                     connectIfNeeded();
163                     if (audioInputStream != null && clientSocket != null) {
164                         // send raw audio to the socket and to pulse audio
165                         isIdle = false;
166                         audioInputStream.transferTo(clientSocket.getOutputStream());
167                         break;
168                     }
169                 } catch (IOException e) {
170                     disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
171                     if (countAttempt == 2) { // we won't retry : log and quit
172                         if (logger.isWarnEnabled()) {
173                             String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
174                             logger.warn(
175                                     "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
176                                     pulseaudioHandler.getHost(), port, e.getMessage());
177                         }
178                         break;
179                     }
180                 } catch (InterruptedException ie) {
181                     logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
182                     break;
183                 }
184             }
185         } finally {
186             try {
187                 if (audioInputStream != null) {
188                     audioInputStream.close();
189                 }
190                 audioStream.close();
191                 scheduleDisconnect();
192             } catch (IOException e) {
193             }
194         }
195         isIdle = true;
196     }
197
198     public void scheduleDisconnect() {
199         logger.debug("Scheduling disconnect");
200         scheduler.schedule(this::disconnect, pulseaudioHandler.getIdleTimeout(), TimeUnit.MILLISECONDS);
201     }
202
203     @Override
204     public Set<AudioFormat> getSupportedFormats() {
205         return SUPPORTED_FORMATS;
206     }
207
208     @Override
209     public Set<Class<? extends AudioStream>> getSupportedStreams() {
210         return SUPPORTED_STREAMS;
211     }
212
213     @Override
214     public PercentType getVolume() {
215         return new PercentType(pulseaudioHandler.getLastVolume());
216     }
217
218     @Override
219     public void setVolume(PercentType volume) {
220         pulseaudioHandler.setVolume(volume.intValue());
221     }
222 }