|
15 | 15 | import java.io.BufferedInputStream;
|
16 | 16 | import java.io.IOException;
|
17 | 17 | import java.io.InputStream;
|
18 |
| -import java.util.Map; |
| 18 | +import java.util.Optional; |
| 19 | +import java.util.Set; |
19 | 20 | import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
|
20 | 21 | import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
|
21 | 22 |
|
22 |
| -import javax.sound.sampled.AudioFileFormat; |
23 | 23 | import javax.sound.sampled.AudioInputStream;
|
24 | 24 | import javax.sound.sampled.AudioSystem;
|
25 | 25 | import javax.sound.sampled.UnsupportedAudioFileException;
|
|
28 | 28 | import org.eclipse.jdt.annotation.Nullable;
|
29 | 29 | import org.openhab.core.audio.AudioFormat;
|
30 | 30 | import org.openhab.core.audio.AudioStream;
|
31 |
| -import org.openhab.core.audio.SizeableAudioStream; |
32 | 31 | import org.openhab.core.audio.UnsupportedAudioFormatException;
|
| 32 | +import org.openhab.core.audio.utils.AudioWaveUtils; |
33 | 33 | import org.slf4j.Logger;
|
34 | 34 | import org.slf4j.LoggerFactory;
|
35 |
| -import org.tritonus.share.sampled.file.TAudioFileFormat; |
36 | 35 |
|
37 | 36 | /**
|
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 |
40 | 39 | *
|
41 | 40 | * @author Gwendal Roulleau - Initial contribution
|
| 41 | + * @author Miguel Álvarez Díez - Extend from AudioStream |
42 | 42 | */
|
43 | 43 | @NonNullByDefault
|
44 |
| -public class ConvertedInputStream extends InputStream { |
| 44 | +public class ConvertedInputStream extends AudioStream { |
45 | 45 |
|
46 | 46 | private final Logger logger = LoggerFactory.getLogger(ConvertedInputStream.class);
|
47 | 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); |
| 48 | + private AudioFormat originalAudioFormat; |
| 49 | + private final AudioFormat outputAudioFormat; |
| 50 | + private final InputStream pcmInnerInputStream; |
50 | 51 |
|
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); |
56 | 54 |
|
57 | 55 | public ConvertedInputStream(AudioStream innerInputStream)
|
58 | 56 | 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 | + } |
60 | 63 |
|
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 | + } |
63 | 71 | }
|
64 | 72 |
|
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 | + } |
66 | 89 | }
|
67 | 90 |
|
68 | 91 | @Override
|
69 | 92 | public int read(byte @Nullable [] b) throws IOException {
|
70 |
| - return pcmNormalizedInputStream.read(b); |
| 93 | + return pcmInnerInputStream.read(b); |
71 | 94 | }
|
72 | 95 |
|
73 | 96 | @Override
|
74 | 97 | 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); |
76 | 99 | }
|
77 | 100 |
|
78 | 101 | @Override
|
79 | 102 | public byte[] readAllBytes() throws IOException {
|
80 |
| - return pcmNormalizedInputStream.readAllBytes(); |
| 103 | + return pcmInnerInputStream.readAllBytes(); |
81 | 104 | }
|
82 | 105 |
|
83 | 106 | @Override
|
84 | 107 | public byte[] readNBytes(int len) throws IOException {
|
85 |
| - return pcmNormalizedInputStream.readNBytes(len); |
| 108 | + return pcmInnerInputStream.readNBytes(len); |
86 | 109 | }
|
87 | 110 |
|
88 | 111 | @Override
|
89 | 112 | 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); |
91 | 114 | }
|
92 | 115 |
|
93 | 116 | @Override
|
94 | 117 | public int read() throws IOException {
|
95 |
| - return pcmNormalizedInputStream.read(); |
| 118 | + return pcmInnerInputStream.read(); |
96 | 119 | }
|
97 | 120 |
|
98 | 121 | @Override
|
99 | 122 | 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(); |
124 | 124 | }
|
125 | 125 |
|
126 | 126 | /**
|
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 |
129 | 128 | *
|
130 | 129 | * @param resetableInnerInputStream A stream supporting reset operation
|
131 | 130 | * (reset is mandatory to parse formation without loosing data)
|
132 | 131 | *
|
133 | 132 | * @return PCM stream
|
134 | 133 | * @throws UnsupportedAudioFileException
|
135 |
| - * @throws IOException |
136 | 134 | * @throws UnsupportedAudioFormatException
|
| 135 | + * @throws IOException |
137 | 136 | */
|
138 | 137 | private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
|
139 | 138 | 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())) { |
160 | 140 | logger.debug("Sound is a MP3. Trying to reencode it");
|
161 | 141 | // convert MP3 to PCM :
|
162 |
| - AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(resetableInnerInputStream); |
| 142 | + AudioInputStream sourceAIS = new MpegAudioFileReader().getAudioInputStream(resetableInnerInputStream); |
163 | 143 | javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
|
164 | 144 |
|
165 | 145 | MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
|
| 146 | + int bitDepth = sourceFormat.getSampleSizeInBits() != -1 ? sourceFormat.getSampleSizeInBits() : 16; |
166 | 147 | 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); |
169 | 150 |
|
170 | 151 | 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)); |
181 | 159 | return audioInputStream;
|
182 | 160 | } else {
|
183 | 161 | throw new UnsupportedAudioFormatException("Pulseaudio audio sink can only play pcm or mp3 stream",
|
184 |
| - audioFormat); |
| 162 | + originalAudioFormat); |
185 | 163 | }
|
186 | 164 | }
|
| 165 | + |
| 166 | + @Override |
| 167 | + public AudioFormat getFormat() { |
| 168 | + return outputAudioFormat; |
| 169 | + } |
187 | 170 | }
|
0 commit comments