@@ -47,11 +47,12 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
47
47
private readonly _enableOutro : HTMLInputElement = input ( { type : "checkbox" } ) ;
48
48
private readonly _formatSelect : HTMLSelectElement = select ( { style : "width: 100%;" } ,
49
49
option ( { value : "wav" } , "Export to .wav file." ) ,
50
+ option ( { value : "mp3" } , "Export to .mp3 file." ) ,
50
51
option ( { value : "midi" } , "Export to .mid file." ) ,
51
52
option ( { value : "json" } , "Export to .json file." ) ,
52
53
) ;
53
- private readonly _cancelButton : HTMLButtonElement = button ( { className : "cancelButton" } ) ;
54
- private readonly _exportButton : HTMLButtonElement = button ( { className : "exportButton" , style : "width:45%;" } , "Export" ) ;
54
+ private readonly _cancelButton : HTMLButtonElement = button ( { class : "cancelButton" } ) ;
55
+ private readonly _exportButton : HTMLButtonElement = button ( { class : "exportButton" , style : "width:45%;" } , "Export" ) ;
55
56
private static readonly midiSustainInstruments : number [ ] = [
56
57
0x4A , // rounded -> recorder
57
58
0x47 , // triangle -> clarinet
@@ -83,7 +84,7 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
83
84
0x6A , // spiky -> shamisen
84
85
] ;
85
86
86
- public readonly container : HTMLDivElement = div ( { className : "prompt noSelection" , style : "width: 200px;" } ,
87
+ public readonly container : HTMLDivElement = div ( { class : "prompt noSelection" , style : "width: 200px;" } ,
87
88
h2 ( "Export Options" ) ,
88
89
div ( { style : "display: flex; flex-direction: row; align-items: center; justify-content: space-between;" } ,
89
90
"File name:" ,
@@ -101,7 +102,8 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
101
102
div ( { style : "display: table-cell; vertical-align: middle;" } , this . _enableOutro ) ,
102
103
) ,
103
104
) ,
104
- div ( { className : "selectContainer" , style : "width: 100%;" } , this . _formatSelect ) ,
105
+ div ( { class : "selectContainer" , style : "width: 100%;" } , this . _formatSelect ) ,
106
+ div ( { style : "text-align: left;" } , "(Be patient, exporting may take some time...)" ) ,
105
107
div ( { style : "display: flex; flex-direction: row-reverse; justify-content: space-between;" } ,
106
108
this . _exportButton ,
107
109
) ,
@@ -181,6 +183,9 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
181
183
case "wav" :
182
184
this . _exportToWav ( ) ;
183
185
break ;
186
+ case "mp3" :
187
+ this . _exportToMp3 ( ) ;
188
+ break ;
184
189
case "midi" :
185
190
this . _exportToMidi ( ) ;
186
191
break ;
@@ -192,9 +197,9 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
192
197
}
193
198
}
194
199
195
- private _exportToWav ( ) : void {
196
-
200
+ private _synthesize ( sampleRate : number ) : { recordedSamplesL : Float32Array , recordedSamplesR : Float32Array } {
197
201
const synth : Synth = new Synth ( this . _doc . song ) ;
202
+ synth . samplesPerSecond = sampleRate ;
198
203
synth . loopRepeatCount = Number ( this . _loopDropDown . value ) - 1 ;
199
204
if ( ! this . _enableIntro . checked ) {
200
205
for ( let introIter : number = 0 ; introIter < this . _doc . song . loopStart ; introIter ++ ) {
@@ -208,8 +213,15 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
208
213
synth . synthesize ( recordedSamplesL , recordedSamplesR , sampleFrames ) ;
209
214
//console.log("export timer", (performance.now() - timer) / 1000.0);
210
215
216
+ return { recordedSamplesL, recordedSamplesR} ;
217
+ }
218
+
219
+ private _exportToWav ( ) : void {
220
+ const sampleRate : number = 48000 ; // Use professional video editing standard sample rate for .wav file export.
221
+ const { recordedSamplesL, recordedSamplesR} = this . _synthesize ( sampleRate ) ;
222
+ const sampleFrames : number = recordedSamplesL . length ;
223
+
211
224
const wavChannelCount : number = 2 ;
212
- const sampleRate : number = 44100 ;
213
225
const bytesPerSample : number = 2 ;
214
226
const bitsPerSample : number = 8 * bytesPerSample ;
215
227
const sampleCount : number = wavChannelCount * sampleFrames ;
@@ -235,9 +247,10 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
235
247
236
248
if ( bytesPerSample > 1 ) {
237
249
// usually samples are signed.
250
+ const range : number = ( 1 << ( bitsPerSample - 1 ) ) - 1 ;
238
251
for ( let i : number = 0 ; i < sampleFrames ; i ++ ) {
239
- let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * ( ( 1 << ( bitsPerSample - 1 ) ) - 1 ) ) ;
240
- let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * ( ( 1 << ( bitsPerSample - 1 ) ) - 1 ) ) ;
252
+ let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * range ) ;
253
+ let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * range ) ;
241
254
if ( bytesPerSample == 2 ) {
242
255
data . setInt16 ( index , valL , true ) ; index += 2 ;
243
256
data . setInt16 ( index , valR , true ) ; index += 2 ;
@@ -258,12 +271,55 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
258
271
}
259
272
}
260
273
261
- const blob = new Blob ( [ arrayBuffer ] , { type : "audio/wav" } ) ;
274
+ const blob : Blob = new Blob ( [ arrayBuffer ] , { type : "audio/wav" } ) ;
262
275
save ( blob , this . _fileName . value . trim ( ) + ".wav" ) ;
263
-
264
276
this . _close ( ) ;
265
277
}
266
278
279
+ private _exportToMp3 ( ) : void {
280
+ const whenEncoderIsAvailable = ( ) : void => {
281
+ const sampleRate : number = 44100 ; // Use consumer CD standard sample rate for .mp3 export.
282
+ const { recordedSamplesL, recordedSamplesR} = this . _synthesize ( sampleRate ) ;
283
+
284
+ const lamejs : any = ( < any > window ) [ "lamejs" ] ;
285
+ const channelCount : number = 2 ;
286
+ const kbps : number = 192 ;
287
+ const sampleBlockSize : number = 1152 ;
288
+ const mp3encoder : any = new lamejs . Mp3Encoder ( channelCount , sampleRate , kbps ) ;
289
+ const mp3Data : any [ ] = [ ] ;
290
+
291
+ const left : Int16Array = new Int16Array ( recordedSamplesL . length ) ;
292
+ const right : Int16Array = new Int16Array ( recordedSamplesR . length ) ;
293
+ const range : number = ( 1 << 15 ) - 1 ;
294
+ for ( let i : number = 0 ; i < recordedSamplesL . length ; i ++ ) {
295
+ left [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * range ) ;
296
+ right [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * range ) ;
297
+ }
298
+
299
+ for ( let i : number = 0 ; i < left . length ; i += sampleBlockSize ) {
300
+ const leftChunk : Int16Array = left . subarray ( i , i + sampleBlockSize ) ;
301
+ const rightChunk : Int16Array = right . subarray ( i , i + sampleBlockSize ) ;
302
+ const mp3buf : any = mp3encoder . encodeBuffer ( leftChunk , rightChunk ) ;
303
+ if ( mp3buf . length > 0 ) mp3Data . push ( mp3buf ) ;
304
+ }
305
+ const mp3buf : any = mp3encoder . flush ( ) ;
306
+ if ( mp3buf . length > 0 ) mp3Data . push ( mp3buf ) ;
307
+
308
+ const blob : Blob = new Blob ( mp3Data , { type : "audio/mp3" } ) ;
309
+ save ( blob , this . _fileName . value . trim ( ) + ".mp3" ) ;
310
+ this . _close ( ) ;
311
+ }
312
+
313
+ if ( "lamejs" in window ) {
314
+ whenEncoderIsAvailable ( ) ;
315
+ } else {
316
+ var script = document . createElement ( "script" ) ;
317
+ script . src = "https://cdn.jsdelivr.net/npm/lamejs@1.2.0/lame.min.js" ;
318
+ script . onload = whenEncoderIsAvailable ;
319
+ document . head . appendChild ( script ) ;
320
+ }
321
+ }
322
+
267
323
private _exportToMidi ( ) : void {
268
324
const song : Song = this . _doc . song ;
269
325
const midiTicksPerBeepBoxTick : number = 2 ;
@@ -699,7 +755,7 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
699
755
writer . rewriteUint32 ( trackStartIndex , writer . getWriteIndex ( ) - trackStartIndex - 4 ) ;
700
756
}
701
757
702
- const blob = new Blob ( [ writer . toCompactArrayBuffer ( ) ] , { type : "audio/midi" } ) ;
758
+ const blob : Blob = new Blob ( [ writer . toCompactArrayBuffer ( ) ] , { type : "audio/midi" } ) ;
703
759
save ( blob , this . _fileName . value . trim ( ) + ".mid" ) ;
704
760
705
761
this . _close ( ) ;
@@ -708,7 +764,7 @@ import {MidiChunkType, MidiFileFormat, MidiControlEventMessage, MidiEventType, M
708
764
private _exportToJson ( ) : void {
709
765
const jsonObject : Object = this . _doc . song . toJsonObject ( this . _enableIntro . checked , Number ( this . _loopDropDown . value ) , this . _enableOutro . checked ) ;
710
766
const jsonString : string = JSON . stringify ( jsonObject , null , '\t' ) ;
711
- const blob = new Blob ( [ jsonString ] , { type : "application/json" } ) ;
767
+ const blob : Blob = new Blob ( [ jsonString ] , { type : "application/json" } ) ;
712
768
save ( blob , this . _fileName . value . trim ( ) + ".json" ) ;
713
769
this . _close ( ) ;
714
770
}
0 commit comments