Midi Inversion

node v18.11.0
version: 5.0.0
endpointsharetweet
const download = require('download'); // File downloads const JZZ = require('jzz'); // Midi tools const rkmidi = require('runkit-midi'); // Midi player const { Note, Scale, Interval, Midi } = require("tonal"); // Scales require('jzz-midi-smf')(JZZ); // Standard midi file parser console.log('Loaded');
// Flip/invert a note about the tonic by the distance between the two (semitones!) // For example ('C', 'E') => 'G#' function flipNote(note, tonic) { const interval = Interval.distance(note, tonic); return Note.transpose(tonic, interval); } // Same as flipNote, but accepts a midi note number as input function flipMidiNote(inputNote, tonic) { return (tonic * 2) - inputNote; }
// Find scales that result in notes that land on another scale when inverted // Eg. Major => Phrygian // Minor => Mixolydian function findMatchingScales() { const matches = []; // Loop though scales Scale.names().forEach(name => { const scale = Scale.get(`C ${name}`).notes; // Flip notes in the scale const notesFlipped = scale.map(note => flipNote(note, 'C')); // Check if the new notes match a known scale const match = Scale.detect(notesFlipped, { match: 'exact' }); if (match.length > 0) { matches.push({ name, match: match[0] }); } }); return matches; } findMatchingScales();
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; }
smf = await loadSong('clocks'); showMidiPlayer(smf);
smf = flipSong(smf, 'clocks'); showMidiPlayer(smf);
// Convert to chiptune showChipTunePlayer(smf);
smf = await loadSong('paperback-writer'); showMidiPlayer(smf);
smf = flipSong(smf, 'paperback-writer'); showChipTunePlayer(smf);
Loading…

no comments

    sign in to comment