2 * Copyright (c) 2010-2024 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.BufferedInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.util.Optional;
20 import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
21 import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
23 import javax.sound.sampled.AudioInputStream;
24 import javax.sound.sampled.AudioSystem;
25 import javax.sound.sampled.UnsupportedAudioFileException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.core.audio.AudioFormat;
30 import org.openhab.core.audio.AudioStream;
31 import org.openhab.core.audio.UnsupportedAudioFormatException;
32 import org.openhab.core.audio.utils.AudioWaveUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * This class convert a stream to the pcm signed
38 * format supported by the pulseaudio sink
40 * @author Gwendal Roulleau - Initial contribution
41 * @author Miguel Álvarez Díez - Extend from AudioStream
44 public class ConvertedInputStream extends AudioStream {
46 private final Logger logger = LoggerFactory.getLogger(ConvertedInputStream.class);
48 private AudioFormat originalAudioFormat;
49 private final AudioFormat outputAudioFormat;
50 private final InputStream pcmInnerInputStream;
52 private static final Set<String> COMPATIBLE_CODEC = Set.of(AudioFormat.CODEC_PCM_ALAW, AudioFormat.CODEC_PCM_ULAW,
53 AudioFormat.CODEC_PCM_UNSIGNED);
55 public ConvertedInputStream(AudioStream innerInputStream)
56 throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
57 this.originalAudioFormat = innerInputStream.getFormat();
59 String container = originalAudioFormat.getContainer();
60 if (container == null) {
61 throw new UnsupportedAudioFormatException("Unknown format, cannot process", innerInputStream.getFormat());
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);
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);
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());
92 public int read(byte @Nullable [] b) throws IOException {
93 return pcmInnerInputStream.read(b);
97 public int read(byte @Nullable [] b, int off, int len) throws IOException {
98 return pcmInnerInputStream.read(b, off, len);
102 public byte[] readAllBytes() throws IOException {
103 return pcmInnerInputStream.readAllBytes();
107 public byte[] readNBytes(int len) throws IOException {
108 return pcmInnerInputStream.readNBytes(len);
112 public int readNBytes(byte @Nullable [] b, int off, int len) throws IOException {
113 return pcmInnerInputStream.readNBytes(b, off, len);
117 public int read() throws IOException {
118 return pcmInnerInputStream.read();
122 public void close() throws IOException {
123 pcmInnerInputStream.close();
127 * If necessary, this method convert to target PCM
129 * @param resetableInnerInputStream A stream supporting reset operation
130 * (reset is mandatory to parse formation without loosing data)
133 * @throws UnsupportedAudioFileException
134 * @throws UnsupportedAudioFormatException
135 * @throws IOException
137 private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
138 throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException {
139 if (AudioFormat.CODEC_MP3.equals(originalAudioFormat.getCodec())) {
140 logger.debug("Sound is a MP3. Trying to reencode it");
141 // convert MP3 to PCM :
142 AudioInputStream sourceAIS = new MpegAudioFileReader().getAudioInputStream(resetableInnerInputStream);
143 javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
145 MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
146 int bitDepth = sourceFormat.getSampleSizeInBits() != -1 ? sourceFormat.getSampleSizeInBits() : 16;
147 javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
148 javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), bitDepth,
149 sourceFormat.getChannels(), 2 * sourceFormat.getChannels(), sourceFormat.getSampleRate(), false);
151 return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
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,
157 AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(targetFormat,
158 AudioSystem.getAudioInputStream(resetableInnerInputStream));
159 return audioInputStream;
161 throw new UnsupportedAudioFormatException("Pulseaudio audio sink can only play pcm or mp3 stream",
162 originalAudioFormat);
167 public AudioFormat getFormat() {
168 return outputAudioFormat;