Last active
March 11, 2025 19:10
-
-
Save iuridiniz/dbbbbab2f181a872e07ec5ecfafae114 to your computer and use it in GitHub Desktop.
The LameMP3Streamer class provides methods to encode raw audio data to MP3 * format using the LAME library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.sun.jna.Library; | |
import com.sun.jna.Native; | |
import com.sun.jna.Pointer; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.sql.Time; | |
/** | |
* The LameMP3Streamer class provides methods to encode raw audio data to MP3 | |
* format using the LAME library. | |
* It includes methods to stream raw audio to MP3 format and to convert raw | |
* audio data to MP3 format. | |
* | |
* <p> | |
* Usage example: | |
* </p> | |
* | |
* <pre> | |
* {@code | |
* try (LameMP3Streamer streamer = new LameMP3Streamer(44100, 16, 2, 128, 2)) { | |
* InputStream rawAudio = new FileInputStream("input.raw"); | |
* OutputStream mp3Stream = new FileOutputStream("output.mp3"); | |
* streamer.streamRawToMP3(rawAudio, mp3Stream); | |
* } | |
* } | |
* </pre> | |
*/ | |
public class LameMP3Streamer implements AutoCloseable { | |
public interface LameLibrary extends Library { | |
LameLibrary INSTANCE = Native.load("mp3lame", LameLibrary.class); | |
Pointer lame_init(); | |
int lame_set_in_samplerate(Pointer gfp, int sampleRate); | |
int lame_set_num_channels(Pointer gfp, int channels); | |
int lame_set_brate(Pointer gfp, int brate); | |
int lame_set_quality(Pointer gfp, int quality); | |
int lame_set_disable_reservoir(Pointer gfp, int disable); | |
int lame_init_params(Pointer gfp); | |
int lame_get_framesize(Pointer gfp); | |
int lame_get_frameNum(Pointer gfp); | |
int lame_get_brate(Pointer gfp); | |
int lame_get_out_samplerate(Pointer gfp); | |
int lame_set_out_samplerate(Pointer gfp, int sampleRate); | |
int lame_encode_buffer(Pointer gfp, short[] pcm_l, short[] pcm_r, int nsamples, byte[] mp3buf, int mp3buf_size); | |
int lame_encode_flush(Pointer gfp, byte[] mp3buf, int size); | |
int lame_encode_flush_nogap(Pointer gfp, byte[] mp3buf, int size); | |
void lame_close(Pointer gfp); | |
} | |
private final Pointer gfp; | |
private final byte[] mp3Buffer; | |
private final short[] leftChannel; | |
private final short[] rightChannel; | |
private final int inputBitDepth; | |
private final int inputChannels; | |
private final int inputSampleRate; | |
/** | |
* Constructs a new LameMP3Streamer with the specified parameters. | |
* | |
* @param inputSampleRate The sample rate of the input audio in Hz. (e.g., | |
* 44100 | |
* for 44.1 kHz audio). | |
* @param inputBitDepth The bit depth of the input audio (e.g., 16 for 16-bit | |
* audio). | |
* @param inputChannels The number of input audio channels (1 for mono, 2 for | |
* stereo). | |
* @param outputSampleRate The desired sample rate for the output MP3 in Hz | |
* (e.g., 44100 for 44.1 kHz audio). | |
* @param outputBitRate The desired bit rate for the output MP3 in kbps | |
* (MPEG-2.5 e.g., 8, 16, 24, 32, 40, 48, 56, 64). | |
* @param outputQuality The quality setting for the output MP3 (0 = best | |
* quality, 9 = worst quality). Internal algorithm | |
* selection. True quality is determined by the bitrate | |
* but this variable will effect quality by selecting | |
* expensive or cheap algorithms. | |
* quality=0..9. 0=best (very slow). 9=worst. | |
*/ | |
public LameMP3Streamer(int inputSampleRate, int inputBitDepth, int inputChannels, int outputSampleRate, | |
int outputBitRate, int outputQuality) { | |
LameLibrary lame = LameLibrary.INSTANCE; | |
this.gfp = lame.lame_init(); | |
lame.lame_set_in_samplerate(gfp, inputSampleRate); | |
lame.lame_set_num_channels(gfp, inputChannels); | |
lame.lame_set_brate(gfp, outputBitRate); | |
lame.lame_set_quality(gfp, outputQuality); | |
lame.lame_set_out_samplerate(gfp, outputSampleRate); | |
lame.lame_set_disable_reservoir(gfp, 1); | |
lame.lame_init_params(gfp); | |
int bufferSize = calculateBufferSize(inputSampleRate, outputBitRate); | |
this.mp3Buffer = new byte[bufferSize]; | |
this.leftChannel = new short[1152]; | |
this.rightChannel = inputChannels == 2 ? new short[1152] : null; | |
this.inputBitDepth = inputBitDepth; | |
this.inputChannels = inputChannels; | |
this.inputSampleRate = inputSampleRate; | |
} | |
public int calculatePCMBytesSize(int ms) { | |
return inputBitDepth / 8 * inputChannels * inputSampleRate * ms / 1000; | |
} | |
private byte[] createZeroedPCM(int ms) { | |
byte[] zeroedPCM = new byte[calculatePCMBytesSize(ms)]; | |
return zeroedPCM; | |
} | |
public byte[] createSilentMP3Frame(int ms) throws IOException { | |
return convertRawToMP3(createZeroedPCM(ms)); | |
} | |
public byte[] createConstantToneMP3Frame(int freq, int ms) throws IOException { | |
byte[] constantTonePCM = createZeroedPCM(ms); | |
for (int i = 0; i < constantTonePCM.length; i += 2) { | |
short sample = (short) (Short.MAX_VALUE * Math.sin(2 * Math.PI * freq * i / (inputSampleRate * 1000))); | |
constantTonePCM[i] = (byte) (sample & 0xFF); | |
constantTonePCM[i + 1] = (byte) ((sample >> 8) & 0xFF); | |
} | |
return convertRawToMP3(constantTonePCM); | |
} | |
/** | |
* Generates a white noise MP3 frame of the specified duration. | |
* | |
* @param ms the duration of the white noise in milliseconds | |
* @return a byte array containing the MP3 encoded white noise frame | |
* @throws IOException if an I/O error occurs during MP3 conversion | |
*/ | |
public byte[] createWhiteNoiseMP3Frame(int ms) throws IOException { | |
// double amplitude = 0.001; // Reduce amplitude to make it inaudible | |
double amplitude = 0.1; // Reduce amplitude to make it inaudible | |
byte[] whiteNoisePCM = new byte[calculatePCMBytesSize(ms)]; | |
for (int i = 0; i < whiteNoisePCM.length; i += 2) { | |
short sample = (short) (Short.MAX_VALUE * amplitude * (Math.random() * 2 - 1)); // Reduce amplitude to make | |
// it | |
// inaudible | |
whiteNoisePCM[i] = (byte) (sample & 0xFF); | |
whiteNoisePCM[i + 1] = (byte) ((sample >> 8) & 0xFF); | |
} | |
return convertRawToMP3(whiteNoisePCM); | |
} | |
public static int calculateBufferSize(int inputSampleRate, int outputBitRate) { | |
// | |
// According to lame.h: | |
// * The required mp3buf_size can be computed from num_samples, | |
// samplerate and encoding rate, but here is a worst case estimate: | |
// | |
// mp3buf_size in bytes = 1.25*num_samples + 7200 | |
// | |
// I think a tighter bound could be: (mt, March 2000) | |
// MPEG1: | |
// num_samples*(bitrate/8)/samplerate + 4*1152*(bitrate/8)/samplerate + 512 | |
// MPEG2: | |
// num_samples*(bitrate/8)/samplerate + 4*576*(bitrate/8)/samplerate + 256 | |
int size = Math.max(256 + (inputSampleRate * 4 * 576 * outputBitRate / 8) / inputSampleRate, 32768); | |
return size; | |
} | |
public int getBufferSize() { | |
return mp3Buffer.length; | |
} | |
@Override | |
public void close() { | |
LameLibrary.INSTANCE.lame_close(gfp); | |
} | |
private void encodeToMP3(InputStream rawAudio, OutputStream mp3Stream) throws IOException { | |
byte[] rawBuffer = new byte[1152 * 2 * inputChannels]; | |
int bytesRead; | |
while ((bytesRead = rawAudio.read(rawBuffer)) > 0) { | |
int samplesRead = bytesRead / (inputBitDepth / 8 * inputChannels); | |
for (int i = 0; i < samplesRead; i++) { | |
leftChannel[i] = (short) ((rawBuffer[i * 2 * inputChannels + 1] << 8) | |
| (rawBuffer[i * 2 * inputChannels] & 0xFF)); | |
if (inputChannels == 2) { | |
rightChannel[i] = (short) ((rawBuffer[i * 2 * inputChannels + 3] << 8) | |
| (rawBuffer[i * 2 * inputChannels + 2] & 0xFF)); | |
} | |
} | |
int mp3Bytes = LameLibrary.INSTANCE.lame_encode_buffer(gfp, leftChannel, rightChannel, samplesRead, | |
mp3Buffer, | |
mp3Buffer.length); | |
if (mp3Bytes > 0) { | |
mp3Stream.write(mp3Buffer, 0, mp3Bytes); | |
} | |
} | |
} | |
public void flush(OutputStream mp3Stream) throws IOException { | |
int mp3Bytes = LameLibrary.INSTANCE.lame_encode_flush(gfp, mp3Buffer, mp3Buffer.length); | |
if (mp3Bytes > 0) { | |
mp3Stream.write(mp3Buffer, 0, mp3Bytes); | |
} | |
} | |
public byte[] flush() throws IOException { | |
ByteArrayOutputStream mp3Stream = new ByteArrayOutputStream(); | |
flush(mp3Stream); | |
return mp3Stream.toByteArray(); | |
} | |
public void streamRawToMP3(InputStream rawAudio, OutputStream mp3Stream) throws IOException { | |
encodeToMP3(rawAudio, mp3Stream); | |
} | |
public byte[] convertRawToMP3(byte[] rawBytes, int offset, int length) throws IOException { | |
ByteArrayInputStream rawAudio = new ByteArrayInputStream(rawBytes, offset, length); | |
ByteArrayOutputStream mp3Stream = new ByteArrayOutputStream(); | |
encodeToMP3(rawAudio, mp3Stream); | |
return mp3Stream.toByteArray(); | |
} | |
public byte[] convertRawToMP3(byte[] rawBytes) throws IOException { | |
return convertRawToMP3(rawBytes, 0, rawBytes.length); | |
} | |
public int getFrameRate() { | |
return getOutputSampleRate() / getFrameSizeInSamples(); | |
} | |
public int getTimePerFrame() { | |
return getFrameSizeInSamples() * 1000 / getOutputSampleRate(); | |
} | |
/** | |
* Retrieves the size of the MP3 frame. | |
* | |
* @return the size of the MP3 frame in samples. | |
*/ | |
public int getFrameSizeInSamples() { | |
return LameLibrary.INSTANCE.lame_get_framesize(gfp); | |
} | |
public int getFrameSizeInBits() { | |
return getOutputBitRate() * getTimePerFrame(); | |
} | |
public int getFrameSizeInBytes() { | |
return getFrameSizeInBits() / 8; | |
} | |
public int getOutputBitRate() { | |
return LameLibrary.INSTANCE.lame_get_brate(gfp); | |
} | |
/** | |
* Retrieves the number of frames that have been encoded by the LAME encoder. | |
* | |
* @return the number of frames encoded. | |
*/ | |
public int getNumberOfFrameEncoded() { | |
return LameLibrary.INSTANCE.lame_get_frameNum(gfp); | |
} | |
public int getTotalSamples() { | |
return getNumberOfFrameEncoded() * getFrameSizeInSamples(); | |
} | |
public Time getTotalDuration() { | |
long totalSamples = getTotalSamples(); | |
int sampleRate = getOutputSampleRate(); | |
long durationInMillis = (totalSamples * 1000L) / sampleRate; | |
return new Time(durationInMillis); | |
} | |
public int getTotalBytes() { | |
return getNumberOfFrameEncoded() * getFrameSizeInBytes(); | |
} | |
public int getOutputSampleRate() { | |
return LameLibrary.INSTANCE.lame_get_out_samplerate(gfp); | |
} | |
public String toString() { | |
return "LameMP3Streamer{" | |
+ "inputSampleRate=" + inputSampleRate + " hz" | |
+ ", inputBitDepth=" + inputBitDepth + " bits" | |
+ ", inputChannels=" + inputChannels + " channels" | |
+ ", outputBitRate=" + getOutputBitRate() + " kbps" | |
+ ", outputSampleRate=" + getOutputSampleRate() + " hz" | |
+ ", FrameSize=" + getFrameSizeInSamples() + " samples" | |
+ ", FrameSize=" + getFrameSizeInBits() + " bits" | |
+ ", FrameSize=" + getFrameSizeInBytes() + " bytes" | |
+ ", NumberOfFrameEncoded=" + getNumberOfFrameEncoded() + " frames" | |
+ ", FrameRate=" + getFrameRate() + " frames per second" | |
+ ", TimePerFrame=" + getTimePerFrame() + " ms" | |
+ ", TotalSamples=" + getTotalSamples() + " samples" | |
+ ", TotalBytes=" + getTotalBytes() + " bytes" | |
+ ", TotalDuration=" + getTotalDuration() | |
+ "}"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment