All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.delthas.javamp3.Sound Maven / Gradle / Ivy

The newest version!
package fr.delthas.javamp3;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.swing.text.html.HTMLDocument;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

/**
 * A sound object, that represents an input stream of uncompressed PCM sound data samples, decoded from encoded MPEG data.
 * 

* To create a sound object from encoded MPEG data (MP1/MP2/MP3), simply use {@link #Sound(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)}. *

* 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. * * See the project README (on Github) for some context and various examples on how to use the library. * * @see Sound#Sound(InputStream) * @see Sound#decodeFullyInto(OutputStream) */ public final class Sound extends FilterInputStream { private Decoder.SoundData soundData; private int index; private AudioFormat audioFormat; /** * Creates a new Sound, that will read from the specified encoded MPEG data stream. *

* 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. A stream containing no MPEG data frames/a zero duration MPEG data source will be considered as invalid and will throw {@link IOException}. *

* 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. *

It is only when reading from this stream that the decoding process will take place (as you read from the stream). 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. *

* 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). *

* The data layout is as follows (this is a contract that won't change): *
The decoded PCM sound data is stored as a contiguous stream of 16-bit little-endian signed samples (2 bytes per sample). *

    *
  • 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), ...} *
  • If the sound is in mono mode, then the samples will be contiguous, e.g. {@code sample_0 (2 bytes), sample_1 (2 bytes), ...} *
* @param in The input stream from which to read the encoded MPEG data, must be non-null. * @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. */ public Sound(InputStream in) throws IOException { super(Objects.requireNonNull(in, "The specified InputStream must be non-null!")); soundData = Decoder.init(in); if(soundData == null) { throw new IOException("No MPEG data in the specified input stream!"); } } /** * {@inheritDoc} *

* Refer to this class documentation for the decoded data layout. */ @Override public int read() throws IOException { if(index == -1) return -1; if(index == soundData.samplesBuffer.length) { if(!Decoder.decodeFrame(soundData)) { index = -1; soundData.samplesBuffer = null; return -1; } index = 1; return soundData.samplesBuffer[0] & 0xFF; } return soundData.samplesBuffer[index++] & 0xFF; } /** * {@inheritDoc} *

* Refer to this class documentation for the decoded data layout. */ @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } /** * {@inheritDoc} *

* Refer to this class documentation for the decoded data layout. */ @Override public int read(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } if(index == -1) return -1; int len_ = len; while(len > 0) { if(index == soundData.samplesBuffer.length) { if(!Decoder.decodeFrame(soundData)) { index = -1; soundData.samplesBuffer = null; return len_ == len ? -1 : len_ - len; } index = 0; } int remaining = soundData.samplesBuffer.length - index; if(remaining > 0) { if(remaining >= len) { System.arraycopy(soundData.samplesBuffer, index, b, off, len); index += len; return len_; } System.arraycopy(soundData.samplesBuffer, index, b, off, remaining); off += remaining; len -= remaining; index = soundData.samplesBuffer.length; } } throw new IllegalStateException("Shouldn't happen (internal error)"); } /** * {@inheritDoc} *

* Fast MPEG seeking isn't implemented yet, so calling this method will still compute all frames to be skipped. */ @Override public long skip(long n) throws IOException { // TODO add MPEG seeking return super.skip(n); } /** * {@inheritDoc} *

* This method returns the number of bytes that can be read until a new MPEG frame has to be read and decoded/processed. */ @Override public int available() throws IOException { if(soundData.samplesBuffer == null) return 0; return soundData.samplesBuffer.length - index; } /** * Closes the underlying input stream and frees up allocated memory. *

* You may still call metadata-related methods (e.g. {@link #isStereo()}) after calling this method. * * @throws IOException If an {@link IOException} is thrown when closing the underlying stream. */ @Override public void close() throws IOException { if(in != null) { in.close(); in = null; soundData.samplesBuffer = null; } index = -1; } /** * Does nothing. *

* Setting a mark and resetting to the mark isn't supported. * * @param readlimit Ignored. */ @Override public synchronized void mark(int readlimit) { super.mark(readlimit); } /** * Throws an IOException. *

* Setting a mark and resetting to the mark isn't supported. * * @throws IOException Always, because setting a mark and resetting to it isn't supported. */ @Override public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } /** * Returns false. *

* Setting a mark and resetting to the mark isn't supported. * * @return false, always. */ @Override public boolean markSupported() { return false; } /** * 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}. *

* 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. *

* This method is blocking and the MPEG decoding process might take a long time, e.g. a few seconds for a sample music track. You are encouraged to call this method e.g. from a background thread. *

* The exact layout of the PCM data produced by this stream is described in this class documentation. * * @param os The output stream in which to put the decoded raw PCM sound samples, must be non-null. * @return The number of BYTES that were written into the output steam. This is different from the number of samples that were written. * @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. */ public int decodeFullyInto(OutputStream os) throws IOException { Objects.requireNonNull(os); if(index == -1) return 0; int remaining = soundData.samplesBuffer.length - index; if(remaining > 0) { os.write(soundData.samplesBuffer, index, remaining); } int read = remaining; while(!Decoder.decodeFrame(soundData)) { os.write(soundData.samplesBuffer); read += soundData.samplesBuffer.length; } soundData.samplesBuffer = null; index = -1; return read; } /** * Returns the sampling frequency of this sound, that is its of samples per second, in Hertz (Hz). *

* For example for a 48kHz sound this would return {@code 48000}. * * @return The sampling frequency of the sound in Hertz. */ public int getSamplingFrequency() { return soundData.frequency; } /** * 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. * * @return {@code true} if the sound is in stereo mode. */ public boolean isStereo() { return soundData.stereo == 1; } /** * Returns the {@link AudioFormat} of this sound, to be used with the {@link javax.sound.sampled} API. *

* You may refer to the project README on Github for example uses of this method. * * @return The {@link AudioFormat} of this sound. */ public AudioFormat getAudioFormat() { if (audioFormat == null) { audioFormat = new AudioFormat(getSamplingFrequency(), 16, isStereo() ? 2 : 1, true, false); } return audioFormat; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy