Skip to content

Commit e2da7a6

Browse files
committed
Moved JavaMP3 inside project (also updated license)
1 parent 0c401cf commit e2da7a6

8 files changed

Lines changed: 2145 additions & 13 deletions

File tree

.gitmodules

Lines changed: 0 additions & 3 deletions
This file was deleted.

JavaMP3

Lines changed: 0 additions & 1 deletion
This file was deleted.

LICENSE

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,31 @@
199199
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200200
See the License for the specific language governing permissions and
201201
limitations under the License.
202+
203+
204+
205+
===============
206+
MP3 decoder
207+
===============
208+
209+
MIT License
210+
211+
Copyright (c) 2017 delthas
212+
213+
Permission is hereby granted, free of charge, to any person obtaining a copy
214+
of this software and associated documentation files (the "Software"), to deal
215+
in the Software without restriction, including without limitation the rights
216+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
217+
copies of the Software, and to permit persons to whom the Software is
218+
furnished to do so, subject to the following conditions:
219+
220+
The above copyright notice and this permission notice shall be included in all
221+
copies or substantial portions of the Software.
222+
223+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
224+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
225+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
226+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
227+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
228+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
229+
SOFTWARE.

core/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@
7272
<artifactId>jorbis</artifactId>
7373
<version>0.0.17</version>
7474
</dependency>
75-
<dependency>
76-
<groupId>fr.delthas</groupId>
77-
<artifactId>javamp3</artifactId>
78-
<version>1.0.1</version>
79-
</dependency>
8075

8176
<!-- mDNS -->
8277
<dependency>

core/src/main/java/xyz/gianlu/librespot/player/codecs/Mp3Codec.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package xyz.gianlu.librespot.player.codecs;
22

3-
import fr.delthas.javamp3.Sound;
43
import org.jetbrains.annotations.NotNull;
54
import org.jetbrains.annotations.Nullable;
65
import xyz.gianlu.librespot.player.*;
6+
import xyz.gianlu.librespot.player.codecs.mp3.Mp3Sound;
77

88
import javax.sound.sampled.LineUnavailableException;
99
import javax.sound.sampled.SourceDataLine;
@@ -16,14 +16,14 @@
1616
public class Mp3Codec extends Codec {
1717
private final LinesHolder.LineWrapper outputLine;
1818
private final byte[] buffer = new byte[BUFFER_SIZE];
19-
private final Sound sound;
19+
private final Mp3Sound sound;
2020

2121
public Mp3Codec(@NotNull GeneralAudioStream audioFile, @Nullable NormalizationData normalizationData, Player.@NotNull Configuration conf,
2222
PlayerRunner.@NotNull Listener listener, @NotNull LinesHolder lines, int duration) throws CodecException, IOException, LinesHolder.MixerException {
2323
super(audioFile, normalizationData, conf, listener, lines, duration);
2424

2525
skipMp3Tags(audioIn);
26-
sound = new Sound(audioIn);
26+
sound = new Mp3Sound(audioIn);
2727

2828
try {
2929
outputLine = lines.getLineFor(conf, sound.getAudioFormat());

core/src/main/java/xyz/gianlu/librespot/player/codecs/mp3/Decoder.java

Lines changed: 1842 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package xyz.gianlu.librespot.player.codecs.mp3;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import javax.sound.sampled.AudioFormat;
6+
import java.io.FilterInputStream;
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.io.OutputStream;
10+
import java.util.Objects;
11+
12+
/**
13+
* A sound object, that represents an input stream of uncompressed PCM sound data samples, decoded from encoded MPEG data.
14+
* <p>
15+
* To create a sound object from encoded MPEG data (MP1/MP2/MP3), simply use {@link #Mp3Sound(InputStream)}. The decoding process will be done as data is read from this stream. You may also, as a convenience, write all the (remaining) decoded data into an {@link OutputStream} using {@link #decodeFullyInto(OutputStream)}.
16+
* <p>
17+
* You may use the several metadata functions such as {@link #getSamplingFrequency()} to get data about the sound. You may use {@link #getAudioFormat()} to get the sound audio format, to be used with the {@link javax.sound.sampled} API.
18+
*
19+
* @author delthas
20+
* @author devgianlu
21+
*
22+
* @see Mp3Sound#Mp3Sound(InputStream)
23+
* @see Mp3Sound#decodeFullyInto(OutputStream)
24+
*/
25+
public final class Mp3Sound extends FilterInputStream {
26+
private Decoder.SoundData soundData;
27+
private int index;
28+
private AudioFormat audioFormat;
29+
30+
/**
31+
* Creates a new Sound, that will read from the specified encoded MPEG data stream.
32+
* <p>
33+
* This method will try to read the very beginning of the MPEG stream (i.e. 1 MPEG frame) to get its sampling frequency and various other metadata. <b>A stream containing no MPEG data frames/a zero duration MPEG data source will be considered as invalid and will throw {@link IOException}.</b>
34+
* <p>
35+
* This method will not read or decode the file fully, which means it doesn't block and is very fast (as opposed to {@link #decodeFullyInto(OutputStream)}; you probably don't need to execute this method in a specific background thread.
36+
* <p>It is only when reading from this stream that the decoding process will take place (as you read from the stream). <b>The decoding process is quite CPU-intensive, though, so you are encouraged to use a background thread/other multithreading techniques to read from the stream without blocking the whole application.</b>
37+
* <p>
38+
* The various metadata methods such as {@link #getSamplingFrequency()} and {@link #isStereo()} may be called as soon as this object is instantiated (i.e. may be called at any time during the object lifetime).
39+
*
40+
* <b>The data layout is as follows (this is a contract that won't change):</b>
41+
* <br>The decoded PCM sound data is stored as a contiguous stream of 16-bit little-endian signed samples (2 bytes per sample).
42+
* <ul>
43+
* <li>If the sound is in stereo mode, then the samples will be interleaved, e.g. {@code left_sample_0 (2 bytes), right_sample_0 (2 bytes), left_sample_1 (2 bytes), right_sample_1 (2 bytes), ...}
44+
* <li>If the sound is in mono mode, then the samples will be contiguous, e.g. {@code sample_0 (2 bytes), sample_1 (2 bytes), ...}
45+
* </ul>
46+
*
47+
* @param in The input stream from which to read the encoded MPEG data, must be non-null.
48+
* @throws IOException If an {@link IOException} is thrown when reading the underlying stream, or if there's an unexpected EOF during an MPEG frame, or if there's an error while decoding the MPEG data, e.g. if there's no MPEG data in the specified stream.
49+
*/
50+
public Mp3Sound(InputStream in) throws IOException {
51+
super(Objects.requireNonNull(in, "The specified InputStream must be non-null!"));
52+
soundData = Decoder.init(in);
53+
if (soundData == null) {
54+
throw new IOException("No MPEG data in the specified input stream!");
55+
}
56+
}
57+
58+
/**
59+
* {@inheritDoc}
60+
* <p>
61+
* Refer to this class documentation for the decoded data layout.
62+
*/
63+
@Override
64+
public int read() throws IOException {
65+
if (index == -1)
66+
return -1;
67+
if (index == soundData.samplesBuffer.length) {
68+
if (!Decoder.decodeFrame(soundData)) {
69+
index = -1;
70+
soundData.samplesBuffer = null;
71+
return -1;
72+
}
73+
index = 1;
74+
return soundData.samplesBuffer[0] & 0xFF;
75+
}
76+
return soundData.samplesBuffer[index++] & 0xFF;
77+
}
78+
79+
80+
/**
81+
* {@inheritDoc}
82+
* <p>
83+
* Refer to this class documentation for the decoded data layout.
84+
*/
85+
@Override
86+
public int read(@NotNull byte[] b) throws IOException {
87+
return read(b, 0, b.length);
88+
}
89+
90+
/**
91+
* {@inheritDoc}
92+
* <p>
93+
* Refer to this class documentation for the decoded data layout.
94+
*/
95+
@Override
96+
public int read(@NotNull byte[] b, int off, int len) throws IOException {
97+
if (off < 0 || len < 0 || len > b.length - off) {
98+
throw new IndexOutOfBoundsException();
99+
} else if (len == 0) {
100+
return 0;
101+
}
102+
if (index == -1)
103+
return -1;
104+
int len_ = len;
105+
while (len > 0) {
106+
if (index == soundData.samplesBuffer.length) {
107+
if (!Decoder.decodeFrame(soundData)) {
108+
index = -1;
109+
soundData.samplesBuffer = null;
110+
return len_ == len ? -1 : len_ - len;
111+
}
112+
index = 0;
113+
}
114+
int remaining = soundData.samplesBuffer.length - index;
115+
if (remaining > 0) {
116+
if (remaining >= len) {
117+
System.arraycopy(soundData.samplesBuffer, index, b, off, len);
118+
index += len;
119+
return len_;
120+
}
121+
System.arraycopy(soundData.samplesBuffer, index, b, off, remaining);
122+
off += remaining;
123+
len -= remaining;
124+
index = soundData.samplesBuffer.length;
125+
}
126+
}
127+
throw new IllegalStateException("Shouldn't happen (internal error)");
128+
}
129+
130+
/**
131+
* {@inheritDoc}
132+
*
133+
* <b>Fast MPEG seeking isn't implemented yet, so calling this method will still compute all frames to be skipped.</b>
134+
*/
135+
@Override
136+
public long skip(long n) throws IOException {
137+
// TODO add MPEG seeking
138+
return super.skip(n);
139+
}
140+
141+
/**
142+
* {@inheritDoc}
143+
*
144+
* <b>This method returns the number of bytes that can be read until a new MPEG frame has to be read and decoded/processed.</b>
145+
*/
146+
@Override
147+
public int available() throws IOException {
148+
if (soundData.samplesBuffer == null)
149+
return 0;
150+
return soundData.samplesBuffer.length - index;
151+
}
152+
153+
/**
154+
* Closes the underlying input stream and frees up allocated memory.
155+
* <p>
156+
* You may still call metadata-related methods (e.g. {@link #isStereo()}) after calling this method.
157+
*
158+
* @throws IOException If an {@link IOException} is thrown when closing the underlying stream.
159+
*/
160+
@Override
161+
public void close() throws IOException {
162+
if (in != null) {
163+
in.close();
164+
in = null;
165+
soundData.samplesBuffer = null;
166+
}
167+
index = -1;
168+
}
169+
170+
/**
171+
* Does nothing.
172+
* <p>
173+
* Setting a mark and resetting to the mark isn't supported.
174+
*
175+
* @param readlimit Ignored.
176+
*/
177+
@Override
178+
public synchronized void mark(int readlimit) {
179+
super.mark(readlimit);
180+
}
181+
182+
/**
183+
* Throws an IOException.
184+
* <p>
185+
* Setting a mark and resetting to the mark isn't supported.
186+
*
187+
* @throws IOException Always, because setting a mark and resetting to it isn't supported.
188+
*/
189+
@Override
190+
public synchronized void reset() throws IOException {
191+
throw new IOException("mark/reset not supported");
192+
}
193+
194+
/**
195+
* Returns false.
196+
* <p>
197+
* Setting a mark and resetting to the mark isn't supported.
198+
*
199+
* @return false, always.
200+
*/
201+
@Override
202+
public boolean markSupported() {
203+
return false;
204+
}
205+
206+
207+
/**
208+
* Fully copy the remaining bytes of this (decoded PCM sound data samples) stream into the specified {@link OutputStream}, that is, fully decodes the rest of the sound and copies the decoded data into the {@link OutputStream}.
209+
* <p>
210+
* This method is simply a convenience wrapper for the following code: {@code copy(this, os)}, where {@code copy} is a method that would fully copy a stream into another.
211+
* <p>
212+
* This method <b>is blocking</b> and the MPEG decoding process <b>might take a long time, e.g. a few seconds for a sample music track</b>. You are encouraged to call this method e.g. from a background thread.
213+
* <p>
214+
* The exact layout of the PCM data produced by this stream is described in this class documentation.
215+
*
216+
* @param os The output stream in which to put the decoded raw PCM sound samples, must be non-null.
217+
* @return The number of <b>BYTES</b> that were written into the output steam. <b>This is different from the number of samples that were written.</b>
218+
* @throws IOException If an {@link IOException} is thrown when reading the underlying stream, or if there's an unexpected EOF during an MPEG frame, or if there's an error while decoding the MPEG data.
219+
*/
220+
public int decodeFullyInto(OutputStream os) throws IOException {
221+
Objects.requireNonNull(os);
222+
if (index == -1)
223+
return 0;
224+
int remaining = soundData.samplesBuffer.length - index;
225+
if (remaining > 0) {
226+
os.write(soundData.samplesBuffer, index, remaining);
227+
}
228+
int read = remaining;
229+
while (!Decoder.decodeFrame(soundData)) {
230+
os.write(soundData.samplesBuffer);
231+
read += soundData.samplesBuffer.length;
232+
}
233+
soundData.samplesBuffer = null;
234+
index = -1;
235+
return read;
236+
}
237+
238+
/**
239+
* Returns the sampling frequency of this sound, that is its of samples per second, in Hertz (Hz).
240+
* <p>
241+
* For example for a 48kHz sound this would return {@code 48000}.
242+
*
243+
* @return The sampling frequency of the sound in Hertz.
244+
*/
245+
public int getSamplingFrequency() {
246+
return soundData.frequency;
247+
}
248+
249+
/**
250+
* Returns {@code true} if the sound is in stereo mode, that is if it has exactly two channels, and returns false otherwise, that is if it has exactly one channel.
251+
*
252+
* @return {@code true} if the sound is in stereo mode.
253+
*/
254+
public boolean isStereo() {
255+
return soundData.stereo == 1;
256+
}
257+
258+
/**
259+
* Returns the {@link AudioFormat} of this sound, to be used with the {@link javax.sound.sampled} API.
260+
* <p>
261+
* You may refer to the project README on Github for example uses of this method.
262+
*
263+
* @return The {@link AudioFormat} of this sound.
264+
*/
265+
public AudioFormat getAudioFormat() {
266+
if (audioFormat == null) {
267+
audioFormat = new AudioFormat(getSamplingFrequency(), 16, isStereo() ? 2 : 1, true, false);
268+
}
269+
return audioFormat;
270+
}
271+
272+
}

pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
<module>core</module>
4747
<module>api</module>
4848
<module>api-client</module>
49-
<module>JavaMP3</module>
5049
</modules>
5150

5251
<dependencies>

0 commit comments

Comments
 (0)