Skip to content

Commit

Permalink
[pulseaudio] Add pulseaudio sink as openhab audio sink (openhab#1895) (
Browse files Browse the repository at this point in the history
…openhab#10423)

* [pulseaudio] Add pulseaudio sink as openhab audio sink (openhab#1895)

This add to the pulseaudio binding the capability to use "pulseaudio sink" as an "openhab sink" to output sound from openhab to a pulse audio server on the network.
You need to load module-simple-protocol-tcp sink in addition to the usual module-cli-protocol-tcp, and enable the sink in the thing configuration.

Closes openhab#1895

Signed-off-by: Gwendal Roulleau <[email protected]>

* Small corrections after review

And getting rid of some other compilation warnings
Signed-off-by: Gwendal Roulleau <[email protected]>

* Fix some registration errors  and allow the binding to load the simple module remotely

Signed-off-by: Gwendal Roulleau <[email protected]>

* Small corrections after reviews

initialize audiosink in a thread with scheduler.submit
clear some warning related code.

Signed-off-by: Gwendal Roulleau <[email protected]>
Better interruptexception handling

* Fix two small concurrency bugs

Signed-off-by: Gwendal Roulleau <[email protected]>

Co-authored-by: Gwendal Roulleau <[email protected]>
Signed-off-by: John Marshall <[email protected]>
  • Loading branch information
2 people authored and themillhousegroup committed May 10, 2021
1 parent 9ee32b7 commit 25185a8
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 40 deletions.
10 changes: 8 additions & 2 deletions bundles/org.openhab.binding.pulseaudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This binding integrates pulseaudio devices.

The Pulseaudio bridge is required as a "bridge" for accessing any other Pulseaudio devices.

You need a running pulseaudio server whith module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:
You need a running pulseaudio server with module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:

* Sink
* Source
Expand Down Expand Up @@ -35,12 +35,18 @@ All devices support some of the following channels:
| slaves | String | Slave sinks of a combined sink |
| routeToSink | String | Shows the sink a sink-input is currently routed to |

## Audio sink

Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported.
Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink).
This requires the module **module-simple-protocol-tcp** to be present on the server which runs your openHAB instance. The binding will try to command (if not discovered first) the load of this module on the pulseaudio server.

## Full Example
### pulseaudio.things
```
Bridge pulseaudio:bridge:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", port=4712 ] {
Things:
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3"] // this name corresponds to pactl list-sinks output
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the name corresponds to pactl list-sinks output
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"]
Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
Expand Down
21 changes: 21 additions & 0 deletions bundles/org.openhab.binding.pulseaudio/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,25 @@

<name>openHAB Add-ons :: Bundles :: Pulseaudio Binding</name>

<dependencies>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId>
<version>1.9.5.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>jlayer</artifactId>
<version>1.0.1.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>tritonus-share</artifactId>
<version>0.3.7.4</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
<feature>openhab-transport-mdns</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pulseaudio/${project.version}</bundle>
<bundle dependency="true">mvn:com.googlecode.soundlibs/tritonus-share/0.3.7.4</bundle>
<bundle dependency="true">mvn:com.googlecode.soundlibs/mp3spi/1.9.5.4</bundle>
<bundle dependency="true">mvn:com.googlecode.soundlibs/jlayer/1.0.1.4</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.UnsupportedAudioFileException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The audio sink for openhab, implemented by a connection to a pulseaudio sink
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public class PulseAudioAudioSink implements AudioSink {

private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);

private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();

private PulseaudioHandler pulseaudioHandler;

private @Nullable Socket clientSocket;

static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
}

public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) {
this.pulseaudioHandler = pulseaudioHandler;
}

@Override
public String getId() {
return pulseaudioHandler.getThing().getUID().toString();
}

@Override
public @Nullable String getLabel(@Nullable Locale locale) {
return pulseaudioHandler.getThing().getLabel();
}

/**
* Convert MP3 to PCM, as this is the only possible format
*
* @param input
* @return
*/
private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
try {
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();

MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);

return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);

} catch (IOException | UnsupportedAudioFileException e) {
logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
}
return null;
}

/**
* Connect to pulseaudio with the simple protocol
*
* @throws IOException
* @throws InterruptedException when interrupted during the loading module wait
*/
public void connectIfNeeded() throws IOException, InterruptedException {
Socket clientSocketLocal = clientSocket;
if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
String host = pulseaudioHandler.getHost();
int port = pulseaudioHandler.getSimpleTcpPort();
clientSocket = new Socket(host, port);
clientSocket.setSoTimeout(500);
}
}

/**
* Disconnect the socket to pulseaudio simple protocol
*/
public void disconnect() {
if (clientSocket != null) {
try {
clientSocket.close();
} catch (IOException e) {
}
}
}

@Override
public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {

if (audioStream == null) {
return;
}

InputStream audioInputStream = null;
try {

if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
audioInputStream = getPCMStreamFromMp3Stream(audioStream);
} else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
audioInputStream = audioStream;
} else {
throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
audioStream.getFormat());
}

for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
try {
connectIfNeeded();
if (audioInputStream != null && clientSocket != null) {
// send raw audio to the socket and to pulse audio
audioInputStream.transferTo(clientSocket.getOutputStream());
break;
}
} catch (IOException e) {
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit
if (logger.isWarnEnabled()) {
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
logger.warn(
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage());
}
break;
}
} catch (InterruptedException ie) {
logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
break;
}
}
} finally {
try {
if (audioInputStream != null) {
audioInputStream.close();
}
audioStream.close();
} catch (IOException e) {
}
}
}

@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_FORMATS;
}

@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}

@Override
public PercentType getVolume() {
return new PercentType(pulseaudioHandler.getLastVolume());
}

@Override
public void setVolume(PercentType volume) {
pulseaudioHandler.setVolume(volume.intValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public class PulseaudioBindingConstants {
public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh";

public static final String DEVICE_PARAMETER_NAME = "name";
public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";

public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;

public static final Map<String, Boolean> TYPE_FILTERS = new HashMap<>();

Expand Down
Loading

0 comments on commit 25185a8

Please sign in to comment.