Skip to content

Commit 94b4639

Browse files
authored
[pulseaudio] Declare pulseaudio module per audio stream (openhab#16254)
* [pulseaudio] Declare pulseaudio module per audio stream Signed-off-by: Miguel Álvarez <[email protected]>
1 parent 9bcb338 commit 94b4639

14 files changed

+707
-505
lines changed

bundles/org.openhab.binding.pulseaudio/README.md

+29-2
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,49 @@ Sink things can register themselves as audio sink in openHAB. MP3 and WAV files
6262
Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink).
6363
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.
6464

65+
### Thing Configuration
66+
67+
| Config Name | Item Type | Description |
68+
|-----------------------------|-----------------------------------------------------------------------------------------------------------------|
69+
| name | text | The name of one specific device. You can also use the description |
70+
| activateSimpleProtocolSink | boolean | Activation of a corresponding sink in OpenHAB |
71+
| additionalFilters | text | Additional filters to select the proper device on the pulseaudio server, in case of ambiguity |
72+
| simpleProtocolIdleModules | integer | Number of Simple Protocol TCP Socket modules to keep loaded in the server |
73+
| simpleProtocolMinPort | integer | Min port used by simple protocol module instances created by the binding on the pulseaudio host |
74+
| simpleProtocolMaxPort | integer | Max port used by simple protocol module instances created by the binding on the pulseaudio host |
75+
| simpleProtocolSOTimeout | integer | Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp |
76+
6577
## Audio source
6678

6779
Source things can register themselves as audio source in openHAB.
6880
WAV input format, rate and channels can be configured on the thing configuration. (defaults to pcm_signed,16000,1)
6981
Use the appropriate parameter in the source thing to activate this possibility (activateSimpleProtocolSource).
7082
This requires the module **module-simple-protocol-tcp** to be present on the target pulseaudio server. The binding will load this module on the pulseaudio server.
7183

84+
### Thing Configuration
85+
86+
| Config ID | Item Type | Description |
87+
|------------------------------|-----------------------------------------------------------------------------------------------------------------|
88+
| name | text | The name of one specific device. You can also use the description |
89+
| activateSimpleProtocolSource | boolean | Activation of a corresponding sink in OpenHAB |
90+
| additionalFilters | text | Additional filters to select the proper device on the pulseaudio server, in case of ambiguity |
91+
| simpleProtocolIdleModules | integer | Number of Simple Protocol TCP Socket modules to keep loaded in the server |
92+
| simpleProtocolMinPort | integer | Min port used by simple protocol module instances created by the binding on the pulseaudio host |
93+
| simpleProtocolMaxPort | integer | Max port used by simple protocol module instances created by the binding on the pulseaudio host |
94+
| simpleProtocolSOTimeout | integer | Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp |
95+
| simpleProtocolSourceFormat | text | The audio format to be used by module-simple-protocol-tcp on the pulseaudio server |
96+
| simpleProtocolSourceRate | integer | The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server |
97+
| simpleProtocolSourceChannels | integer | The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server |
98+
7299
## Full Example
73100

74101
### pulseaudio.things
75102

76103
```java
77104
Bridge pulseaudio:bridge:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", port=4712 ] {
78105
Things:
79-
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink=true, simpleProtocolSinkPort=4711, additionalFilters="analog-stereo###internal"]
80-
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
106+
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink=true, additionalFilters="analog-stereo###internal"]
107+
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo", activateSimpleProtocolSource=true]
81108
Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"]
82109
Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
83110
Thing combined-sink hdmiAndAnalog "Zone 1+2" @ "Room" [name="combined"]

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java

+68-85
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
import java.io.BufferedInputStream;
1616
import java.io.IOException;
1717
import java.io.InputStream;
18-
import java.util.Map;
18+
import java.util.Optional;
19+
import java.util.Set;
1920
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
2021
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
2122

22-
import javax.sound.sampled.AudioFileFormat;
2323
import javax.sound.sampled.AudioInputStream;
2424
import javax.sound.sampled.AudioSystem;
2525
import javax.sound.sampled.UnsupportedAudioFileException;
@@ -28,160 +28,143 @@
2828
import org.eclipse.jdt.annotation.Nullable;
2929
import org.openhab.core.audio.AudioFormat;
3030
import org.openhab.core.audio.AudioStream;
31-
import org.openhab.core.audio.SizeableAudioStream;
3231
import org.openhab.core.audio.UnsupportedAudioFormatException;
32+
import org.openhab.core.audio.utils.AudioWaveUtils;
3333
import org.slf4j.Logger;
3434
import org.slf4j.LoggerFactory;
35-
import org.tritonus.share.sampled.file.TAudioFileFormat;
3635

3736
/**
38-
* This class convert a stream to the normalized pcm
39-
* format wanted by the pulseaudio sink
37+
* This class convert a stream to the pcm signed
38+
* format supported by the pulseaudio sink
4039
*
4140
* @author Gwendal Roulleau - Initial contribution
41+
* @author Miguel Álvarez Díez - Extend from AudioStream
4242
*/
4343
@NonNullByDefault
44-
public class ConvertedInputStream extends InputStream {
44+
public class ConvertedInputStream extends AudioStream {
4545

4646
private final Logger logger = LoggerFactory.getLogger(ConvertedInputStream.class);
4747

48-
private static final javax.sound.sampled.AudioFormat TARGET_FORMAT = new javax.sound.sampled.AudioFormat(
49-
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 2, 4, 44100, false);
48+
private AudioFormat originalAudioFormat;
49+
private final AudioFormat outputAudioFormat;
50+
private final InputStream pcmInnerInputStream;
5051

51-
private final AudioFormat audioFormat;
52-
private AudioInputStream pcmNormalizedInputStream;
53-
54-
private long duration = -1;
55-
private long length = -1;
52+
private static final Set<String> COMPATIBLE_CODEC = Set.of(AudioFormat.CODEC_PCM_ALAW, AudioFormat.CODEC_PCM_ULAW,
53+
AudioFormat.CODEC_PCM_UNSIGNED);
5654

5755
public ConvertedInputStream(AudioStream innerInputStream)
5856
throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
59-
this.audioFormat = innerInputStream.getFormat();
57+
this.originalAudioFormat = innerInputStream.getFormat();
58+
59+
String container = originalAudioFormat.getContainer();
60+
if (container == null) {
61+
throw new UnsupportedAudioFormatException("Unknown format, cannot process", innerInputStream.getFormat());
62+
}
6063

61-
if (innerInputStream instanceof SizeableAudioStream sizeableAudioStream) {
62-
length = sizeableAudioStream.length();
64+
if (container.equals(AudioFormat.CONTAINER_WAVE)) {
65+
if (originalAudioFormat.getFrequency() == null || originalAudioFormat.getChannels() == null
66+
|| originalAudioFormat.getBitRate() == null || originalAudioFormat.getCodec() == null
67+
|| originalAudioFormat.getBitDepth() == null || originalAudioFormat.isBigEndian() == null) {
68+
// parse it by ourself to maybe get missing information :
69+
this.originalAudioFormat = AudioWaveUtils.parseWavFormat(innerInputStream);
70+
}
6371
}
6472

65-
pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new BufferedInputStream(innerInputStream)));
73+
if (AudioFormat.CODEC_PCM_SIGNED.equals(originalAudioFormat.getCodec())) {
74+
outputAudioFormat = originalAudioFormat;
75+
pcmInnerInputStream = innerInputStream;
76+
if (container.equals(AudioFormat.CONTAINER_WAVE)) {
77+
AudioWaveUtils.removeFMT(innerInputStream);
78+
}
79+
80+
} else {
81+
pcmInnerInputStream = getPCMStream(new BufferedInputStream(innerInputStream));
82+
var javaAudioFormat = ((AudioInputStream) pcmInnerInputStream).getFormat();
83+
int bitRate = (int) javaAudioFormat.getSampleRate() * javaAudioFormat.getSampleSizeInBits()
84+
* javaAudioFormat.getChannels();
85+
outputAudioFormat = new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_PCM_SIGNED,
86+
javaAudioFormat.isBigEndian(), javaAudioFormat.getSampleSizeInBits(), bitRate,
87+
(long) javaAudioFormat.getSampleRate(), javaAudioFormat.getChannels());
88+
}
6689
}
6790

6891
@Override
6992
public int read(byte @Nullable [] b) throws IOException {
70-
return pcmNormalizedInputStream.read(b);
93+
return pcmInnerInputStream.read(b);
7194
}
7295

7396
@Override
7497
public int read(byte @Nullable [] b, int off, int len) throws IOException {
75-
return pcmNormalizedInputStream.read(b, off, len);
98+
return pcmInnerInputStream.read(b, off, len);
7699
}
77100

78101
@Override
79102
public byte[] readAllBytes() throws IOException {
80-
return pcmNormalizedInputStream.readAllBytes();
103+
return pcmInnerInputStream.readAllBytes();
81104
}
82105

83106
@Override
84107
public byte[] readNBytes(int len) throws IOException {
85-
return pcmNormalizedInputStream.readNBytes(len);
108+
return pcmInnerInputStream.readNBytes(len);
86109
}
87110

88111
@Override
89112
public int readNBytes(byte @Nullable [] b, int off, int len) throws IOException {
90-
return pcmNormalizedInputStream.readNBytes(b, off, len);
113+
return pcmInnerInputStream.readNBytes(b, off, len);
91114
}
92115

93116
@Override
94117
public int read() throws IOException {
95-
return pcmNormalizedInputStream.read();
118+
return pcmInnerInputStream.read();
96119
}
97120

98121
@Override
99122
public void close() throws IOException {
100-
pcmNormalizedInputStream.close();
101-
}
102-
103-
/**
104-
* Ensure right PCM format by converting if needed (sample rate, channel)
105-
*
106-
* @param pcmInputStream
107-
*
108-
* @return A PCM normalized stream (2 channel, 44100hz, 16 bit signed)
109-
*/
110-
private AudioInputStream getPCMStreamNormalized(AudioInputStream pcmInputStream) {
111-
javax.sound.sampled.AudioFormat format = pcmInputStream.getFormat();
112-
if (format.getChannels() != 2
113-
|| !format.getEncoding().equals(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED)
114-
|| Math.abs(format.getFrameRate() - 44100) > 1000) {
115-
logger.debug("Sound is not in the target format. Trying to reencode it");
116-
return AudioSystem.getAudioInputStream(TARGET_FORMAT, pcmInputStream);
117-
} else {
118-
return pcmInputStream;
119-
}
120-
}
121-
122-
public long getDuration() {
123-
return duration;
123+
pcmInnerInputStream.close();
124124
}
125125

126126
/**
127-
* If necessary, this method convert MP3 to PCM, and try to
128-
* extract duration information.
127+
* If necessary, this method convert to target PCM
129128
*
130129
* @param resetableInnerInputStream A stream supporting reset operation
131130
* (reset is mandatory to parse formation without loosing data)
132131
*
133132
* @return PCM stream
134133
* @throws UnsupportedAudioFileException
135-
* @throws IOException
136134
* @throws UnsupportedAudioFormatException
135+
* @throws IOException
137136
*/
138137
private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
139138
throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException {
140-
if (AudioFormat.MP3.isCompatible(audioFormat)) {
141-
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
142-
143-
if (length > 0) { // compute duration if possible
144-
AudioFileFormat audioFileFormat = mpegAudioFileReader.getAudioFileFormat(resetableInnerInputStream);
145-
if (audioFileFormat instanceof TAudioFileFormat) {
146-
Map<String, Object> taudioFileFormatProperties = ((TAudioFileFormat) audioFileFormat).properties();
147-
if (taudioFileFormatProperties.containsKey("mp3.framesize.bytes")
148-
&& taudioFileFormatProperties.containsKey("mp3.framerate.fps")) {
149-
Integer frameSize = (Integer) taudioFileFormatProperties.get("mp3.framesize.bytes");
150-
Float frameRate = (Float) taudioFileFormatProperties.get("mp3.framerate.fps");
151-
if (frameSize != null && frameRate != null) {
152-
duration = Math.round((length / (frameSize * frameRate)) * 1000);
153-
logger.debug("Duration of input stream : {}", duration);
154-
}
155-
}
156-
}
157-
resetableInnerInputStream.reset();
158-
}
159-
139+
if (AudioFormat.CODEC_MP3.equals(originalAudioFormat.getCodec())) {
160140
logger.debug("Sound is a MP3. Trying to reencode it");
161141
// convert MP3 to PCM :
162-
AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(resetableInnerInputStream);
142+
AudioInputStream sourceAIS = new MpegAudioFileReader().getAudioInputStream(resetableInnerInputStream);
163143
javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
164144

165145
MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
146+
int bitDepth = sourceFormat.getSampleSizeInBits() != -1 ? sourceFormat.getSampleSizeInBits() : 16;
166147
javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
167-
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
168-
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
148+
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), bitDepth,
149+
sourceFormat.getChannels(), 2 * sourceFormat.getChannels(), sourceFormat.getSampleRate(), false);
169150

170151
return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
171-
} else if (AudioFormat.WAV.isCompatible(audioFormat)) {
172-
// return the same input stream, but try to compute the duration first
173-
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(resetableInnerInputStream);
174-
if (length > 0) {
175-
int frameSize = audioInputStream.getFormat().getFrameSize();
176-
float frameRate = audioInputStream.getFormat().getFrameRate();
177-
float durationInSeconds = (length / (frameSize * frameRate));
178-
duration = Math.round(durationInSeconds * 1000);
179-
logger.debug("Duration of input stream : {}", duration);
180-
}
152+
} else if (COMPATIBLE_CODEC.contains(originalAudioFormat.getCodec())) {
153+
long frequency = Optional.ofNullable(originalAudioFormat.getFrequency()).orElse(44100L);
154+
int channel = Optional.ofNullable(originalAudioFormat.getChannels()).orElse(1);
155+
javax.sound.sampled.AudioFormat targetFormat = new javax.sound.sampled.AudioFormat(frequency, 16, channel,
156+
true, false);
157+
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(targetFormat,
158+
AudioSystem.getAudioInputStream(resetableInnerInputStream));
181159
return audioInputStream;
182160
} else {
183161
throw new UnsupportedAudioFormatException("Pulseaudio audio sink can only play pcm or mp3 stream",
184-
audioFormat);
162+
originalAudioFormat);
185163
}
186164
}
165+
166+
@Override
167+
public AudioFormat getFormat() {
168+
return outputAudioFormat;
169+
}
187170
}

0 commit comments

Comments
 (0)