mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#939 Adds MP3 encoder preferences to the User Preferences dialog and provides user control over sample rate and MP3 encoder settings. MP3 preferences are global and affect both recording and streaming. (#1159)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* Enumeration of (input) audio sample rates for mono audio.
|
||||
*/
|
||||
public enum AudioSampleRate
|
||||
{
|
||||
SR_8000(AudioFormats.PCM_SIGNED_8_KHZ_16BITS_MONO, "8000 Hz (no resample / default)"),
|
||||
SR_16000(AudioFormats.PCM_SIGNED_16_KHZ_16BITS_MONO, "16000 Hz"),
|
||||
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16BITS_MONO, "22050 Hz"),
|
||||
SR_44100(AudioFormats.PCM_SIGNED_44100_HZ_16BITS_MONO, "44100 Hz");
|
||||
|
||||
private AudioFormat mAudioFormat;
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param audioFormat for the specified sample rate
|
||||
*/
|
||||
AudioSampleRate(AudioFormat audioFormat, String label)
|
||||
{
|
||||
mAudioFormat = audioFormat;
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default sample rate
|
||||
*/
|
||||
public static AudioSampleRate getDefault()
|
||||
{
|
||||
return SR_8000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio format for the specified sample rate entry
|
||||
*/
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return mAudioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample rate for the format
|
||||
*/
|
||||
public double getSampleRate()
|
||||
{
|
||||
return getAudioFormat().getSampleRate();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
/**
|
||||
* MP3 Channel Mode
|
||||
*
|
||||
* see: http://www.mp3-tech.org/programmer/frame_header.html
|
||||
*/
|
||||
public enum ChannelMode
|
||||
{
|
||||
STEREO("STEREO"),
|
||||
JOINT_STEREO("JOINT STEREO"),
|
||||
DUAL_CHANNEL("DUAL CHANNEL"),
|
||||
MONO("MONO");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
ChannelMode(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public static ChannelMode fromValue(int value)
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 0:
|
||||
return STEREO;
|
||||
case 1:
|
||||
return JOINT_STEREO;
|
||||
case 2:
|
||||
return DUAL_CHANNEL;
|
||||
case 3:
|
||||
default:
|
||||
return MONO;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,11 +25,11 @@ public interface IAudioConverter
|
||||
/**
|
||||
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
|
||||
*/
|
||||
public byte[] convert(List<float[]> audioBuffers);
|
||||
List<byte[]> convert(List<float[]> audioBuffers);
|
||||
|
||||
/**
|
||||
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
|
||||
* remaining bytes to produce a full frame.
|
||||
*/
|
||||
public byte[] flush();
|
||||
List<byte[]> flush();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,11 +14,13 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ISilenceGenerator
|
||||
{
|
||||
byte[] generate(long duration);
|
||||
List<byte[]> generate(long duration);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||
import net.sourceforge.lame.mp3.MPEGMode;
|
||||
|
||||
/**
|
||||
* Factory class to create and configure LAME MP3 encoder
|
||||
*/
|
||||
public class LameFactory
|
||||
{
|
||||
/**
|
||||
* Creates a LAME MP3 encoder using the specified input sample rate and MP3 settings
|
||||
* @param input sample rate
|
||||
* @param setting to apply to the encoder
|
||||
* @return lame encoder
|
||||
*/
|
||||
public static LameEncoder getLameEncoder(AudioSampleRate input, MP3Setting setting)
|
||||
{
|
||||
return switch(setting)
|
||||
{
|
||||
case CBR_16 -> new LameEncoder(input.getAudioFormat(), 16, MPEGMode.MONO, LameEncoder.DEFAULT_QUALITY, false);
|
||||
case CBR_32 -> new LameEncoder(input.getAudioFormat(), 32, MPEGMode.MONO, LameEncoder.DEFAULT_QUALITY, false);
|
||||
case ABR_56 -> new LameEncoder(input.getAudioFormat(), 56, MPEGMode.MONO, LameEncoder.DEFAULT_QUALITY, true);
|
||||
case VBR_5 -> new LameEncoder(input.getAudioFormat(), LameEncoder.DEFAULT_BITRATE, MPEGMode.MONO, 5, true);
|
||||
case VBR_7 -> new LameEncoder(input.getAudioFormat(), LameEncoder.DEFAULT_BITRATE, MPEGMode.MONO, 7, true);
|
||||
default -> throw new IllegalArgumentException("Unrecognized MP3 setting: " + setting);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a resampler to resample from 8 kHz default audio sample rate to the specified sample rate.
|
||||
* @param sampleRate to resample to
|
||||
* @return resampler.
|
||||
*/
|
||||
public static RealResampler getResampler(AudioSampleRate sampleRate)
|
||||
{
|
||||
return switch(sampleRate)
|
||||
{
|
||||
case SR_16000 -> new RealResampler(8000, 16000, 4096, 512);
|
||||
case SR_22050 -> new RealResampler(8000, 22050, 4096, 512);
|
||||
case SR_44100 -> new RealResampler(8000, 44100, 4096, 512);
|
||||
default -> throw new IllegalArgumentException("Unrecognized sample rate for resampling: " + sampleRate);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,21 +14,22 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioUtils;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
import net.sourceforge.lame.mp3.MPEGMode;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MP3AudioConverter implements IAudioConverter
|
||||
@@ -36,91 +37,89 @@ public class MP3AudioConverter implements IAudioConverter
|
||||
private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class );
|
||||
public static final int AUDIO_QUALITY = Lame.QUALITY_LOW;
|
||||
private LameEncoder mEncoder;
|
||||
private ByteArrayOutputStream mMP3Stream = new ByteArrayOutputStream();
|
||||
private byte[] mMP3Buffer;
|
||||
private RealResampler mResampler;
|
||||
private ByteArrayOutputStream mMP3EncodedFramesStream = new ByteArrayOutputStream();
|
||||
private byte[] mOutputFramesBuffer;
|
||||
|
||||
/**
|
||||
* Converts PCM 8kHz 16-bit Little Endian audio packets to Mono, Low Quality MP3 compressed audio.
|
||||
* Converts PCM 16-bit Little Endian audio packets to Mono, MP3 compressed audio.
|
||||
*
|
||||
* @param bitRate for converted MP3 audio
|
||||
* @param variableBitRate (VBR) true or false for constant bit rate (CBR)
|
||||
* @param audioSampleRate for the desired input sample rate (resampled from default 8 kHz as needed)
|
||||
* @param setting to configure the LAME encoder
|
||||
*/
|
||||
public MP3AudioConverter(int bitRate, boolean variableBitRate)
|
||||
public MP3AudioConverter(AudioSampleRate audioSampleRate, MP3Setting setting)
|
||||
{
|
||||
mEncoder = new LameEncoder(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
|
||||
bitRate, MPEGMode.MONO, AUDIO_QUALITY, variableBitRate);
|
||||
mEncoder = LameFactory.getLameEncoder(audioSampleRate, setting);
|
||||
|
||||
mMP3Buffer = new byte[mEncoder.getPCMBufferSize()];
|
||||
if(audioSampleRate != AudioSampleRate.SR_8000)
|
||||
{
|
||||
mResampler = LameFactory.getResampler(audioSampleRate);
|
||||
}
|
||||
|
||||
mOutputFramesBuffer = new byte[mEncoder.getPCMBufferSize()];
|
||||
}
|
||||
|
||||
public List<byte[]> convert(List<float[]> audioPackets)
|
||||
{
|
||||
List<byte[]> converted = new ArrayList<>();
|
||||
|
||||
if(mResampler != null)
|
||||
{
|
||||
audioPackets = mResampler.resample(audioPackets);
|
||||
}
|
||||
|
||||
for(int x = 0; x < audioPackets.size(); x++)
|
||||
{
|
||||
byte[] bytesToEncode = ConversionUtils.convertToSigned16BitSamples(audioPackets.get(x)).array();
|
||||
int bytesToEncodePointer = 0;
|
||||
|
||||
int inputChunkSize = FastMath.min(mOutputFramesBuffer.length, bytesToEncode.length);
|
||||
int outputChunkSize = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while(bytesToEncodePointer < bytesToEncode.length)
|
||||
{
|
||||
outputChunkSize = mEncoder.encodeBuffer(bytesToEncode, bytesToEncodePointer, inputChunkSize, mOutputFramesBuffer);
|
||||
bytesToEncodePointer += inputChunkSize;
|
||||
inputChunkSize = FastMath.min(mOutputFramesBuffer.length, bytesToEncode.length - bytesToEncodePointer);
|
||||
|
||||
if(outputChunkSize > 0)
|
||||
{
|
||||
converted.add(Arrays.copyOf(mOutputFramesBuffer, outputChunkSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
int finalChunkSize = mEncoder.encodeFinish(mOutputFramesBuffer);
|
||||
|
||||
if(finalChunkSize > 0)
|
||||
{
|
||||
converted.add(Arrays.copyOf(mOutputFramesBuffer, finalChunkSize));
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(List<float[]> audioPackets)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
byte[] pcmBytes = AudioUtils.convertTo16BitSamples(audioPackets);
|
||||
|
||||
int pcmBufferSize = FastMath.min(mMP3Buffer.length, pcmBytes.length);
|
||||
|
||||
int mp3BufferSize = 0;
|
||||
|
||||
int pcmBytesPosition = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
|
||||
{
|
||||
pcmBytesPosition += pcmBufferSize;
|
||||
pcmBufferSize = FastMath.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
|
||||
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
|
||||
}
|
||||
|
||||
return mMP3Stream.toByteArray();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] convertAudio(List<float[]> audioBuffers)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
byte[] pcmBytes = AudioUtils.convert(audioBuffers);
|
||||
|
||||
int pcmBufferSize = FastMath.min(mMP3Buffer.length, pcmBytes.length);
|
||||
|
||||
int mp3BufferSize = 0;
|
||||
|
||||
int pcmBytesPosition = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
|
||||
{
|
||||
pcmBytesPosition += pcmBufferSize;
|
||||
pcmBufferSize = FastMath.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
|
||||
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
|
||||
}
|
||||
|
||||
return mMP3Stream.toByteArray();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] flush()
|
||||
public List<byte[]> flush()
|
||||
{
|
||||
byte[] lastPartialFrame = new byte[mEncoder.getMP3BufferSize()];
|
||||
|
||||
int length = mEncoder.encodeFinish(lastPartialFrame);
|
||||
|
||||
return Arrays.copyOf(lastPartialFrame, length);
|
||||
byte[] frame = Arrays.copyOf(lastPartialFrame, length);
|
||||
|
||||
if(frame.length == 0)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.singletonList(frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
public class MP3BitRate
|
||||
{
|
||||
private final static int[] MPEG_1_L3 = new int[]{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
|
||||
private final static int[] MPEG_2_L3 = new int[]{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
|
||||
private final static int[] MPEG_2_5_L3 = new int[]{0, 8, 16, 24, 32, 40, 48, 56, 64};
|
||||
|
||||
public static int getLayer3BitRate(MPEGVersion version, int value)
|
||||
{
|
||||
switch(version)
|
||||
{
|
||||
case V_1:
|
||||
if(0 <= value && value < MPEG_1_L3.length)
|
||||
{
|
||||
return MPEG_1_L3[value];
|
||||
}
|
||||
break;
|
||||
case V_2:
|
||||
if(0 <= value && value < MPEG_2_L3.length)
|
||||
{
|
||||
return MPEG_2_L3[value];
|
||||
}
|
||||
break;
|
||||
case V_2_5:
|
||||
if(0 <= value && value < MPEG_2_5_L3.length)
|
||||
{
|
||||
return MPEG_2_5_L3[value];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,73 +14,53 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MP3FrameInspector
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MP3FrameInspector.class);
|
||||
|
||||
public static void inspect(byte[] frames)
|
||||
public static void inspect(List<byte[]> frames)
|
||||
{
|
||||
if(frames == null || frames.length == 0)
|
||||
if(frames.isEmpty())
|
||||
{
|
||||
mLog.info("No Frame Data!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(frames.length % 144 != 0)
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
log("Frame byte array not multiple of 144 byte frame size - length:" + frames.length, frames);
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int frameCounter = 0;
|
||||
|
||||
//Check for erroneous frame sync
|
||||
for(int x = 0; x < frames.length - 1; x++)
|
||||
{
|
||||
if(frames[x] == (byte)0xFF && (frames[x + 1] & 0xE0) == 0xE0 && (x % 144) != 0)
|
||||
for(int x = 0; x < frame.length; x++)
|
||||
{
|
||||
log("Bad frame sync detected at byte " + x + " in frame " + (x / 144), frames);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(MP3Header.isValid(frame, x))
|
||||
{
|
||||
sb.append("\n").append(++frameCounter).append(": ");
|
||||
sb.append(MP3Header.inspect(frame, x)).append(" - ");
|
||||
}
|
||||
|
||||
//Check MP3 headers
|
||||
|
||||
// log("Frames ...", frames);
|
||||
}
|
||||
|
||||
private static void log(String message, byte[] frames)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(message).append("\n");
|
||||
|
||||
int frameCounter = 0;
|
||||
sb.append(frameCounter++).append(" ");
|
||||
|
||||
for(int x = 0; x < frames.length; x++)
|
||||
{
|
||||
if(x > 0 && x % 144 == 0)
|
||||
{
|
||||
sb.append("\n");
|
||||
sb.append(frameCounter++).append(" ");
|
||||
sb.append(String.format("%02X ", frame[x]));
|
||||
}
|
||||
|
||||
sb.append(String.format("%02X ", frames[x]));
|
||||
mLog.info(sb.toString());
|
||||
}
|
||||
|
||||
mLog.info(sb.toString());
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.info("Starting ...");
|
||||
MP3SilenceGenerator gen = new MP3SilenceGenerator();
|
||||
MP3SilenceGenerator gen = new MP3SilenceGenerator(AudioSampleRate.SR_8000, MP3Setting.getDefault());
|
||||
|
||||
byte[] audio = gen.generate(173);
|
||||
List<byte[]> audio = gen.generate(173);
|
||||
|
||||
inspect(audio);
|
||||
|
||||
|
||||
115
src/main/java/io/github/dsheirer/audio/convert/MP3Header.java
Normal file
115
src/main/java/io/github/dsheirer/audio/convert/MP3Header.java
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
/**
|
||||
* Utility class for inspecting/parsing MP3 frames
|
||||
*/
|
||||
public class MP3Header
|
||||
{
|
||||
private MP3Header() {}
|
||||
|
||||
/**
|
||||
* Indicates if the byte array at the offset has a valid MP3 frame sync and is at least 4 bytes long
|
||||
* @param frame array of bytes
|
||||
* @param offset into the array
|
||||
* @return true if the first two offset bytes contain a frame sync and length from offset is at least 4
|
||||
*/
|
||||
public static boolean isValid(byte[] frame, int offset)
|
||||
{
|
||||
return frame.length >= (offset + 4) && hasSync(frame, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the byte array at the offset has a valid MP3 frame sync
|
||||
* @param frame array of bytes
|
||||
* @param offset into the array
|
||||
* @return true if the first two offset bytes contain a frame sync
|
||||
*/
|
||||
public static boolean hasSync(byte[] frame, int offset)
|
||||
{
|
||||
return (frame[offset] & 0xFF) == 0xFF && (frame[offset + 1] & 0xE0) == 0xE0;
|
||||
}
|
||||
|
||||
/**
|
||||
* MPEG version for the frame
|
||||
*/
|
||||
public static MPEGVersion getMPEGVersion(byte[] frame, int offset)
|
||||
{
|
||||
return MPEGVersion.fromValue((frame[offset + 1] & 0x18) >> 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* MPEG layer for the frame. Normally layer 3 for audio.
|
||||
*/
|
||||
public static MPEGLayer getMPEGLayer(byte[] frame, int offset)
|
||||
{
|
||||
return MPEGLayer.fromValue((frame[offset + 1] & 0x6) >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* MP3 bit rate
|
||||
*/
|
||||
public static int getBitRate(byte[] frame, int offset)
|
||||
{
|
||||
return MP3BitRate.getLayer3BitRate(getMPEGVersion(frame, offset), (frame[offset + 2] & 0xF0) >> 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input audio sample rate.
|
||||
*/
|
||||
public static int getSampleRate(byte[] frame, int offset)
|
||||
{
|
||||
return MP3SampleRate.getSampleRate(getMPEGVersion(frame, offset), (frame[offset + 2] & 0x0C) >> 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel mode
|
||||
*/
|
||||
public static ChannelMode getChannelMode(byte[] frame, int offset)
|
||||
{
|
||||
return ChannelMode.fromValue((frame[offset + 3] & 0xC0) >> 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the frame bytes and provides details about the header.
|
||||
*/
|
||||
public static String inspect(byte[] frame, int offset)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("MP3 FRAME");
|
||||
|
||||
if(!isValid(frame, offset))
|
||||
{
|
||||
sb.append(" INVALID");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
sb.append(" VER:").append(getMPEGVersion(frame, offset));
|
||||
sb.append(" MODE:").append(getChannelMode(frame, offset));
|
||||
sb.append(" ").append(getMPEGLayer(frame, offset));
|
||||
sb.append(" BR:").append(getBitRate(frame, offset));
|
||||
sb.append(" SR:").append(getSampleRate(frame, offset));
|
||||
|
||||
//TODO: add MONO/STEREO
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
public class MP3SampleRate
|
||||
{
|
||||
private final static int[] MPEG_1 = new int[]{44100, 48000, 32000};
|
||||
private final static int[] MPEG_2 = new int[]{22050, 24000, 16000};
|
||||
private final static int[] MPEG2_5 = new int[]{11025, 12000, 8000};
|
||||
|
||||
public static int getSampleRate(MPEGVersion version, int value)
|
||||
{
|
||||
switch(version)
|
||||
{
|
||||
case V_1:
|
||||
if(0 <= value && value < MPEG_1.length)
|
||||
{
|
||||
return MPEG_1[value];
|
||||
}
|
||||
break;
|
||||
case V_2:
|
||||
if(0 <= value && value < MPEG_2.length)
|
||||
{
|
||||
return MPEG_2[value];
|
||||
}
|
||||
break;
|
||||
case V_2_5:
|
||||
if(0 <= value && value < MPEG2_5.length)
|
||||
{
|
||||
return MPEG2_5[value];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
|
||||
/**
|
||||
* MP3 encoder settings.
|
||||
*
|
||||
* A curated list of LAME audio encoder settings that are optimized for mono-channel voice that is originally
|
||||
* sampled at 8,000 Hz.
|
||||
*
|
||||
* See: https://svn.code.sf.net/p/lame/svn/trunk/lame/doc/html/usage.html
|
||||
*/
|
||||
public enum MP3Setting
|
||||
{
|
||||
CBR_16(false, 16, "CBR - Constant Bit Rate 16 kbps (default)"),
|
||||
CBR_32(false, 32, "CBR - Constant Bit Rate 32 kbps"),
|
||||
ABR_56(true, 56, "ABR - Average Bit Rate 56 kbps"),
|
||||
VBR_5(true, Lame.QUALITY_MIDDLE, "VBR - Middle Quality (5)"),
|
||||
VBR_7(true, Lame.QUALITY_LOW, "VBR - Low Quality (7)");
|
||||
|
||||
private boolean mVariableBitRate;
|
||||
private int mSetting;
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param variableBitRate flag
|
||||
* @param setting for the entry
|
||||
* @param label to display
|
||||
*/
|
||||
MP3Setting(boolean variableBitRate, int setting, String label)
|
||||
{
|
||||
mVariableBitRate = variableBitRate;
|
||||
mSetting = setting;
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default setting
|
||||
*/
|
||||
public static MP3Setting getDefault()
|
||||
{
|
||||
return MP3Setting.CBR_16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the entry is Constant Bit Rate (CBR), or Average/Variable Bit Rate (ABR or VBR)
|
||||
* @return true if CBR
|
||||
*/
|
||||
public boolean isVariableBitRate()
|
||||
{
|
||||
return mVariableBitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* LAME encoder quality value
|
||||
*/
|
||||
public int getSetting()
|
||||
{
|
||||
return mSetting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,15 +14,14 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
@@ -31,47 +30,42 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
public static final int MP3_BIT_RATE = 16;
|
||||
public static final boolean CONSTANT_BIT_RATE = false;
|
||||
|
||||
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE);
|
||||
private MP3AudioConverter mMP3AudioConverter;
|
||||
private AudioSampleRate mAudioSampleRate;
|
||||
private byte[] mPreviousPartialFrameData;
|
||||
|
||||
/**
|
||||
* Generates MP3 audio silence frames
|
||||
*/
|
||||
public MP3SilenceGenerator()
|
||||
public MP3SilenceGenerator(AudioSampleRate audioSampleRate, MP3Setting setting)
|
||||
{
|
||||
mAudioSampleRate = audioSampleRate;
|
||||
mMP3AudioConverter = new MP3AudioConverter(audioSampleRate, setting);
|
||||
}
|
||||
|
||||
public byte[] generate(long duration)
|
||||
/**
|
||||
* Generates silence frames
|
||||
* @param duration_ms in milliseconds
|
||||
* @return
|
||||
*/
|
||||
public List<byte[]> generate(long duration_ms)
|
||||
{
|
||||
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||
double duration_secs = (double)duration_ms / 1000.0;
|
||||
|
||||
//We generate silence at 8000 kHz, because it gets resampled by the silence generator to the target rate
|
||||
int length = (int)(duration_secs * AudioSampleRate.SR_8000.getSampleRate());
|
||||
|
||||
List<float[]> silenceBuffers = new ArrayList<>();
|
||||
silenceBuffers.add(new float[length]);
|
||||
byte[] frameData = mGenerator.convert(silenceBuffers);
|
||||
|
||||
frameData = merge(mPreviousPartialFrameData, frameData);
|
||||
|
||||
if(frameData != null && frameData.length > 0)
|
||||
int added = 0;
|
||||
while(added < length)
|
||||
{
|
||||
if(frameData.length < 144)
|
||||
{
|
||||
mPreviousPartialFrameData = frameData;
|
||||
return null;
|
||||
}
|
||||
else if((frameData.length % 144) == 0)
|
||||
{
|
||||
mPreviousPartialFrameData = null;
|
||||
return frameData;
|
||||
}
|
||||
else
|
||||
{
|
||||
int integralFrameLength = (int)(frameData.length / 144) * 144;
|
||||
mPreviousPartialFrameData = Arrays.copyOfRange(frameData, integralFrameLength, frameData.length);
|
||||
return Arrays.copyOf(frameData, integralFrameLength);
|
||||
}
|
||||
int chunk = Math.min(length - added, 256);
|
||||
silenceBuffers.add(new float[chunk]);
|
||||
added += chunk;
|
||||
}
|
||||
|
||||
return null;
|
||||
return mMP3AudioConverter.convert(silenceBuffers);
|
||||
}
|
||||
|
||||
private static byte[] merge(byte[] a, byte[] b)
|
||||
@@ -100,19 +94,24 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
for(long x = 243; x < 500; x ++)
|
||||
MP3SilenceGenerator generator = new MP3SilenceGenerator(AudioSampleRate.SR_44100, MP3Setting.VBR_7);
|
||||
|
||||
int x = 100;
|
||||
|
||||
// for(; x < 1000; x += 10)
|
||||
// {
|
||||
List<byte[]> silenceFrames = generator.generate(x);
|
||||
|
||||
mLog.debug("Generated [" + silenceFrames.size() + "] silence frames");
|
||||
|
||||
for(byte[] silence: silenceFrames)
|
||||
{
|
||||
MP3SilenceGenerator generator = new MP3SilenceGenerator();
|
||||
|
||||
byte[] silence = generator.generate(x);
|
||||
|
||||
if(silence != null && silence.length > 0)
|
||||
{
|
||||
mLog.debug("Silence:" + x + " Length:" + silence.length);
|
||||
MP3FrameInspector.inspect(silence);
|
||||
}
|
||||
mLog.debug("Silence:" + x + " Length:" + silence.length);
|
||||
}
|
||||
|
||||
MP3FrameInspector.inspect(silenceFrames);
|
||||
// }
|
||||
|
||||
mLog.debug("Finished");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
/**
|
||||
* MPEG Layer
|
||||
*
|
||||
* see: http://www.mp3-tech.org/programmer/frame_header.html
|
||||
*/
|
||||
public enum MPEGLayer
|
||||
{
|
||||
RESERVED("LAYER-RESERVED"),
|
||||
LAYER3("LAYER-3"),
|
||||
LAYER2("LAYER-2"),
|
||||
LAYER1("LAYER-1");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
MPEGLayer(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public static MPEGLayer fromValue(int value)
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 1:
|
||||
return LAYER3;
|
||||
case 2:
|
||||
return LAYER2;
|
||||
case 3:
|
||||
return LAYER1;
|
||||
default:
|
||||
case 0:
|
||||
return RESERVED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
/**
|
||||
* MPEG Version
|
||||
*
|
||||
* see: http://www.mp3-tech.org/programmer/frame_header.html
|
||||
*/
|
||||
public enum MPEGVersion
|
||||
{
|
||||
V_2_5("2.5"),
|
||||
RESERVED("RESERVED"),
|
||||
V_2("2"),
|
||||
V_1("1");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
MPEGVersion(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public static MPEGVersion fromValue(int value)
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 0:
|
||||
return V_2_5;
|
||||
case 2:
|
||||
return V_2;
|
||||
case 3:
|
||||
return V_1;
|
||||
default:
|
||||
return RESERVED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user