Skip to content

Instantly share code, notes, and snippets.

@iuridiniz
Last active March 11, 2025 19:10
Show Gist options
  • Save iuridiniz/dbbbbab2f181a872e07ec5ecfafae114 to your computer and use it in GitHub Desktop.
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
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