22
33import { InstrumentType , EnvelopeType , Config , getArpeggioPitchIndex } from "../synth/SynthConfig" ;
44import { Instrument , Pattern , Note , Song , Synth } from "../synth/synth" ;
5- // import {ColorConfig} from "./ColorConfig";
5+ import { ColorConfig } from "./ColorConfig" ;
66import { Preset , EditorConfig } from "./EditorConfig" ;
77import { SongDocument } from "./SongDocument" ;
88import { Prompt } from "./Prompt" ;
@@ -41,6 +41,14 @@ function save(blob: Blob, name: string): void {
4141}
4242
4343export class ExportPrompt implements Prompt {
44+ private synth : Synth ;
45+ private thenExportTo : string ;
46+ private recordedSamplesL : Float32Array ;
47+ private recordedSamplesR : Float32Array ;
48+ private sampleFrames : number ;
49+ private totalChunks : number ;
50+ private currentChunk : number ;
51+ private outputStarted : boolean = false ;
4452 private readonly _fileName : HTMLInputElement = input ( { type : "text" , style : "width: 10em;" , value : "BeepBox-Song" , maxlength : 250 , "autofocus" : "autofocus" } ) ;
4553 private readonly _computedSamplesLabel : HTMLDivElement = div ( { style : "width: 10em;" } , new Text ( "0:00" ) ) ;
4654 private readonly _enableIntro : HTMLInputElement = input ( { type : "checkbox" } ) ;
@@ -54,6 +62,12 @@ export class ExportPrompt implements Prompt {
5462 ) ;
5563 private readonly _cancelButton : HTMLButtonElement = button ( { class : "cancelButton" } ) ;
5664 private readonly _exportButton : HTMLButtonElement = button ( { class : "exportButton" , style : "width:45%;" } , "Export" ) ;
65+ private readonly _outputProgressBar : HTMLDivElement = div ( { style : `width: 0%; background: ${ ColorConfig . loopAccent } ; height: 100%; position: absolute; z-index: 2;` } ) ;
66+ private readonly _outputProgressLabel : HTMLDivElement = div ( { style : `position: relative; top: -1px; z-index: 3;` } , "0%" ) ;
67+ private readonly _outputProgressContainer : HTMLDivElement = div ( { style : `height: 12px; background: ${ ColorConfig . uiWidgetBackground } ; display: block; position: relative; z-index: 1;` } ,
68+ this . _outputProgressBar ,
69+ this . _outputProgressLabel ,
70+ ) ;
5771 private static readonly midiSustainInstruments : number [ ] = [
5872 0x4A , // rounded -> recorder
5973 0x47 , // triangle -> clarinet
@@ -108,7 +122,8 @@ export class ExportPrompt implements Prompt {
108122 ) ,
109123 ) ,
110124 div ( { class : "selectContainer" , style : "width: 100%;" } , this . _formatSelect ) ,
111- div ( { style : "text-align: left;" } , "(Be patient, exporting may take some time...)" ) ,
125+ div ( { style : "text-align: left;" } , "Exporting can be slow. Reloading the page or clicking the X will cancel it. Please be patient." ) ,
126+ this . _outputProgressContainer ,
112127 div ( { style : "display: flex; flex-direction: row-reverse; justify-content: space-between;" } ,
113128 this . _exportButton ,
114129 ) ,
@@ -165,6 +180,7 @@ export class ExportPrompt implements Prompt {
165180 }
166181
167182 private _close = ( ) : void => {
183+ this . outputStarted = false ;
168184 this . _doc . undo ( ) ;
169185 }
170186
@@ -212,49 +228,120 @@ export class ExportPrompt implements Prompt {
212228 }
213229
214230 private _export = ( ) : void => {
231+ if ( this . outputStarted == true )
232+ return ;
215233 window . localStorage . setItem ( "exportFormat" , this . _formatSelect . value ) ;
216234 switch ( this . _formatSelect . value ) {
217235 case "wav" :
218- this . _exportToWav ( ) ;
236+ this . outputStarted = true ;
237+ this . _exportTo ( "wav" ) ;
219238 break ;
220239 case "mp3" :
221- this . _exportToMp3 ( ) ;
240+ this . outputStarted = true ;
241+ this . _exportTo ( "mp3" ) ;
222242 break ;
223243 case "midi" :
244+ this . outputStarted = true ;
224245 this . _exportToMidi ( ) ;
225246 break ;
226247 case "json" :
248+ this . outputStarted = true ;
227249 this . _exportToJson ( ) ;
228250 break ;
229251 default :
230252 throw new Error ( "Unhandled file export type." ) ;
231253 }
232254 }
233255
234- private _synthesize ( sampleRate : number ) : { recordedSamplesL : Float32Array , recordedSamplesR : Float32Array } {
235- const synth : Synth = new Synth ( this . _doc . song ) ;
236- synth . samplesPerSecond = sampleRate ;
237- synth . loopRepeatCount = Number ( this . _loopDropDown . value ) - 1 ;
256+ private _synthesize ( ) : void {
257+ //const timer: number = performance.now();
258+
259+ // If output was stopped e.g. user clicked the close button, abort.
260+ if ( this . outputStarted == false ) {
261+ return ;
262+ }
263+
264+ // Update progress bar UI once per 5 sec of exported data
265+ const samplesPerChunk : number = this . synth . samplesPerSecond * 5 ; //e.g. 44100 * 5
266+ const currentFrame : number = this . currentChunk * samplesPerChunk ;
267+
268+ const samplesInChunk : number = Math . min ( samplesPerChunk , this . sampleFrames - currentFrame ) ;
269+ const tempSamplesL = new Float32Array ( samplesInChunk ) ;
270+ const tempSamplesR = new Float32Array ( samplesInChunk ) ;
271+
272+ this . synth . synthesize ( tempSamplesL , tempSamplesR , samplesInChunk ) ;
273+
274+ // Concatenate chunk data into final array
275+ this . recordedSamplesL . set ( tempSamplesL , currentFrame ) ;
276+ this . recordedSamplesR . set ( tempSamplesR , currentFrame ) ;
277+
278+ // Update UI
279+ this . _outputProgressBar . style . setProperty ( "width" , Math . round ( ( this . currentChunk + 1 ) / this . totalChunks * 100.0 ) + "%" ) ;
280+ this . _outputProgressLabel . innerText = Math . round ( ( this . currentChunk + 1 ) / this . totalChunks * 100.0 ) + "%" ;
281+
282+ // Next call, synthesize the next chunk.
283+ this . currentChunk ++ ;
284+
285+ if ( this . currentChunk >= this . totalChunks ) {
286+ // Done, call final function
287+ this . _outputProgressLabel . innerText = "Encoding..." ;
288+ if ( this . thenExportTo == "wav" ) {
289+ this . _exportToWavFinish ( ) ;
290+ }
291+ else if ( this . thenExportTo == "mp3" ) {
292+ this . _exportToMp3Finish ( ) ;
293+ }
294+ else {
295+ throw new Error ( "Unrecognized file export type chosen!" ) ;
296+ }
297+ }
298+ else {
299+ // Continue batch export
300+ setTimeout ( ( ) => { this . _synthesize ( ) ; } ) ;
301+ }
302+
303+ //console.log("export timer", (performance.now() - timer) / 1000.0);
304+ }
305+
306+ private _exportTo ( type : string ) : void {
307+ // Batch the export operation
308+ this . thenExportTo = type ;
309+ this . currentChunk = 0 ;
310+ this . synth = new Synth ( this . _doc . song ) ;
311+ if ( type == "wav" ) {
312+ this . synth . samplesPerSecond = 48000 ; // Use professional video editing standard sample rate for .wav file export.
313+ }
314+ else if ( type == "mp3" ) {
315+ this . synth . samplesPerSecond = 44100 ; // Use consumer CD standard sample rate for .mp3 export.
316+ }
317+ else {
318+ throw new Error ( "Unrecognized file export type chosen!" ) ;
319+ }
320+
321+ this . _outputProgressBar . style . setProperty ( "width" , "0%" ) ;
322+ this . _outputProgressLabel . innerText = "0%" ;
323+
324+ this . synth . loopRepeatCount = Number ( this . _loopDropDown . value ) - 1 ;
238325 if ( ! this . _enableIntro . checked ) {
239326 for ( let introIter : number = 0 ; introIter < this . _doc . song . loopStart ; introIter ++ ) {
240- synth . nextBar ( ) ;
327+ this . synth . nextBar ( ) ;
241328 }
242329 }
243- synth . computeLatestModValues ( ) ;
244- const sampleFrames : number = synth . getTotalSamples ( this . _enableIntro . checked , this . _enableOutro . checked , synth . loopRepeatCount ) ;
245- const recordedSamplesL : Float32Array = new Float32Array ( sampleFrames ) ;
246- const recordedSamplesR : Float32Array = new Float32Array ( sampleFrames ) ;
247- //const timer: number = performance.now( );
248- synth . synthesize ( recordedSamplesL , recordedSamplesR , sampleFrames ) ;
249- //console.log("export timer", (performance.now() - timer) / 1000.0 );
330+ this . synth . computeLatestModValues ( ) ;
331+
332+ this . sampleFrames = this . synth . getTotalSamples ( this . _enableIntro . checked , this . _enableOutro . checked , this . synth . loopRepeatCount ) ;
333+ // Compute how many UI updates will need to run to determine how many
334+ this . totalChunks = Math . ceil ( this . sampleFrames / ( this . synth . samplesPerSecond * 5 ) ) ;
335+ this . recordedSamplesL = new Float32Array ( this . sampleFrames ) ;
336+ this . recordedSamplesR = new Float32Array ( this . sampleFrames ) ;
250337
251- return { recordedSamplesL, recordedSamplesR } ;
338+ // Batch the actual export
339+ setTimeout ( ( ) => { this . _synthesize ( ) ; } ) ;
252340 }
253341
254- private _exportToWav ( ) : void {
255- const sampleRate : number = 48000 ; // Use professional video editing standard sample rate for .wav file export.
256- const { recordedSamplesL, recordedSamplesR } = this . _synthesize ( sampleRate ) ;
257- const sampleFrames : number = recordedSamplesL . length ;
342+ private _exportToWavFinish ( ) : void {
343+ const sampleFrames : number = this . recordedSamplesL . length ;
344+ const sampleRate : number = this . synth . samplesPerSecond ;
258345
259346 const wavChannelCount : number = 2 ;
260347 const bytesPerSample : number = 2 ;
@@ -284,8 +371,8 @@ export class ExportPrompt implements Prompt {
284371 // usually samples are signed.
285372 const range : number = ( 1 << ( bitsPerSample - 1 ) ) - 1 ;
286373 for ( let i : number = 0 ; i < sampleFrames ; i ++ ) {
287- let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * range ) ;
288- let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * range ) ;
374+ let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesL [ i ] ) ) * range ) ;
375+ let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesR [ i ] ) ) * range ) ;
289376 if ( bytesPerSample == 2 ) {
290377 data . setInt16 ( index , valL , true ) ; index += 2 ;
291378 data . setInt16 ( index , valR , true ) ; index += 2 ;
@@ -299,8 +386,8 @@ export class ExportPrompt implements Prompt {
299386 } else {
300387 // 8 bit samples are a special case: they are unsigned.
301388 for ( let i : number = 0 ; i < sampleFrames ; i ++ ) {
302- let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * 127 + 128 ) ;
303- let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * 127 + 128 ) ;
389+ let valL : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesL [ i ] ) ) * 127 + 128 ) ;
390+ let valR : number = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesR [ i ] ) ) * 127 + 128 ) ;
304391 data . setUint8 ( index , valL > 255 ? 255 : ( valL < 0 ? 0 : valL ) ) ; index ++ ;
305392 data . setUint8 ( index , valR > 255 ? 255 : ( valR < 0 ? 0 : valR ) ) ; index ++ ;
306393 }
@@ -312,24 +399,21 @@ export class ExportPrompt implements Prompt {
312399 this . _close ( ) ;
313400 }
314401
315- private _exportToMp3 ( ) : void {
402+ private _exportToMp3Finish ( ) : void {
316403 const whenEncoderIsAvailable = ( ) : void => {
317- const sampleRate : number = 44100 ; // Use consumer CD standard sample rate for .mp3 export.
318- const { recordedSamplesL, recordedSamplesR } = this . _synthesize ( sampleRate ) ;
319-
320404 const lamejs : any = ( < any > window ) [ "lamejs" ] ;
321405 const channelCount : number = 2 ;
322406 const kbps : number = 192 ;
323407 const sampleBlockSize : number = 1152 ;
324- const mp3encoder : any = new lamejs . Mp3Encoder ( channelCount , sampleRate , kbps ) ;
408+ const mp3encoder : any = new lamejs . Mp3Encoder ( channelCount , this . synth . samplesPerSecond , kbps ) ;
325409 const mp3Data : any [ ] = [ ] ;
326410
327- const left : Int16Array = new Int16Array ( recordedSamplesL . length ) ;
328- const right : Int16Array = new Int16Array ( recordedSamplesR . length ) ;
411+ const left : Int16Array = new Int16Array ( this . recordedSamplesL . length ) ;
412+ const right : Int16Array = new Int16Array ( this . recordedSamplesR . length ) ;
329413 const range : number = ( 1 << 15 ) - 1 ;
330- for ( let i : number = 0 ; i < recordedSamplesL . length ; i ++ ) {
331- left [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesL [ i ] ) ) * range ) ;
332- right [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , recordedSamplesR [ i ] ) ) * range ) ;
414+ for ( let i : number = 0 ; i < this . recordedSamplesL . length ; i ++ ) {
415+ left [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesL [ i ] ) ) * range ) ;
416+ right [ i ] = Math . floor ( Math . max ( - 1 , Math . min ( 1 , this . recordedSamplesR [ i ] ) ) * range ) ;
333417 }
334418
335419 for ( let i : number = 0 ; i < left . length ; i += sampleBlockSize ) {
@@ -338,6 +422,7 @@ export class ExportPrompt implements Prompt {
338422 const mp3buf : any = mp3encoder . encodeBuffer ( leftChunk , rightChunk ) ;
339423 if ( mp3buf . length > 0 ) mp3Data . push ( mp3buf ) ;
340424 }
425+
341426 const mp3buf : any = mp3encoder . flush ( ) ;
342427 if ( mp3buf . length > 0 ) mp3Data . push ( mp3buf ) ;
343428
0 commit comments