]> git.basschouten.com Git - openhab-addons.git/blob
be668fac5e2ef0a340501505f827ac5513b19a43
[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.Map;
19 import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
20 import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
21
22 import javax.sound.sampled.AudioFileFormat;
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.SizeableAudioStream;
32 import org.openhab.core.audio.UnsupportedAudioFormatException;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.tritonus.share.sampled.file.TAudioFileFormat;
36
37 /**
38  * This class convert a stream to the normalized pcm
39  * format wanted by the pulseaudio sink
40  *
41  * @author Gwendal Roulleau - Initial contribution
42  */
43 @NonNullByDefault
44 public class ConvertedInputStream extends InputStream {
45
46     private final Logger logger = LoggerFactory.getLogger(ConvertedInputStream.class);
47
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);
50
51     private final AudioFormat audioFormat;
52     private AudioInputStream pcmNormalizedInputStream;
53
54     private long duration = -1;
55     private long length = -1;
56
57     public ConvertedInputStream(AudioStream innerInputStream)
58             throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
59         this.audioFormat = innerInputStream.getFormat();
60
61         if (innerInputStream instanceof SizeableAudioStream sizeableAudioStream) {
62             length = sizeableAudioStream.length();
63         }
64
65         pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new BufferedInputStream(innerInputStream)));
66     }
67
68     @Override
69     public int read(byte @Nullable [] b) throws IOException {
70         return pcmNormalizedInputStream.read(b);
71     }
72
73     @Override
74     public int read(byte @Nullable [] b, int off, int len) throws IOException {
75         return pcmNormalizedInputStream.read(b, off, len);
76     }
77
78     @Override
79     public byte[] readAllBytes() throws IOException {
80         return pcmNormalizedInputStream.readAllBytes();
81     }
82
83     @Override
84     public byte[] readNBytes(int len) throws IOException {
85         return pcmNormalizedInputStream.readNBytes(len);
86     }
87
88     @Override
89     public int readNBytes(byte @Nullable [] b, int off, int len) throws IOException {
90         return pcmNormalizedInputStream.readNBytes(b, off, len);
91     }
92
93     @Override
94     public int read() throws IOException {
95         return pcmNormalizedInputStream.read();
96     }
97
98     @Override
99     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;
124     }
125
126     /**
127      * If necessary, this method convert MP3 to PCM, and try to
128      * extract duration information.
129      *
130      * @param resetableInnerInputStream A stream supporting reset operation
131      *            (reset is mandatory to parse formation without loosing data)
132      *
133      * @return PCM stream
134      * @throws UnsupportedAudioFileException
135      * @throws IOException
136      * @throws UnsupportedAudioFormatException
137      */
138     private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
139             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
160             logger.debug("Sound is a MP3. Trying to reencode it");
161             // convert MP3 to PCM :
162             AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(resetableInnerInputStream);
163             javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
164
165             MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
166             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);
169
170             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             }
181             return audioInputStream;
182         } else {
183             throw new UnsupportedAudioFormatException("Pulseaudio audio sink can only play pcm or mp3 stream",
184                     audioFormat);
185         }
186     }
187 }