Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pulseaudio] Add pulseaudio sink as openhab audio sink #10423

Merged
merged 5 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion 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,6 +35,12 @@ 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.
This requires the module **module-simple-protocol-tcp** loaded and accessible by the server which runs your openHAB instance.

## Full Example
### pulseaudio.things
```
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>
dalgwen marked this conversation as resolved.
Show resolved Hide resolved
<scope>compile</scope>
dalgwen marked this conversation as resolved.
Show resolved Hide resolved
</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,188 @@
/**
* 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.net.UnknownHostException;
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.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
*
*/
public class PulseAudioAudioSink implements AudioSink {
dalgwen marked this conversation as resolved.
Show resolved Hide resolved

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 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 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.error("Cannot convert this mp3 stream to pcm stream", e);
dalgwen marked this conversation as resolved.
Show resolved Hide resolved
}
return null;
}

/**
* Connect to pulseaudio with the simple protocol
*
* @throws UnknownHostException
* @throws IOException
*/
private void connectIfNeeded() throws IOException {
if (clientSocket == null || !clientSocket.isConnected() || clientSocket.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());
}

try {
connectIfNeeded();
// send raw audio to the socket and to pulse audio
audioInputStream.transferTo(clientSocket.getOutputStream());
} catch (IOException e) {
logger.error("Error while trying to send audio to pulseaudio audio sink. Cannot connect to {} : {}",
pulseaudioHandler.getHost(), pulseaudioHandler.getSimpleTcpPort());
}
} finally {
try {
if (audioInputStream != null) {
audioInputStream.close();
}
;
dalgwen marked this conversation as resolved.
Show resolved Hide resolved
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,8 @@ 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 Map<String, Boolean> TYPE_FILTERS = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,22 @@ protected void removeHandler(ThingHandler thingHandler) {
discoveryServiceReg.get(thingHandler).unregister();
discoveryServiceReg.remove(thingHandler);
}

super.removeHandler(thingHandler);
}

@Override
protected ThingHandler createHandler(Thing thing) {

ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(handler);
return handler;
} else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new PulseaudioHandler(thing);
PulseaudioHandler pulseaudioHandler = new PulseaudioHandler(thing, bundleContext);
return pulseaudioHandler;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ public void initialize() {

@Override
public void dispose() {
pollingJob.cancel(true);
if (pollingJob != null) {
pollingJob.cancel(true);
}
client.disconnect();
super.dispose();
}
Expand Down
Loading