let smf = null;
// List of songs
const songs = {
'clocks': {
url: 'https://drive.google.com/uc?export=download&id=11oGjNtR1iRzviAh87QvvwZFxqedF4NzX', // Midi file
tracks: [0, 1, 2, 3, 10, 11], // The tracks we want to keep
percussion: [11], // We do not want to invert the percussion track
scale: 'Mixolydian', // Mixolydian maps to the Minor scale when inverted
key: 'Eb'
},
'paperback-writer': {
url: 'https://drive.google.com/uc?export=download&id=1XmO8omUcx59HoP4pdoqfcaVtGGewgPmH',
tracks: [0, 3, 4, 10, 11, 12],
percussion: [3],
scale: 'Mixolydian',
key: 'G'
},
};
// Filter unwanted tracks from the midi
// smf only implements a subset of array functions
function filterTracks(smf, indicesToKeep) {
for (let i = smf.length - 1; i >= 0; i--) {
if (!indicesToKeep.includes(i)) {
smf.splice(i, 1);
}
}
return smf;
}
// Loop through the notes in the midi tracks
// and run a callback function
function applyToNotes(smf, callback) {
const notes = [];
for (var i = 0; i < smf.length; i++) {
if (smf[i] instanceof JZZ.MIDI.SMF.MTrk) {
for (var j = 0; j < smf[i].length; j++) {
var note = smf[i][j].getNote();
if (typeof note != 'undefined') {
callback(i, j, note);
}
}
}
}
}
// Get track notes by index
function getTrackNotes(smf, trackIndex) {
const result = [];
applyToNotes(smf, (i, j, note) => {
if (i === trackIndex) {
result.push(Note.fromMidi(note));
}
});
return result;
}
// Guess the tonic/root note from a list of notes in a track
function guessTonic(notes, key) {
return notes.find(n => Note.get(n).pc === key);
}
// Display a midi player
function showMidiPlayer(smf) {
return rkmidi(smf.dump());
}
// Pass the midi from Node to the frontend as base64
// And load a chiptune player script via CDN
function showChipTunePlayer(smf) {
const u8 = smf.toUint8Array();
const b64 = Buffer.from(u8).toString('base64');
return `
<script src="https://unpkg.com/picoaudio/dist/browser/PicoAudio.js"></script>
<script>
function base64ToArrayBuffer(base64) {
var binaryString = atob(base64);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
const b64="${b64}";
const u8 = new Uint8Array(base64ToArrayBuffer(b64))
const picoAudio = new PicoAudio();
const data = picoAudio.parseSMF(u8);
picoAudio.setData(data);
picoAudio.init();
</script>
<input type="button" value="play" onclick="picoAudio.play()" />
<input type="button" value="stop" onclick="picoAudio.stop()" />
`
}
async function loadSong(name) {
const song = songs[name];
// Fetch the midi file
let midiBuffer = await download(song.url);
// Parse midi
smf = new JZZ.MIDI.SMF(midiBuffer);
// Filter out unwanted tracks
filterTracks(smf, song.tracks);
// Show 10 sample notes
let trackNotes = getTrackNotes(smf, 2);
trackNotes.length = 10;
console.log(`${name} Original`);
console.log(trackNotes);
return smf;
}
function flipSong(smf, name) {
const song = songs[name];
// Loop through the tracks and find the tonic/root notes
// which we will store in an array
song.tonics = song.tracks.map((t, i) => {
const notes = getTrackNotes(smf, i);
const tonic = guessTonic(notes, song.key)
return Note.get(tonic).midi;
});
// Loop through the notes in each track and invert them;
applyToNotes(smf, (i, j, note) => {
if (!song.percussion.includes(song.tracks[i]) && song.tonics[i]) {
const flippedNote = flipMidiNote(note, song.tonics[i]);
smf[i][j].setNote(flippedNote);
}
});
// Show 10 sample notes
trackNotes = getTrackNotes(smf, 2);
trackNotes.length = 10;
console.log(`${name} inverted`);
console.log(trackNotes);
return smf;
}