@@ -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