Skip to content

Commit c4d9b88

Browse files
committed
Added MP3 quality slider to export window
1 parent 49afd9b commit c4d9b88

2 files changed

Lines changed: 90 additions & 41 deletions

File tree

src/main/java/net/raphimc/noteblocktool/audio/library/LameLibrary.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ public interface LameLibrary extends Library {
2828

2929
LameLibrary INSTANCE = loadNative();
3030

31+
int vbr_off = 0;
32+
int vbr_mt = 1; /* obsolete, same as vbr_mtrh */
33+
int vbr_rh = 2;
34+
int vbr_abr = 3;
35+
int vbr_mtrh = 4;
36+
int vbr_max_indicator = 5; /* Don't use this! It's used for sanity checks. */
37+
int vbr_default = vbr_mtrh; /* change this to change the default VBR mode of LAME */
38+
3139
static LameLibrary loadNative() {
3240
try {
3341
final Map<String, Object> options = new HashMap<>();
@@ -50,12 +58,18 @@ static boolean isLoaded() {
5058

5159
int lame_set_num_channels(final Pointer lame, final int num_channels);
5260

61+
int lame_set_VBR(final Pointer lame, final int vbr_mode);
62+
63+
int lame_set_VBR_quality(final Pointer lame, float vbr_quality);
64+
5365
int lame_init_params(final Pointer lame);
5466

5567
int lame_encode_buffer_interleaved_ieee_float(final Pointer lame, final float[] pcm, final int num_samples, final byte[] mp3buf, final int mp3buf_size);
5668

5769
int lame_encode_flush(final Pointer lame, final byte[] mp3buf, final int mp3buf_size);
5870

71+
int lame_get_lametag_frame(final Pointer lame, final byte[] buffer, final int size);
72+
5973
int lame_close(final Pointer lame);
6074

6175
}

src/main/java/net/raphimc/noteblocktool/frames/ExportFrame.java

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ public class ExportFrame extends JFrame {
7070
private final JCheckBox audioMixerThreaded = new JCheckBox("Multithreaded Rendering");
7171
private final JLabel sampleRateLabel = new JLabel("Sample Rate:");
7272
private final JSpinner sampleRate = new JSpinner(new SpinnerNumberModel(48000, 8000, 192000, 8000));
73-
private final JLabel bitDepthLabel = new JLabel("PCM Bit Depth:");
74-
private final JComboBox<BitDepth> bitDepth = new JComboBox<>(BitDepth.values());
73+
private final JLabel wavBitDepthLabel = new JLabel("WAV Bit Depth:");
74+
private final JComboBox<WavBitDepth> wavBitDepth = new JComboBox<>(WavBitDepth.values());
75+
private final JLabel mp3QualityLabel = new JLabel("MP3 Quality:");
76+
private final JSlider mp3Quality = new JSlider(0, 100, 60);
7577
private final JLabel channelsLabel = new JLabel("Channels:");
7678
private final JComboBox<Channels> channels = new JComboBox<>(Channels.values());
7779
private final JLabel volumeLabel = new JLabel("Volume:");
@@ -122,14 +124,22 @@ private void initComponents() {
122124
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.sampleRateLabel);
123125
GBC.create(root).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.sampleRate);
124126

125-
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.bitDepthLabel);
126-
GBC.create(root).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.bitDepth, () -> {
127-
this.bitDepth.setSelectedIndex(1);
127+
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.wavBitDepthLabel);
128+
GBC.create(root).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.wavBitDepth, () -> {
129+
this.wavBitDepth.setSelectedItem(WavBitDepth.PCM16);
130+
});
131+
132+
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.mp3QualityLabel);
133+
GBC.create(root).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.mp3Quality, () -> {
134+
this.mp3Quality.setMajorTickSpacing(10);
135+
this.mp3Quality.setMinorTickSpacing(5);
136+
this.mp3Quality.setPaintTicks(true);
137+
this.mp3Quality.setPaintLabels(true);
128138
});
129139

130140
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.channelsLabel);
131141
GBC.create(root).grid(1, gridy++).insets(5, 0, 0, 5).weightx(1).fill(GBC.HORIZONTAL).add(this.channels, () -> {
132-
this.channels.setSelectedIndex(1);
142+
this.channels.setSelectedItem(Channels.STEREO);
133143
});
134144

135145
GBC.create(root).grid(0, gridy).insets(5, 5, 0, 5).anchor(GBC.LINE_START).add(this.volumeLabel);
@@ -177,8 +187,11 @@ private void updateVisibility() {
177187
this.sampleRateLabel.setVisible(outputFormat.isAudioFile());
178188
this.sampleRate.setVisible(outputFormat.isAudioFile());
179189

180-
this.bitDepthLabel.setVisible(outputFormat.isAudioFile() && !outputFormat.equals(OutputFormat.MP3));
181-
this.bitDepth.setVisible(outputFormat.isAudioFile() && !outputFormat.equals(OutputFormat.MP3));
190+
this.wavBitDepthLabel.setVisible(outputFormat.isAudioFile() && outputFormat.equals(OutputFormat.WAV));
191+
this.wavBitDepth.setVisible(outputFormat.isAudioFile() && outputFormat.equals(OutputFormat.WAV));
192+
193+
this.mp3QualityLabel.setVisible(outputFormat.isAudioFile() && outputFormat.equals(OutputFormat.MP3));
194+
this.mp3Quality.setVisible(outputFormat.isAudioFile() && outputFormat.equals(OutputFormat.MP3));
182195

183196
this.channelsLabel.setVisible(outputFormat.isAudioFile());
184197
this.channels.setVisible(outputFormat.isAudioFile());
@@ -220,7 +233,8 @@ private void export() {
220233
this.audioMixerGlobalNormalization.setEnabled(true);
221234
this.audioMixerThreaded.setEnabled(true);
222235
this.sampleRate.setEnabled(true);
223-
this.bitDepth.setEnabled(true);
236+
this.wavBitDepth.setEnabled(true);
237+
this.mp3Quality.setEnabled(true);
224238
this.channels.setEnabled(true);
225239
this.volume.setEnabled(true);
226240
this.timingJitter.setEnabled(true);
@@ -238,7 +252,8 @@ private void export() {
238252
this.audioMixerGlobalNormalization.setEnabled(false);
239253
this.audioMixerThreaded.setEnabled(false);
240254
this.sampleRate.setEnabled(false);
241-
this.bitDepth.setEnabled(false);
255+
this.wavBitDepth.setEnabled(false);
256+
this.mp3Quality.setEnabled(false);
242257
this.channels.setEnabled(false);
243258
this.volume.setEnabled(false);
244259
this.timingJitter.setEnabled(false);
@@ -409,7 +424,8 @@ private void doExport(final File outFile) {
409424
this.audioMixerGlobalNormalization.setEnabled(true);
410425
this.audioMixerThreaded.setEnabled(true);
411426
this.sampleRate.setEnabled(true);
412-
this.bitDepth.setEnabled(true);
427+
this.wavBitDepth.setEnabled(true);
428+
this.mp3Quality.setEnabled(true);
413429
this.channels.setEnabled(true);
414430
this.volume.setEnabled(true);
415431
this.timingJitter.setEnabled(true);
@@ -426,18 +442,11 @@ private void exportSong(final ListFrame.LoadedSong song, final File file, final
426442
if (outputFormat.isSongFile()) {
427443
this.writeSong(song, file, outputFormat.getSongFormat());
428444
} else if (outputFormat.isAudioFile()) {
429-
final AudioFormat audioFormat = new AudioFormat(
430-
((Number) this.sampleRate.getValue()).floatValue(),
431-
((BitDepth) this.bitDepth.getSelectedItem()).getBitDepth(),
432-
((Channels) this.channels.getSelectedItem()).getChannels(),
433-
true,
434-
false
435-
);
436-
final PcmFloatAudioFormat renderAudioFormat = new PcmFloatAudioFormat(audioFormat);
437-
445+
final PcmFloatAudioFormat renderAudioFormat = new PcmFloatAudioFormat(((Number) this.sampleRate.getValue()).floatValue(), ((Channels) this.channels.getSelectedItem()).getChannels());
438446
final SongRenderer songRenderer = switch ((AudioRendererType) this.audioSystem.getSelectedItem()) {
439447
case OPENAL -> new ProgressSongRenderer(song.song(), progressConsumer, soundData -> new OpenALAudioSystem(soundData, MAX_SOUNDS, renderAudioFormat));
440-
case AUDIO_MIXER -> new ProgressSongRenderer(song.song(), progressConsumer, soundData -> new AudioMixerAudioSystem(soundData, MAX_SOUNDS, !this.audioMixerGlobalNormalization.isSelected(), this.audioMixerThreaded.isSelected(), renderAudioFormat));
448+
case AUDIO_MIXER ->
449+
new ProgressSongRenderer(song.song(), progressConsumer, soundData -> new AudioMixerAudioSystem(soundData, MAX_SOUNDS, !this.audioMixerGlobalNormalization.isSelected(), this.audioMixerThreaded.isSelected(), renderAudioFormat));
441450
case BASS -> new ProgressSongRenderer(song.song(), progressConsumer, soundData -> new BassAudioSystem(soundData, MAX_SOUNDS, renderAudioFormat));
442451
};
443452
songRenderer.getAudioSystem().setMasterVolume(this.volume.getValue() / 100F);
@@ -451,45 +460,72 @@ private void exportSong(final ListFrame.LoadedSong song, final File file, final
451460
if (this.audioSystem.getSelectedItem() == AudioRendererType.AUDIO_MIXER && this.audioMixerGlobalNormalization.isSelected()) {
452461
SoundSampleUtil.normalize(samples);
453462
}
454-
if (outputFormat.equals(OutputFormat.WAV) || outputFormat.equals(OutputFormat.AIF)) {
463+
if (outputFormat.equals(OutputFormat.WAV)) {
455464
progressConsumer.accept(101F);
465+
final AudioFormat audioFormat = new AudioFormat(
466+
((Number) this.sampleRate.getValue()).floatValue(),
467+
((WavBitDepth) this.wavBitDepth.getSelectedItem()).getBitDepth(),
468+
((Channels) this.channels.getSelectedItem()).getChannels(),
469+
true,
470+
false
471+
);
456472
final AudioInputStream audioInputStream = AudioIO.createAudioInputStream(samples, audioFormat);
457-
AudioSystem.write(audioInputStream, outputFormat.equals(OutputFormat.WAV) ? AudioFileFormat.Type.WAVE : AudioFileFormat.Type.AIFF, file);
473+
AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, file);
458474
audioInputStream.close();
459475
} else if (outputFormat.equals(OutputFormat.MP3)) {
460476
progressConsumer.accept(200F);
461-
final FileOutputStream fos = new FileOutputStream(file);
462-
final int numSamples = samples.length / audioFormat.getChannels();
463-
final byte[] mp3Buffer = new byte[(int) (1.25 * numSamples + 7200)];
464477
final Pointer lame = LameLibrary.INSTANCE.lame_init();
465478
if (lame == null) {
466479
throw new IllegalStateException("Failed to initialize LAME encoder");
467480
}
468-
int result = LameLibrary.INSTANCE.lame_set_in_samplerate(lame, (int) audioFormat.getSampleRate());
481+
int result = LameLibrary.INSTANCE.lame_set_in_samplerate(lame, (int) renderAudioFormat.getSampleRate());
469482
if (result < 0) {
470483
throw new IllegalStateException("Failed to set sample rate: " + result);
471484
}
472-
result = LameLibrary.INSTANCE.lame_set_num_channels(lame, audioFormat.getChannels());
485+
result = LameLibrary.INSTANCE.lame_set_num_channels(lame, renderAudioFormat.getChannels());
473486
if (result < 0) {
474487
throw new IllegalStateException("Failed to set channels: " + result);
475488
}
489+
result = LameLibrary.INSTANCE.lame_set_VBR(lame, LameLibrary.vbr_default);
490+
if (result < 0) {
491+
throw new IllegalStateException("Failed to set VBR mode: " + result);
492+
}
493+
result = LameLibrary.INSTANCE.lame_set_VBR_quality(lame, (1F - (this.mp3Quality.getValue() / 100F)) * 9F);
494+
if (result < 0) {
495+
throw new IllegalStateException("Failed to set VBR quality: " + result);
496+
}
476497
result = LameLibrary.INSTANCE.lame_init_params(lame);
477498
if (result < 0) {
478499
throw new IllegalStateException("Failed to initialize LAME parameters: " + result);
479500
}
480-
result = LameLibrary.INSTANCE.lame_encode_buffer_interleaved_ieee_float(lame, samples, numSamples, mp3Buffer, mp3Buffer.length);
501+
502+
final int frameCount = samples.length / renderAudioFormat.getChannels();
503+
final byte[] dataBuffer = new byte[(int) (1.25F * frameCount + 7200)];
504+
final int dataLength = LameLibrary.INSTANCE.lame_encode_buffer_interleaved_ieee_float(lame, samples, frameCount, dataBuffer, dataBuffer.length);
505+
if (dataLength < 0) {
506+
throw new IllegalStateException("Failed to encode buffer: " + dataLength);
507+
}
508+
final byte[] trailerBuffer = new byte[7200];
509+
final int trailerLength = LameLibrary.INSTANCE.lame_encode_flush(lame, trailerBuffer, trailerBuffer.length);
510+
if (trailerLength < 0) {
511+
throw new IllegalStateException("Failed to flush encoder: " + trailerLength);
512+
}
513+
final byte[] headerBuffer = new byte[LameLibrary.INSTANCE.lame_get_lametag_frame(lame, null, 0)];
514+
final int headerLength = LameLibrary.INSTANCE.lame_get_lametag_frame(lame, headerBuffer, headerBuffer.length);
515+
if (headerLength < 0) {
516+
throw new IllegalStateException("Failed to get LAME tag frame: " + headerLength);
517+
}
518+
result = LameLibrary.INSTANCE.lame_close(lame);
481519
if (result < 0) {
482-
throw new IllegalStateException("Failed to encode buffer: " + result);
520+
throw new IllegalStateException("Failed to close encoder: " + result);
483521
}
522+
484523
progressConsumer.accept(101F);
485-
fos.write(mp3Buffer, 0, result);
486-
result = LameLibrary.INSTANCE.lame_encode_flush(lame, mp3Buffer, mp3Buffer.length);
487-
if (result < 0) {
488-
throw new IllegalStateException("Failed to flush encoder: " + result);
524+
try (FileOutputStream fos = new FileOutputStream(file)) {
525+
fos.write(headerBuffer, 0, headerLength);
526+
fos.write(dataBuffer, 0, dataLength);
527+
fos.write(trailerBuffer, 0, trailerLength);
489528
}
490-
fos.write(mp3Buffer, 0, result);
491-
LameLibrary.INSTANCE.lame_close(lame);
492-
fos.close();
493529
} else {
494530
throw new UnsupportedOperationException("Unsupported output format: " + this.format.getSelectedIndex());
495531
}
@@ -514,8 +550,7 @@ private enum OutputFormat {
514550
MCSP2("MCSP2", "mcsp2", SongFormat.MCSP2),
515551
TXT("TXT", "txt", SongFormat.TXT),
516552
MP3("MP3 (Using LAME encoder)", "mp3", null),
517-
WAV("WAV", "wav", null),
518-
AIF("AIF", "aif", null);
553+
WAV("WAV", "wav", null);
519554

520555
private final String name;
521556
private final String extension;
@@ -566,7 +601,7 @@ public String toString() {
566601
}
567602
}
568603

569-
private enum BitDepth {
604+
private enum WavBitDepth {
570605
PCM8("PCM 8", 8),
571606
PCM16("PCM 16", 16),
572607
PCM24("PCM 24", 24),
@@ -575,7 +610,7 @@ private enum BitDepth {
575610
private final String name;
576611
private final int bitDepth;
577612

578-
BitDepth(final String name, final int bitDepth) {
613+
WavBitDepth(final String name, final int bitDepth) {
579614
this.name = name;
580615
this.bitDepth = bitDepth;
581616
}

0 commit comments

Comments
 (0)