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