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 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;
26 import javax.sound.sampled.AudioInputStream;
27 import javax.sound.sampled.UnsupportedAudioFileException;
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;
43 * The audio sink for openhab, implemented by a connection to a pulseaudio sink
45 * @author Gwendal Roulleau - Initial contribution
49 public class PulseAudioAudioSink implements AudioSink {
51 private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
53 private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
54 private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
56 private PulseaudioHandler pulseaudioHandler;
57 private ScheduledExecutorService scheduler;
59 private @Nullable Socket clientSocket;
61 private boolean isIdle = true;
64 SUPPORTED_FORMATS.add(AudioFormat.WAV);
65 SUPPORTED_FORMATS.add(AudioFormat.MP3);
66 SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
69 public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
70 this.pulseaudioHandler = pulseaudioHandler;
71 this.scheduler = scheduler;
75 public String getId() {
76 return pulseaudioHandler.getThing().getUID().toString();
80 public @Nullable String getLabel(@Nullable Locale locale) {
81 return pulseaudioHandler.getThing().getLabel();
85 * Convert MP3 to PCM, as this is the only possible format
90 private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
92 MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
93 AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
94 javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
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);
101 return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
103 } catch (IOException | UnsupportedAudioFileException e) {
104 logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
110 * Connect to pulseaudio with the simple protocol
112 * @throws IOException
113 * @throws InterruptedException when interrupted during the loading module wait
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);
126 * Disconnect the socket to pulseaudio simple protocol
128 public void disconnect() {
129 if (clientSocket != null && isIdle) {
130 logger.debug("Disconnecting");
132 clientSocket.close();
133 } catch (IOException e) {
136 logger.debug("Stream still running or socket not open");
141 public void process(@Nullable AudioStream audioStream)
142 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
144 if (audioStream == null) {
148 InputStream audioInputStream = null;
151 if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
152 audioInputStream = getPCMStreamFromMp3Stream(audioStream);
153 } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
154 audioInputStream = audioStream;
156 throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
157 audioStream.getFormat());
160 for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
163 if (audioInputStream != null && clientSocket != null) {
164 // send raw audio to the socket and to pulse audio
166 audioInputStream.transferTo(clientSocket.getOutputStream());
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";
175 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
176 pulseaudioHandler.getHost(), port, e.getMessage());
180 } catch (InterruptedException ie) {
181 logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
187 if (audioInputStream != null) {
188 audioInputStream.close();
191 scheduleDisconnect();
192 } catch (IOException e) {
198 public void scheduleDisconnect() {
199 logger.debug("Scheduling disconnect");
200 scheduler.schedule(this::disconnect, pulseaudioHandler.getIdleTimeout(), TimeUnit.MILLISECONDS);
204 public Set<AudioFormat> getSupportedFormats() {
205 return SUPPORTED_FORMATS;
209 public Set<Class<? extends AudioStream>> getSupportedStreams() {
210 return SUPPORTED_STREAMS;
214 public PercentType getVolume() {
215 return new PercentType(pulseaudioHandler.getLastVolume());
219 public void setVolume(PercentType volume) {
220 pulseaudioHandler.setVolume(volume.intValue());