2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pulseaudio.internal;
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;
21 import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
22 import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
24 import javax.sound.sampled.AudioInputStream;
25 import javax.sound.sampled.UnsupportedAudioFileException;
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;
41 * The audio sink for openhab, implemented by a connection to a pulseaudio sink
43 * @author Gwendal Roulleau - Initial contribution
47 public class PulseAudioAudioSink implements AudioSink {
49 private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
51 private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
52 private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
54 private PulseaudioHandler pulseaudioHandler;
56 private @Nullable Socket clientSocket;
59 SUPPORTED_FORMATS.add(AudioFormat.WAV);
60 SUPPORTED_FORMATS.add(AudioFormat.MP3);
61 SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
64 public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) {
65 this.pulseaudioHandler = pulseaudioHandler;
69 public String getId() {
70 return pulseaudioHandler.getThing().getUID().toString();
74 public @Nullable String getLabel(@Nullable Locale locale) {
75 return pulseaudioHandler.getThing().getLabel();
79 * Convert MP3 to PCM, as this is the only possible format
84 private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
86 MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
87 AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
88 javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
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);
95 return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
97 } catch (IOException | UnsupportedAudioFileException e) {
98 logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
104 * Connect to pulseaudio with the simple protocol
106 * @throws IOException
107 * @throws InterruptedException when interrupted during the loading module wait
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);
120 * Disconnect the socket to pulseaudio simple protocol
122 public void disconnect() {
123 if (clientSocket != null) {
125 clientSocket.close();
126 } catch (IOException e) {
132 public void process(@Nullable AudioStream audioStream)
133 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
135 if (audioStream == null) {
139 InputStream audioInputStream = null;
142 if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
143 audioInputStream = getPCMStreamFromMp3Stream(audioStream);
144 } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
145 audioInputStream = audioStream;
147 throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
148 audioStream.getFormat());
151 for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
154 if (audioInputStream != null && clientSocket != null) {
155 // send raw audio to the socket and to pulse audio
156 audioInputStream.transferTo(clientSocket.getOutputStream());
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";
165 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
166 pulseaudioHandler.getHost(), port, e.getMessage());
170 } catch (InterruptedException ie) {
171 logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
177 if (audioInputStream != null) {
178 audioInputStream.close();
181 } catch (IOException e) {
187 public Set<AudioFormat> getSupportedFormats() {
188 return SUPPORTED_FORMATS;
192 public Set<Class<? extends AudioStream>> getSupportedStreams() {
193 return SUPPORTED_STREAMS;
197 public PercentType getVolume() {
198 return new PercentType(pulseaudioHandler.getLastVolume());
202 public void setVolume(PercentType volume) {
203 pulseaudioHandler.setVolume(volume.intValue());