Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@

<ul id="saveddropdown" class="dropdown-content">
<li><a id="save-html"></a></li>
<li><a id="save-midi"></a></li>
<li><a id="save-svg"></a></li>
<li><a id="save-png"></a></li>
<li><a id="save-wav"></a></li>
Expand Down
178 changes: 171 additions & 7 deletions js/SaveInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class SaveInterface {
* @member {number}
*/
this.timeLastSaved = -100;

/**
* HTML template for saving projects.
* @member {string}
Expand Down Expand Up @@ -264,9 +264,173 @@ class SaveInterface {
}, 500);
}

/**
* Save MIDI file.
*
* This method generates required MIDI data.
*
* @param {SaveInterface} activity - The activity object to save.
* @returns {void}
* @memberof SaveInterface
* @method
* @instance
*/
saveMIDI(activity) {
// Suppress music and turtle output when generating
activity.logo.runningMIDI = true;
activity.logo.runLogoCommands();
document.body.style.cursor = "wait";
}

/**
* Perform actions after generating MIDI data.
*
* This method generates a MIDI file using _midiData.
*
* @returns {void}
* @memberof SaveInterface
* @method
* @instance
*/
afterSaveMIDI() {
const generateMidi = (data) => {
const normalizeNote = (note) => {
return note.replace("♯", "#").replace("♭", "b");
};
const MIDI_INSTRUMENTS = {
default: 0, // Acoustic Grand Piano
piano: 0,
violin: 40,
viola: 41,
cello: 42,
"double bass": 43,
bass: 32,
sitar: 104,
guitar: 24,
"acoustic guitar": 25,
"electric guitar": 27,
flute: 73,
clarinet: 71,
saxophone: 65,
tuba: 58,
trumpet: 56,
oboe: 68,
trombone: 57,
banjo: 105,
koto: 107,
dulcimer: 15,
bassoon: 70,
celeste: 8,
xylophone: 13,
"electronic synth": 81,
sine: 81, // Approximate with Lead 2 (Sawtooth)
square: 80,
sawtooth: 81,
triangle: 81, // Approximate with Lead 2 (Sawtooth)
vibraphone: 11
};

const DRUM_MIDI_MAP = {
"snare drum": 38,
"kick drum": 36,
"tom tom": 41,
"floor tom tom": 43,
"cup drum": 47, // Closest: Low-Mid Tom
"darbuka drum": 50, // Closest: High Tom
"japanese drum": 56, // Closest: Cowbell or Tambourine
"hi hat": 42,
"ride bell": 53,
"cow bell": 56,
"triangle bell": 81,
"finger cymbals": 69, // Closest: Open Hi-Hat
"chime": 82, // Closest: Shaker
"gong": 52, // Closest: Chinese Cymbal
"clang": 55, // Closest: Splash Cymbal
"crash": 49,
"clap": 39,
"slap": 40,
"raindrop": 88 // Custom mapping (not in GM), can use melodic notes
};

const midi = new Midi();
midi.header.ticksPerBeat = 480;

Object.entries(data).forEach(([blockIndex, notes]) => {

const mainTrack = midi.addTrack();
mainTrack.name = `Track ${parseInt(blockIndex) + 1}`;

let trackMap = new Map();
let globalTime = 0;

notes.forEach((noteData) => {
if (!noteData.note || noteData.note.length === 0) return;
const duration = ((1 / noteData.duration) * 60 * 4) / noteData.bpm;
const instrument = noteData.instrument || "default";

if (noteData.drum) {
const drum = noteData.drum || false;
if (!trackMap.has(drum)) {
const drumTrack = midi.addTrack();
drumTrack.name = `Track ${parseInt(blockIndex) + 1} - ${drum}`;
drumTrack.channel = 9; // Drums must be on Channel 10
trackMap.set(drum, drumTrack);
}

const drumTrack = trackMap.get(drum);

const midiNumber = DRUM_MIDI_MAP[drum] || 36; // default to Bass Drum
drumTrack.addNote({
midi: midiNumber,
time: globalTime,
duration: duration,
velocity: 0.9,
});

} else {
if (!trackMap.has(instrument)) {
const instrumentTrack = midi.addTrack();
instrumentTrack.name = `Track ${parseInt(blockIndex) + 1} - ${instrument}`;
instrumentTrack.instrument.number = MIDI_INSTRUMENTS[instrument] ?? MIDI_INSTRUMENTS["default"];
trackMap.set(instrument, instrumentTrack);
}

const instrumentTrack = trackMap.get(instrument);

noteData.note.forEach((pitch) => {

if (!pitch.includes("R")) {
instrumentTrack.addNote({
name: normalizeNote(pitch),
time: globalTime,
duration: duration,
velocity: 0.8
});
}
});
}
globalTime += duration;
});
globalTime = 0;
});

// Generate the MIDI file and trigger download.
const midiData = midi.toArray();
const blob = new Blob([midiData], { type: "audio/midi" });
const url = URL.createObjectURL(blob);
activity.save.download("midi", url, null);
};
const data = activity.logo._midiData;
setTimeout(() => {
generateMidi(data);
activity.logo._midiData = {};
document.body.style.cursor = "default";
},500);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a space after the comma

}

/**
* This method is to save SVG representation of an activity
*
*
* @param {SaveInterface} activity -The activity object to save
* @returns {void}
* @method
Expand Down Expand Up @@ -306,23 +470,23 @@ class SaveInterface {
* @returns {void}
* @method
* @instance
*/
*/
saveBlockArtwork(activity) {
const svg = "data:image/svg+xml;utf8," + activity.printBlockSVG();
activity.save.download("svg", svg, null);
}

/**
* This method is to save BlockArtwork and download the PNG representation of block artwork from the provided activity.
*
* @param {SaveInterface} activity - The activity object containing block artwork to save.
* @returns {void}
* @method
* @instance
*/
*/
saveBlockArtworkPNG(activity) {
activity.printBlockPNG().then((pngDataUrl) => {
activity.save.download("png", pngDataUrl, null);
activity.save.download("png", pngDataUrl, null);
})
}

Expand Down Expand Up @@ -587,7 +751,7 @@ class SaveInterface {
tmp.remove();
this.activity.textMsg(
_("The Lilypond code is copied to clipboard. You can paste it here: ") +
"<a href='http://hacklily.org' target='_blank'>http://hacklily.org</a> "
"<a href='http://hacklily.org' target='_blank'>http://hacklily.org</a> "
);
}
this.download("ly", "data:text;utf8," + encodeURIComponent(lydata), filename);
Expand Down
2 changes: 2 additions & 0 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,7 @@ class Activity {
activity.save.saveHTML.bind(activity.save),
doSVG,
activity.save.saveSVG.bind(activity.save),
activity.save.saveMIDI.bind(activity.save),
activity.save.savePNG.bind(activity.save),
activity.save.saveWAV.bind(activity.save),
activity.save.saveLilypond.bind(activity.save),
Expand Down Expand Up @@ -6921,6 +6922,7 @@ class Activity {
this.save.saveHTML.bind(this.save),
doSVG,
this.save.saveSVG.bind(this.save),
this.save.saveMIDI.bind(this.save),
this.save.savePNG.bind(this.save),
this.save.saveWAV.bind(this.save),
this.save.saveLilypond.bind(this.save),
Expand Down
3 changes: 2 additions & 1 deletion js/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ requirejs.config({
twewn: "../lib/tweenjs",
prefixfree: "../bower_components/prefixfree/prefixfree.min",
samples: "../sounds/samples",
planet: "../js/planet"
planet: "../js/planet",
tonejsMidi: "../node_modules/@tonejs/midi/dist/Midi"
},
packages: []
});
Expand Down
21 changes: 19 additions & 2 deletions js/logo.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class Logo {
this.collectingStats = false;
this.runningAbc = false;
this.runningMxml = false;
this.runningMIDI = false;
this._checkingCompletionState = false;
this.recording = false;

Expand All @@ -231,6 +232,9 @@ class Logo {
this.updatingStatusMatrix = false;
this.statusFields = [];

// Midi Data
this._midiData = {};

// When running in step-by-step mode, the next command to run
// is queued here.
this.stepQueue = {};
Expand Down Expand Up @@ -796,6 +800,15 @@ class Logo {
}
}

notationMIDI(note, drum, duration, turtle, bpm, instrument) {

if (!this._midiData[turtle]) {
this._midiData[turtle] = [];
}
if (drum) drum = drum[0];
this._midiData[turtle].push({ note, duration, bpm, instrument, drum });
}

// ========================================================================

/**
Expand Down Expand Up @@ -900,7 +913,7 @@ class Logo {

this.activity.turtles
.ithTurtle(turtle)
.initTurtle(this.runningLilypond || this.runningAbc || this.runningMxml);
.initTurtle(this.runningLilypond || this.runningAbc || this.runningMxml || this.runningMIDI);
}

/**
Expand Down Expand Up @@ -1690,7 +1703,11 @@ class Logo {
// console.log("saving mxml output");
logo.activity.save.afterSaveMxml();
logo.runningMxml = false;
} else if (tur.singer.suppressOutput) {
} else if (logo.runningMIDI) {
logo.activity.save.afterSaveMIDI(); //save midi
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this comment

logo.runningMIDI = false;
}
else if (tur.singer.suppressOutput) {
// console.debug("finishing compiling");
if (!logo.recording) {
logo.activity.errorMsg(_("Playback is ready."));
Expand Down
9 changes: 9 additions & 0 deletions js/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Toolbar {
["save-html-beg", _("Save project as HTML"), "innerHTML"],
["save-png-beg", _("Save mouse artwork as PNG"), "innerHTML"],
["save-html", _("Save project as HTML"), "innerHTML"],
["save-midi", _("Save project as MIDI"), "innerHTML"],
["save-svg", _("Save mouse artwork as SVG"), "innerHTML"],
["save-png", _("Save mouse artwork as PNG"), "innerHTML"],
["save-wav", _("Save music as WAV"), "innerHTML"],
Expand Down Expand Up @@ -143,6 +144,7 @@ class Toolbar {
_("Switch to advanced mode"),
_("Select language"),
_("Save project as HTML"),
_("Save project as MIDI"),
_("Save mouse artwork as SVG"),
_("Save mouse artwork as PNG"),
_("Save music as WAV"),
Expand Down Expand Up @@ -575,6 +577,7 @@ class Toolbar {
*
* @public
* @param {Function} html_onclick - The onclick handler for HTML.
* @param {Function} midi_onclick - The onclick handler for MIDI.
* @param {Function} doSVG_onclick - The onclick handler for SVG.
* @param {Function} svg_onclick - The onclick handler for SVG.
* @param {Function} png_onclick - The onclick handler for PNG.
Expand All @@ -589,6 +592,7 @@ class Toolbar {
html_onclick,
doSVG_onclick,
svg_onclick,
midi_onclick,
png_onclick,
wave_onclick,
ly_onclick,
Expand Down Expand Up @@ -683,6 +687,11 @@ class Toolbar {
}

if (_THIS_IS_MUSIC_BLOCKS_) {
const saveMIDI = docById("save-midi");
saveMIDI.onclick = () => {
midi_onclick(this.activity);
};

const saveWAV = docById('save-wav');
saveWAV.onclick = () => {
wave_onclick(this.activity);
Expand Down
5 changes: 4 additions & 1 deletion js/turtle-singer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ class Singer {
const tur = activity.turtles.ithTurtle(turtle);
const bpmFactor =
TONEBPM / (tur.singer.bpm.length > 0 ? last(tur.singer.bpm) : Singer.masterBPM);
let bpmValue = Number(last(tur.singer.bpm));

let noteBeatValue = isOsc
? noteValue === 0
Expand Down Expand Up @@ -1957,8 +1958,10 @@ class Singer {
if (
activity.logo.runningLilypond ||
activity.logo.runningMxml ||
activity.logo.runningAbc
activity.logo.runningAbc ||
activity.logo.runningMIDI
) {
activity.logo.notationMIDI(chordNotes, chordDrums, d, turtle, bpmValue || 90, last(tur.singer.instrumentNames));
activity.logo.updateNotation(chordNotes, d, turtle, -1, chordDrums);
}
}
Expand Down
Loading