scribbletune
Version:
Create music with JavaScript and Node.js!
599 lines (591 loc) • 17.8 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
arp: () => arp,
chord: () => import_harmonics4.chord,
chords: () => import_harmonics4.chords,
clip: () => clip,
getChordDegrees: () => getChordDegrees,
getChordsByProgression: () => getChordsByProgression,
midi: () => midi,
mode: () => import_harmonics4.scale,
modes: () => import_harmonics4.scales,
progression: () => progression,
scale: () => import_harmonics4.scale,
scales: () => import_harmonics4.scales,
sizzleMap: () => sizzleMap
});
module.exports = __toCommonJS(src_exports);
var import_harmonics4 = require("harmonics");
// src/arp.ts
var import_harmonics2 = require("harmonics");
// src/utils.ts
var import_harmonics = require("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 = (0, import_harmonics.inlineChord)(el);
} catch (e) {
e1 = e;
}
try {
c2 = (0, import_harmonics.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 (0, import_harmonics.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((0, import_harmonics2.inlineChord)(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
var import_node_fs = __toESM(require("fs"), 1);
var import_midi = require("@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);
}
import_node_fs.default.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 import_midi.File();
const track = new import_midi.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
var import_harmonics3 = require("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 = (0, import_harmonics3.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 [];
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
arp,
chord,
chords,
clip,
getChordDegrees,
getChordsByProgression,
midi,
mode,
modes,
progression,
scale,
scales,
sizzleMap
});
//# sourceMappingURL=index.cjs.map