UNPKG

scribbletune

Version:

Create music with JavaScript and Node.js!

552 lines (545 loc) 15.7 kB
// src/index.ts import { chord as chord2, chords, scale as scale2, scales } from "harmonics"; // src/arp.ts import { inlineChord as inlineChord2 } from "harmonics"; // src/utils.ts import { chord, inlineChord } from "harmonics"; var isNote = (str) => /^[a-gA-G](?:#|b)?\d$/.test(str); var expandStr = (str) => { str = JSON.stringify(str.split("")); str = str.replace(/,"\[",/g, ", ["); str = str.replace(/"\[",/g, "["); str = str.replace(/,"\]"/g, "]"); return JSON.parse(str); }; var shuffle = (arr, fullShuffle = true) => { const lastIndex = arr.length - 1; arr.forEach((el, idx) => { if (idx >= lastIndex) { return; } const rnd = fullShuffle ? ( // Pick random number from idx+1 to lastIndex (Modified algorithm, (N-1)! combinations) // Math.random -> [0, 1) -> [0, lastIndex-idx ) --floor-> [0, lastIndex-idx-1] // rnd = [0, lastIndex-idx-1] + 1 + idx = [1 + idx, lastIndex] // (Original algorithm would pick rnd = [idx, lastIndex], thus any element could arrive back into its slot) Math.floor(Math.random() * (lastIndex - idx)) + 1 + idx ) : ( // Pick random number from idx to lastIndex (Unmodified Richard Durstenfeld, N! combinations) Math.floor(Math.random() * (lastIndex + 1 - idx)) + idx ); arr[idx] = arr[rnd]; arr[rnd] = el; }); return arr; }; var sizzleMap = (maxLevel = 127) => { const pi = Math.PI; const piArr = [ pi / 6, pi / 4, pi / 3, pi / 2, 2 * pi / 3, 3 * pi / 4, 5 * pi / 6, pi ]; const piArrRev = [ 0, pi / 6, pi / 4, pi / 3, pi / 2, 2 * pi / 3, 3 * pi / 4, 5 * pi / 6 ]; piArrRev.reverse(); const arr = piArr.concat(piArrRev); return arr.map((element) => Math.round(Math.sin(element) * maxLevel)); }; var pickOne = (arr) => arr[Math.floor(Math.random() * arr.length)]; var dice = () => !!Math.round(Math.random()); var errorHasMessage = (x) => { return typeof x === "object" && x !== null && "message" in x && typeof x.message === "string"; }; var convertChordToNotes = (el) => { let c1; let c2; let e1; let e2; try { c1 = inlineChord(el); } catch (e) { e1 = e; } try { c2 = chord(el.replace(/_/g, " ")); } catch (e) { e2 = e; } if (!e1 && !e2 && c1 && c2) { if (c1.toString() !== c2.toString()) { throw new Error(`Chord ${el} cannot decode, guessing ${c1} or ${c2}`); } return c1; } if (!e1 && c1) { return c1; } if (!e2 && c2) { return c2; } return chord(el); }; var convertChordsToNotes = (el) => { if (typeof el === "string" && isNote(el)) { return [el]; } if (Array.isArray(el)) { el.forEach((n) => { if (Array.isArray(n)) { n.forEach((n1) => { if (typeof n1 !== "string" || !isNote(n1)) { throw new TypeError("array of arrays must comprise valid notes"); } }); } else if (typeof n !== "string" || !isNote(n)) { throw new TypeError("array must comprise valid notes"); } }); return el; } if (!Array.isArray(el)) { const c = convertChordToNotes(el); if (c?.length) { return c; } } throw new Error(`Chord ${el} not found`); }; var randomInt = (num = 1) => Math.round(Math.random() * num); // src/arp.ts var DEFAULT_OCTAVE = 4; var fillArr = (arr, len) => { const bumpOctave = (el) => { if (!el) { throw new Error("Empty element"); } const note = el.replace(/\d/, ""); const oct = el.replace(/\D/g, "") || DEFAULT_OCTAVE; if (!note) { throw new Error("Incorrect note"); } return note + (+oct + 1); }; const arr1 = arr.map(bumpOctave); const arr2 = arr1.map(bumpOctave); const finalArr = [...arr, ...arr1, ...arr2]; return finalArr.slice(0, len); }; var arp = (chordsOrParams) => { let finalArr = []; const params = { count: 4, order: "0123", chords: "" }; if (typeof chordsOrParams === "string") { params.chords = chordsOrParams; } else { if (chordsOrParams.order?.match(/\D/g)) { throw new TypeError("Invalid value for order"); } if (chordsOrParams.count > 8 || chordsOrParams.count < 2) { throw new TypeError("Invalid value for count"); } if (chordsOrParams.count && !chordsOrParams.order) { params.order = Array.from(Array(chordsOrParams.count).keys()).join(""); } Object.assign(params, chordsOrParams); } if (typeof params.chords === "string") { const chordsArr = params.chords.split(" "); chordsArr.forEach((c, i) => { try { const filledArr = fillArr(inlineChord2(c), params.count); const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]); finalArr = [...finalArr, ...reorderedArr]; } catch (_e) { throw new Error( `Cannot decode chord ${i + 1} "${c}" in given "${params.chords}"` ); } }); } else if (Array.isArray(params.chords)) { params.chords.forEach((c, i) => { try { const filledArr = fillArr(c, params.count); const reorderedArr = params.order.split("").map((idx) => filledArr[Number(idx)]); finalArr = [...finalArr, ...reorderedArr]; } catch (e) { throw new Error( `${errorHasMessage(e) ? e.message : e} in chord ${i + 1} "${c}"` ); } }); } else { throw new TypeError("Invalid value for chords"); } return finalArr; }; // src/clip-utils.ts var defaultParams = { notes: ["C4"], pattern: "x", shuffle: false, sizzle: false, sizzleReps: 1, arpegiate: false, subdiv: "4n", amp: 100, accentLow: 70, randomNotes: null, offlineRendering: false }; var validatePattern = (pattern) => { if (/[^x\-_[\]R]/.test(pattern)) { throw new TypeError( `pattern can only comprise x - _ [ ] R, found ${pattern}` ); } }; var preprocessClipParams = (params, extraDefaults) => { params = { ...defaultParams, ...extraDefaults, ...params || {} }; if (typeof params.notes === "string") { params.notes = params.notes.replace(/\s{2,}/g, " ").split(" "); } params.notes = params.notes ? params.notes.map(convertChordsToNotes) : []; validatePattern(params.pattern); if (params.shuffle) { params.notes = shuffle(params.notes); } if (params.randomNotes && typeof params.randomNotes === "string") { params.randomNotes = params.randomNotes.replace(/\s{2,}/g, " ").split(/\s/); } if (params.randomNotes) { params.randomNotes = params.randomNotes.map( convertChordsToNotes ); } return params; }; // src/clip.ts var hdr = { "1m": 2048, "2m": 4096, "3m": 6144, "4m": 8192, "1n": 512, "2n": 256, "4n": 128, "8n": 64, "16n": 32 }; var clip = (params) => { params = preprocessClipParams(params); const clipNotes = []; let step = 0; const recursivelyApplyPatternToNotes = (patternArr, length, parentNoteLength) => { let totalLength = 0; patternArr.forEach((char, idx) => { if (typeof char === "string") { let note = null; if (char === "-") { } else if (char === "R" && randomInt() && // Use 1/2 probability for R to pick from param.notes params.randomNotes && params.randomNotes.length > 0) { note = params.randomNotes[randomInt(params.randomNotes.length - 1)]; } else if (params.notes) { note = params.notes[step]; } if (char === "x" || char === "R") { step++; } if (char === "x" || char === "-" || char === "R") { clipNotes.push({ note, length, level: char === "R" && !params.randomNotes ? params.accentLow : params.amp }); totalLength += length; } if (char === "_" && clipNotes.length) { clipNotes[clipNotes.length - 1].length += length; totalLength += length; } if (parentNoteLength && totalLength !== parentNoteLength && idx === patternArr.length - 1) { const diff = Math.abs( parentNoteLength - totalLength ); const lastClipNote = clipNotes[clipNotes.length - 1]; if (lastClipNote.length > diff) { lastClipNote.length = lastClipNote.length - diff; } else { lastClipNote.length = lastClipNote.length + diff; } } if (step === params.notes?.length) { step = 0; } } if (Array.isArray(char)) { let isTriplet = false; if (char.length % 2 !== 0 || length % 2 !== 0) { isTriplet = true; } recursivelyApplyPatternToNotes( char, Math.round(length / char.length), isTriplet && length ); totalLength += length; } }); }; recursivelyApplyPatternToNotes( expandStr(params.pattern), hdr[params.subdiv] || hdr["4n"], false ); if (params.sizzle) { const volArr = []; const style = params.sizzle === true ? "sin" : params.sizzle; const beats = clipNotes.length; const amp = params.amp; const sizzleReps = params.sizzleReps; const stepLevel = amp / (beats / sizzleReps); if (style === "sin" || style === "cos") { for (let i = 0; i < beats; i++) { const level = Math[style](i * Math.PI / (beats / sizzleReps)) * amp; volArr.push(Math.round(Math.abs(level))); } } if (style === "rampUp") { let level = 0; for (let i = 0; i < beats; i++) { if (i % (beats / sizzleReps) === 0) { level = 0; } else { level = level + stepLevel; } volArr.push(Math.round(Math.abs(level))); } } if (style === "rampDown") { let level = amp; for (let i = 0; i < beats; i++) { if (i % (beats / sizzleReps) === 0) { level = amp; } else { level = level - stepLevel; } volArr.push(Math.round(Math.abs(level))); } } for (let i = 0; i < volArr.length; i++) { clipNotes[i].level = volArr[i] ? volArr[i] : 1; } } if (params.accent) { if (/[^x-]/.test(params.accent)) { throw new TypeError("Accent can only have x and - characters"); } let a = 0; for (const clipNote of clipNotes) { let level = params.accent[a] === "x" ? params.amp : params.accentLow; if (params.sizzle) { level = (clipNote.level + level) / 2; } clipNote.level = Math.round(level); a = a + 1; if (a === params.accent.length) { a = 0; } } } return clipNotes; }; // src/midi.ts import fs from "fs"; import { File, Track } from "@scribbletune/midi"; var midi = (notes, fileName = "music.mid", bpm) => { const file = createFileFromNotes(notes, bpm); const bytes = file.toBytes(); if (fileName === null) { return bytes; } if (!fileName.endsWith(".mid")) { fileName = `${fileName}.mid`; } if (typeof window !== "undefined" && window.URL && typeof window.URL.createObjectURL === "function") { return createDownloadLink(bytes, fileName); } fs.writeFileSync(fileName, bytes, "binary"); console.log(`MIDI file generated: ${fileName}.`); }; var createDownloadLink = (b, fileName) => { const bytes = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) { const ascii = b.charCodeAt(i); bytes[i] = ascii; } const blob = new Blob([bytes], { type: "audio/midi" }); const link = document.createElement("a"); link.href = typeof window !== "undefined" && typeof window.URL !== "undefined" && typeof window.URL.createObjectURL !== "undefined" && window.URL.createObjectURL(blob) || ""; link.download = fileName; link.innerText = "Download MIDI file"; return link; }; var createFileFromNotes = (notes, bpm) => { const file = new File(); const track = new Track(); if (typeof bpm === "number") { track.setTempo(bpm); } file.addTrack(track); for (const noteObj of notes) { const level = noteObj.level || 127; if (noteObj.note) { if (typeof noteObj.note === "string") { track.noteOn(0, noteObj.note, noteObj.length, level); track.noteOff(0, noteObj.note, noteObj.length, level); } else { track.addChord(0, noteObj.note, noteObj.length, level); } } else { track.noteOff(0, "", noteObj.length); } } return file; }; // src/progression.ts import { scale } from "harmonics"; var getChordDegrees = (mode) => { const theRomans = { ionian: ["I", "ii", "iii", "IV", "V", "vi", "vii\xB0"], dorian: ["i", "ii", "III", "IV", "v", "vi\xB0", "VII"], phrygian: ["i", "II", "III", "iv", "v\xB0", "VI", "vii"], lydian: ["I", "II", "iii", "iv\xB0", "V", "vi", "vii"], mixolydian: ["I", "ii", "iii\xB0", "IV", "v", "vi", "VII"], aeolian: ["i", "ii\xB0", "III", "iv", "v", "VI", "VII"], locrian: ["i\xB0", "II", "iii", "iv", "V", "VI", "vii"], "melodic minor": ["i", "ii", "III+", "IV", "V", "vi\xB0", "vii\xB0"], "harmonic minor": ["i", "ii\xB0", "III+", "iv", "V", "VI", "vii\xB0"] }; theRomans.major = theRomans.ionian; theRomans.minor = theRomans.aeolian; return theRomans[mode] || []; }; var idxByDegree = { i: 0, ii: 1, iii: 2, iv: 3, v: 4, vi: 5, vii: 6 }; var getChordName = (roman) => { const str = roman.replace(/\W/g, ""); let prefix = "M"; if (str.toLowerCase() === str) { prefix = "m"; } if (roman.indexOf("\xB0") > -1) { return `${prefix}7b5`; } if (roman.indexOf("+") > -1) { return `${prefix}#5`; } if (roman.indexOf("7") > -1) { return prefix === "M" ? "maj7" : "m7"; } return prefix; }; var getChordsByProgression = (noteOctaveScale, chordDegress) => { const noteOctaveScaleArr = noteOctaveScale.split(" "); if (!noteOctaveScaleArr[0].match(/\d/)) { noteOctaveScaleArr[0] += "4"; noteOctaveScale = noteOctaveScaleArr.join(" "); } const mode = scale(noteOctaveScale); const chordDegreesArr = chordDegress.replace(/\s*,+\s*/g, " ").split(" "); const chordFamily = chordDegreesArr.map((roman) => { const chordName = getChordName(roman); const scaleId = idxByDegree[roman.replace(/\W|\d/g, "").toLowerCase()]; const note = mode[scaleId]; const oct = note.replace(/\D+/, ""); return `${note.replace(/\d/, "") + chordName}_${oct}`; }); return chordFamily.toString().replace(/,/g, " "); }; var getProgFactory = ({ T, P, D }) => { return (count = 4) => { const chords2 = []; chords2.push(pickOne(T)); let i = 1; if (i < count - 1) { chords2.push(pickOne(P)); i++; } if (i < count - 1 && dice()) { chords2.push(pickOne(P)); i++; } if (i < count - 1) { chords2.push(pickOne(D)); i++; } if (i < count - 1) { chords2.push(pickOne(P)); i++; } if (i < count - 1) { chords2.push(pickOne(D)); i++; } if (i < count - 1 && dice()) { chords2.push(pickOne(P)); i++; } while (i < count) { chords2.push(pickOne(D)); i++; } return chords2; }; }; var M = getProgFactory({ T: ["I", "vi"], P: ["ii", "IV"], D: ["V"] }); var m = getProgFactory({ T: ["i", "VI"], P: ["ii", "iv"], D: ["V"] }); var progression = (scaleType, count = 4) => { if (scaleType === "major" || scaleType === "M") { return M(count); } if (scaleType === "minor" || scaleType === "m") { return m(count); } return []; }; export { arp, chord2 as chord, chords, clip, getChordDegrees, getChordsByProgression, midi, scale2 as mode, scales as modes, progression, scale2 as scale, scales, sizzleMap }; //# sourceMappingURL=index.js.map