@@ -48,7 +48,7 @@ class SaveInterface {
48
48
* @member {number}
49
49
*/
50
50
this . timeLastSaved = - 100 ;
51
-
51
+
52
52
/**
53
53
* HTML template for saving projects.
54
54
* @member {string}
@@ -264,9 +264,173 @@ class SaveInterface {
264
264
} , 500 ) ;
265
265
}
266
266
267
+ /**
268
+ * Save MIDI file.
269
+ *
270
+ * This method generates required MIDI data.
271
+ *
272
+ * @param {SaveInterface } activity - The activity object to save.
273
+ * @returns {void }
274
+ * @memberof SaveInterface
275
+ * @method
276
+ * @instance
277
+ */
278
+ saveMIDI ( activity ) {
279
+ // Suppress music and turtle output when generating
280
+ activity . logo . runningMIDI = true ;
281
+ activity . logo . runLogoCommands ( ) ;
282
+ document . body . style . cursor = "wait" ;
283
+ }
284
+
285
+ /**
286
+ * Perform actions after generating MIDI data.
287
+ *
288
+ * This method generates a MIDI file using _midiData.
289
+ *
290
+ * @returns {void }
291
+ * @memberof SaveInterface
292
+ * @method
293
+ * @instance
294
+ */
295
+ afterSaveMIDI ( ) {
296
+ const generateMidi = ( data ) => {
297
+ const normalizeNote = ( note ) => {
298
+ return note . replace ( "♯" , "#" ) . replace ( "♭" , "b" ) ;
299
+ } ;
300
+ const MIDI_INSTRUMENTS = {
301
+ default : 0 , // Acoustic Grand Piano
302
+ piano : 0 ,
303
+ violin : 40 ,
304
+ viola : 41 ,
305
+ cello : 42 ,
306
+ "double bass" : 43 ,
307
+ bass : 32 ,
308
+ sitar : 104 ,
309
+ guitar : 24 ,
310
+ "acoustic guitar" : 25 ,
311
+ "electric guitar" : 27 ,
312
+ flute : 73 ,
313
+ clarinet : 71 ,
314
+ saxophone : 65 ,
315
+ tuba : 58 ,
316
+ trumpet : 56 ,
317
+ oboe : 68 ,
318
+ trombone : 57 ,
319
+ banjo : 105 ,
320
+ koto : 107 ,
321
+ dulcimer : 15 ,
322
+ bassoon : 70 ,
323
+ celeste : 8 ,
324
+ xylophone : 13 ,
325
+ "electronic synth" : 81 ,
326
+ sine : 81 , // Approximate with Lead 2 (Sawtooth)
327
+ square : 80 ,
328
+ sawtooth : 81 ,
329
+ triangle : 81 , // Approximate with Lead 2 (Sawtooth)
330
+ vibraphone : 11
331
+ } ;
332
+
333
+ const DRUM_MIDI_MAP = {
334
+ "snare drum" : 38 ,
335
+ "kick drum" : 36 ,
336
+ "tom tom" : 41 ,
337
+ "floor tom tom" : 43 ,
338
+ "cup drum" : 47 , // Closest: Low-Mid Tom
339
+ "darbuka drum" : 50 , // Closest: High Tom
340
+ "japanese drum" : 56 , // Closest: Cowbell or Tambourine
341
+ "hi hat" : 42 ,
342
+ "ride bell" : 53 ,
343
+ "cow bell" : 56 ,
344
+ "triangle bell" : 81 ,
345
+ "finger cymbals" : 69 , // Closest: Open Hi-Hat
346
+ "chime" : 82 , // Closest: Shaker
347
+ "gong" : 52 , // Closest: Chinese Cymbal
348
+ "clang" : 55 , // Closest: Splash Cymbal
349
+ "crash" : 49 ,
350
+ "clap" : 39 ,
351
+ "slap" : 40 ,
352
+ "raindrop" : 88 // Custom mapping (not in GM), can use melodic notes
353
+ } ;
354
+
355
+ const midi = new Midi ( ) ;
356
+ midi . header . ticksPerBeat = 480 ;
357
+
358
+ Object . entries ( data ) . forEach ( ( [ blockIndex , notes ] ) => {
359
+
360
+ const mainTrack = midi . addTrack ( ) ;
361
+ mainTrack . name = `Track ${ parseInt ( blockIndex ) + 1 } ` ;
362
+
363
+ let trackMap = new Map ( ) ;
364
+ let globalTime = 0 ;
365
+
366
+ notes . forEach ( ( noteData ) => {
367
+ if ( ! noteData . note || noteData . note . length === 0 ) return ;
368
+ const duration = ( ( 1 / noteData . duration ) * 60 * 4 ) / noteData . bpm ;
369
+ const instrument = noteData . instrument || "default" ;
370
+
371
+ if ( noteData . drum ) {
372
+ const drum = noteData . drum || false ;
373
+ if ( ! trackMap . has ( drum ) ) {
374
+ const drumTrack = midi . addTrack ( ) ;
375
+ drumTrack . name = `Track ${ parseInt ( blockIndex ) + 1 } - ${ drum } ` ;
376
+ drumTrack . channel = 9 ; // Drums must be on Channel 10
377
+ trackMap . set ( drum , drumTrack ) ;
378
+ }
379
+
380
+ const drumTrack = trackMap . get ( drum ) ;
381
+
382
+ const midiNumber = DRUM_MIDI_MAP [ drum ] || 36 ; // default to Bass Drum
383
+ drumTrack . addNote ( {
384
+ midi : midiNumber ,
385
+ time : globalTime ,
386
+ duration : duration ,
387
+ velocity : 0.9 ,
388
+ } ) ;
389
+
390
+ } else {
391
+ if ( ! trackMap . has ( instrument ) ) {
392
+ const instrumentTrack = midi . addTrack ( ) ;
393
+ instrumentTrack . name = `Track ${ parseInt ( blockIndex ) + 1 } - ${ instrument } ` ;
394
+ instrumentTrack . instrument . number = MIDI_INSTRUMENTS [ instrument ] ?? MIDI_INSTRUMENTS [ "default" ] ;
395
+ trackMap . set ( instrument , instrumentTrack ) ;
396
+ }
397
+
398
+ const instrumentTrack = trackMap . get ( instrument ) ;
399
+
400
+ noteData . note . forEach ( ( pitch ) => {
401
+
402
+ if ( ! pitch . includes ( "R" ) ) {
403
+ instrumentTrack . addNote ( {
404
+ name : normalizeNote ( pitch ) ,
405
+ time : globalTime ,
406
+ duration : duration ,
407
+ velocity : 0.8
408
+ } ) ;
409
+ }
410
+ } ) ;
411
+ }
412
+ globalTime += duration ;
413
+ } ) ;
414
+ globalTime = 0 ;
415
+ } ) ;
416
+
417
+ // Generate the MIDI file and trigger download.
418
+ const midiData = midi . toArray ( ) ;
419
+ const blob = new Blob ( [ midiData ] , { type : "audio/midi" } ) ;
420
+ const url = URL . createObjectURL ( blob ) ;
421
+ activity . save . download ( "midi" , url , null ) ;
422
+ } ;
423
+ const data = activity . logo . _midiData ;
424
+ setTimeout ( ( ) => {
425
+ generateMidi ( data ) ;
426
+ activity . logo . _midiData = { } ;
427
+ document . body . style . cursor = "default" ;
428
+ } , 500 ) ;
429
+ }
430
+
267
431
/**
268
432
* This method is to save SVG representation of an activity
269
- *
433
+ *
270
434
* @param {SaveInterface } activity -The activity object to save
271
435
* @returns {void }
272
436
* @method
@@ -306,23 +470,23 @@ class SaveInterface {
306
470
* @returns {void }
307
471
* @method
308
472
* @instance
309
- */
473
+ */
310
474
saveBlockArtwork ( activity ) {
311
475
const svg = "data:image/svg+xml;utf8," + activity . printBlockSVG ( ) ;
312
476
activity . save . download ( "svg" , svg , null ) ;
313
477
}
314
-
478
+
315
479
/**
316
480
* This method is to save BlockArtwork and download the PNG representation of block artwork from the provided activity.
317
481
*
318
482
* @param {SaveInterface } activity - The activity object containing block artwork to save.
319
483
* @returns {void }
320
484
* @method
321
485
* @instance
322
- */
486
+ */
323
487
saveBlockArtworkPNG ( activity ) {
324
488
activity . printBlockPNG ( ) . then ( ( pngDataUrl ) => {
325
- activity . save . download ( "png" , pngDataUrl , null ) ;
489
+ activity . save . download ( "png" , pngDataUrl , null ) ;
326
490
} )
327
491
}
328
492
@@ -587,7 +751,7 @@ class SaveInterface {
587
751
tmp . remove ( ) ;
588
752
this . activity . textMsg (
589
753
_ ( "The Lilypond code is copied to clipboard. You can paste it here: " ) +
590
- "<a href='http://hacklily.org' target='_blank'>http://hacklily.org</a> "
754
+ "<a href='http://hacklily.org' target='_blank'>http://hacklily.org</a> "
591
755
) ;
592
756
}
593
757
this . download ( "ly" , "data:text;utf8," + encodeURIComponent ( lydata ) , filename ) ;
0 commit comments