]> git.basschouten.com Git - openhab-addons.git/blob
4a438fa6dcf677e629c9e0c242f6b863263cba48
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pulseaudio.internal;
14
15 import java.io.BufferedInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.util.Optional;
19 import java.util.Set;
20 import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
21 import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
22
23 import javax.sound.sampled.AudioInputStream;
24 import javax.sound.sampled.AudioSystem;
25 import javax.sound.sampled.UnsupportedAudioFileException;
26
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;
35
36 /**
37  * This class convert a stream to the pcm signed
38  * format supported by the pulseaudio sink
39  *
40  * @author Gwendal Roulleau - Initial contribution
41  * @author Miguel Álvarez Díez - Extend from AudioStream
42  */
43 @NonNullByDefault
44 public class ConvertedInputStream extends AudioStream {
45
46     private final Logger logger = LoggerFactory.getLogger(ConvertedInputStream.class);
47
48     private AudioFormat originalAudioFormat;
49     private final AudioFormat outputAudioFormat;
50     private final InputStream pcmInnerInputStream;
51
52     private static final Set<String> COMPATIBLE_CODEC = Set.of(AudioFormat.CODEC_PCM_ALAW, AudioFormat.CODEC_PCM_ULAW,
53             AudioFormat.CODEC_PCM_UNSIGNED);
54
55     public ConvertedInputStream(AudioStream innerInputStream)
56             throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
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         }
63
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             }
71         }
72
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         }
89     }
90
91     @Override
92     public int read(byte @Nullable [] b) throws IOException {
93         return pcmInnerInputStream.read(b);
94     }
95
96     @Override
97     public int read(byte @Nullable [] b, int off, int len) throws IOException {
98         return pcmInnerInputStream.read(b, off, len);
99     }
100
101     @Override
102     public byte[] readAllBytes() throws IOException {
103         return pcmInnerInputStream.readAllBytes();
104     }
105
106     @Override
107     public byte[] readNBytes(int len) throws IOException {
108         return pcmInnerInputStream.readNBytes(len);
109     }
110
111     @Override
112     public int readNBytes(byte @Nullable [] b, int off, int len) throws IOException {
113         return pcmInnerInputStream.readNBytes(b, off, len);
114     }
115
116     @Override
117     public int read() throws IOException {
118         return pcmInnerInputStream.read();
119     }
120
121     @Override
122     public void close() throws IOException {
123         pcmInnerInputStream.close();
124     }
125
126     /**
127      * If necessary, this method convert to target PCM
128      *
129      * @param resetableInnerInputStream A stream supporting reset operation
130      *            (reset is mandatory to parse formation without loosing data)
131      *
132      * @return PCM stream
133      * @throws UnsupportedAudioFileException
134      * @throws UnsupportedAudioFormatException
135      * @throws IOException
136      */
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();
144
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);
150
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,
156                     true, false);
157             AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(targetFormat,
158                     AudioSystem.getAudioInputStream(resetableInnerInputStream));
159             return audioInputStream;
160         } else {
161             throw new UnsupportedAudioFormatException("Pulseaudio audio sink can only play pcm or mp3 stream",
162                     originalAudioFormat);
163         }
164     }
165
166     @Override
167     public AudioFormat getFormat() {
168         return outputAudioFormat;
169     }
170 }