scribbletune
Version:
Create music with JavaScript and Node.js!
1,185 lines (1,172 loc) • 36.9 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/browser-index.ts
var browser_index_exports = {};
__export(browser_index_exports, {
Session: () => Session,
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
});
module.exports = __toCommonJS(browser_index_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 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/channel/instrument-factory.ts
function checkToneObjLoaded(toneObject, resolve) {
const skipRecursion = toneObject instanceof Tone.Sampler;
if ("loaded" in toneObject) {
if (toneObject.loaded) {
resolve();
return;
}
if (skipRecursion) {
return;
}
let handled = false;
["buffer", "_buffer", "_buffers"].forEach((key) => {
if (key in toneObject) {
checkToneObjLoaded(toneObject[key], resolve);
handled = true;
}
});
if (handled) {
return;
}
}
const hasOnload = toneObject instanceof Tone.ToneAudioBuffer || toneObject instanceof Tone.ToneBufferSource || // Falback for "future" objects
"loaded" in toneObject && "onload" in toneObject;
if (!hasOnload) {
resolve();
} else {
const oldOnLoad = toneObject.onload;
toneObject.onload = () => {
if (oldOnLoad && typeof oldOnLoad === "function") {
toneObject.onload = oldOnLoad;
oldOnLoad();
}
resolve();
};
}
}
function recreateToneObjectInContext(toneObject, context) {
context = context || Tone.getContext();
return new Promise((resolve, _reject) => {
if (toneObject instanceof Tone.PolySynth) {
const newObj = new Tone[toneObject._dummyVoice.name]({
...toneObject.get(),
context
});
checkToneObjLoaded(newObj, () => resolve(newObj));
} else if (toneObject instanceof Tone.Player) {
const newObj = new Tone.Player({
url: toneObject._buffer,
context,
onload: () => checkToneObjLoaded(newObj, () => resolve(newObj))
});
} else if (toneObject instanceof Tone.Sampler) {
const { attack, curve, release, volume } = toneObject.get();
const paramsFromSampler = {
attack,
curve,
release,
volume
};
const paramsFromBuffers = {
baseUrl: toneObject._buffers.baseUrl,
urls: Object.fromEntries(toneObject._buffers._buffers.entries())
};
const newObj = new Tone.Sampler({
...paramsFromSampler,
...paramsFromBuffers,
context,
onload: () => checkToneObjLoaded(
newObj,
() => resolve(newObj)
)
});
} else {
const newObj = new Tone[toneObject.name]({
...toneObject.get(),
context,
onload: () => checkToneObjLoaded(newObj, () => resolve(newObj))
});
checkToneObjLoaded(newObj, () => resolve(newObj));
}
});
}
function createInstrument(context, params, channelMeta) {
let instrument;
let external;
context = context || Tone.getContext();
const loadPromise = new Promise((resolve, reject) => {
if (params.synth) {
if (params.instrument) {
throw new Error(
"Either synth or instrument can be provided, but not both."
);
}
if (params.synth.synth) {
const synthName = params.synth.synth;
const preset = params.synth.preset || {};
instrument = new Tone[synthName]({
...preset,
context,
// Use onload for cases when synthName calls out Tone.Sample/Player/Sampler.
// It could be a universal way to load Tone.js instruments.
onload: () => checkToneObjLoaded(instrument, resolve)
// This onload is ignored in all synths. Therefore we call checkToneObjLoaded() again below.
// It is safe to call resolve() multiple times for Promise<void>
});
checkToneObjLoaded(instrument, resolve);
} else {
instrument = params.synth;
console.warn(
'The "synth" parameter with instrument will be deprecated in the future. Please use the "instrument" parameter instead.'
);
checkToneObjLoaded(instrument, resolve);
}
} else if (typeof params.instrument === "string") {
instrument = new Tone[params.instrument]({ context });
checkToneObjLoaded(instrument, resolve);
} else if (params.instrument) {
instrument = params.instrument;
checkToneObjLoaded(instrument, resolve);
} else if (params.sample || params.buffer) {
instrument = new Tone.Player({
url: params.sample || params.buffer,
context,
onload: () => checkToneObjLoaded(instrument, resolve)
});
} else if (params.samples) {
instrument = new Tone.Sampler({
urls: params.samples,
context,
onload: () => checkToneObjLoaded(instrument, resolve)
});
} else if (params.sampler) {
instrument = params.sampler;
checkToneObjLoaded(instrument, resolve);
} else if (params.player) {
instrument = params.player;
checkToneObjLoaded(instrument, resolve);
} else if (params.external) {
external = { ...params.external };
instrument = {
context,
volume: { value: 0 }
};
if (params.external.init) {
return params.external.init(context.rawContext).then(() => {
resolve();
}).catch((e) => {
reject(
new Error(
`${errorHasMessage(e) ? e.message : e} loading external output module of channel idx ${channelMeta.idx}, ${channelMeta.name ?? "(no name)"}`
)
);
});
} else {
resolve();
}
} else {
throw new Error(
"One of required synth|instrument|sample|sampler|samples|buffer|player|external is not provided!"
);
}
if (!instrument) {
throw new Error("Failed instantiating instrument from given params.");
}
});
const initPromise = loadPromise.then(() => {
if (!external && instrument?.context !== context) {
return recreateToneObjectInContext(instrument, context).then((newObj) => {
instrument = newObj;
});
}
}).then(() => {
if (params.volume) {
instrument.volume.value = params.volume;
external?.setVolume?.(params.volume);
}
return instrument;
});
return { instrument, external, initPromise };
}
// src/channel/sequence-builder.ts
function buildSequenceCallback(params, host, playerCb, eventCb) {
if (host.external) {
const ext = host.external;
return (time, el) => {
if (el === "x" || el === "R") {
const counter = host.clipNoteCount;
if (host.hasLoaded) {
const note = getNote(el, params, counter)[0];
const duration = getDuration(params, counter);
const durSeconds = Tone.Time(duration).toSeconds();
playerCb({ note, duration, time, counter });
try {
ext.triggerAttackRelease?.(note, durSeconds, time);
} catch (e) {
eventCb("error", { e });
}
}
host.clipNoteCount++;
}
};
}
if (host.instrument instanceof Tone.Player) {
return (time, el) => {
if (el === "x" || el === "R") {
const counter = host.clipNoteCount;
if (host.hasLoaded) {
playerCb({ note: "", duration: "", time, counter });
try {
host.instrument.start(time);
} catch (e) {
eventCb("error", { e });
}
}
host.clipNoteCount++;
}
};
}
if (host.instrument instanceof Tone.PolySynth || host.instrument instanceof Tone.Sampler) {
return (time, el) => {
if (el === "x" || el === "R") {
const counter = host.clipNoteCount;
if (host.hasLoaded) {
const note = getNote(el, params, counter);
const duration = getDuration(params, counter);
playerCb({ note, duration, time, counter });
try {
host.instrument.triggerAttackRelease(note, duration, time);
} catch (e) {
eventCb("error", { e });
}
}
host.clipNoteCount++;
}
};
}
if (host.instrument instanceof Tone.NoiseSynth) {
return (time, el) => {
if (el === "x" || el === "R") {
const counter = host.clipNoteCount;
if (host.hasLoaded) {
const duration = getDuration(params, counter);
playerCb({ note: "", duration, time, counter });
try {
host.instrument.triggerAttackRelease(
duration,
time
);
} catch (e) {
eventCb("error", { e });
}
}
host.clipNoteCount++;
}
};
}
return (time, el) => {
if (el === "x" || el === "R") {
const counter = host.clipNoteCount;
if (host.hasLoaded) {
const note = getNote(el, params, counter)[0];
const duration = getDuration(params, counter);
playerCb({ note, duration, time, counter });
try {
host.instrument.triggerAttackRelease(note, duration, time);
} catch (e) {
eventCb("error", { e });
}
}
host.clipNoteCount++;
}
};
}
// 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/browser-clip.ts
var defaultSubdiv = "4n";
var defaultDur = "8n";
var getNote = (el, params, counter) => {
if (el === "R" && params.randomNotes && params.randomNotes.length > 0) {
return params.randomNotes[randomInt(params.randomNotes.length - 1)];
}
if (params.notes) {
return params.notes[counter % (params.notes.length || 1)];
}
return "";
};
var getDuration = (params, counter) => {
return params.durations ? params.durations[counter % params.durations.length] : params.dur || params.subdiv || defaultDur;
};
var recursivelyApplyPatternToDurations = (patternArr, length, durations = []) => {
patternArr.forEach((char) => {
if (typeof char === "string") {
if (char === "x" || char === "R") {
durations.push(length);
}
if (char === "_" && durations.length) {
durations[durations.length - 1] += length;
}
}
if (Array.isArray(char)) {
recursivelyApplyPatternToDurations(char, length / char.length, durations);
}
});
return durations;
};
var generateSequence = (params, channel, context) => {
context = context || Tone.getContext();
if (!params.pattern) {
throw new Error("No pattern provided!");
}
if (!params.durations && !params.dur) {
params.durations = recursivelyApplyPatternToDurations(
expandStr(params.pattern),
Tone.Ticks(params.subdiv || defaultSubdiv).toSeconds()
);
}
let callback;
if (channel) {
callback = channel.getSeqFn(params);
} else if (params.sample) {
const player = new Tone.Player({ url: params.sample, context });
player.toDestination();
player.sync();
const host = {
instrument: player,
hasLoaded: false,
clipNoteCount: 0
};
checkToneObjLoaded(player, () => {
host.hasLoaded = true;
});
const noop = () => {
};
callback = buildSequenceCallback(params, host, noop, noop);
} else {
throw new Error(
"Either a Channel or a sample URL must be provided to create a clip."
);
}
return new Tone.Sequence({
callback,
events: expandStr(params.pattern),
subdivision: params.subdiv || defaultSubdiv,
context
});
};
var totalPatternDuration = (pattern, subdivOrLength) => {
return typeof subdivOrLength === "number" ? subdivOrLength * expandStr(pattern).length : Tone.Ticks(subdivOrLength).toSeconds() * expandStr(pattern).length;
};
var leastCommonMultiple = (n1, n2) => {
const [smallest, largest] = n1 < n2 ? [n1, n2] : [n2, n1];
let i = largest;
while (i % smallest !== 0) {
i += largest;
}
return i;
};
var renderingDuration = (pattern, subdivOrLength, notes, randomNotes) => {
const patternRegularNotesCount = pattern.split("").filter((c) => {
return c === "x";
}).length;
const patternRandomNotesCount = pattern.split("").filter((c) => {
return c === "R";
}).length;
const patternNotesCount = randomNotes?.length ? patternRegularNotesCount : patternRegularNotesCount + patternRandomNotesCount;
const notesCount = notes.length || 1;
return totalPatternDuration(pattern, subdivOrLength) / patternNotesCount * leastCommonMultiple(notesCount, patternNotesCount);
};
var ongoingRenderingCounter = 0;
var originalContext;
var offlineRenderClip = (params, duration) => {
if (!originalContext) {
originalContext = Tone.getContext();
}
ongoingRenderingCounter++;
const player = new Tone.Player({ context: originalContext, loop: true });
Tone.Offline(async (context) => {
const sequence = generateSequence(params, context);
await Tone.loaded();
sequence.start();
context.transport.start();
}, duration).then((buffer) => {
player.buffer = buffer;
ongoingRenderingCounter--;
if (ongoingRenderingCounter === 0) {
if (originalContext) {
Tone.setContext(originalContext);
}
params.offlineRenderingCallback?.();
}
});
player.toDestination();
player.sync();
return player;
};
var clip = (params, channel) => {
params = preprocessClipParams(params, { align: "1m", alignOffset: "0" });
if (params.offlineRendering) {
return offlineRenderClip(
params,
renderingDuration(
params.pattern,
params.subdiv || defaultSubdiv,
params.notes || [],
params.randomNotes
)
);
}
return generateSequence(params, channel, originalContext);
};
// 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 [];
};
// src/channel/effects-chain.ts
function initEffects(instrument, context, params) {
context = context || Tone.getContext();
const createEffect = (effect) => {
return new Promise((resolve, _reject) => {
if (typeof effect === "string") {
resolve(
new Tone[effect]({
context
})
);
} else if (effect.context !== context) {
return recreateToneObjectInContext(
effect,
context
);
} else {
resolve(effect);
}
}).then((effectOut) => {
return effectOut.toDestination();
});
};
const startEffect = (eff) => {
return typeof eff.start === "function" ? eff.start() : eff;
};
const toArray = (someVal) => {
if (!someVal) {
return [];
}
if (Array.isArray(someVal)) {
return someVal;
}
return [someVal];
};
const effectsIn = toArray(params.effects);
if (params.external) {
if (effectsIn.length !== 0) {
throw new Error("Effects cannot be used with external output");
}
return Promise.resolve();
}
return Promise.all(effectsIn.map(createEffect)).then((results) => results.map(startEffect)).then((effects) => {
instrument.chain(...effects).toDestination();
});
}
// src/channel.ts
var getNextPos = (clip2) => {
const transportPosTicks = Tone.Transport.ticks;
if (transportPosTicks < Tone.Ticks("4n").toTicks()) {
return 0;
}
const align = clip2?.align || "1m";
const alignOffset = clip2?.alignOffset || "0";
const alignTicks = Tone.Ticks(align).toTicks();
const alignOffsetTicks = Tone.Ticks(alignOffset).toTicks();
const nextPosTicks = Tone.Ticks(
Math.floor(transportPosTicks / alignTicks + 1) * alignTicks + alignOffsetTicks
);
return nextPosTicks;
};
var Channel = class {
constructor(params) {
this.idx = params.idx || 0;
this.name = params.name || `ch ${params.idx}`;
this.activePatternIdx = -1;
this.channelClips = [];
this.clipNoteCount = 0;
const { clips, samples, sample, synth, ...params1 } = params;
const { external, sampler, buffer, ...params2 } = params1;
const { player, instrument, volume, ...params3 } = params2;
const { eventCb, playerCb, effects, ...params4 } = params3;
const { context = Tone.getContext(), ...originalParamsFiltered } = params4;
this.eventCbFn = eventCb;
this.playerCbFn = playerCb;
this.hasLoaded = false;
this.hasFailed = false;
const result = createInstrument(context, params, {
idx: this.idx,
name: this.name
});
this.instrument = result.instrument;
this.external = result.external;
this.initializerTask = result.initPromise.then((finalInstrument) => {
this.instrument = finalInstrument;
return initEffects(this.instrument, context, params);
});
let clipsFailed = false;
try {
(params.clips ?? []).forEach((c, i) => {
try {
this.addClip({
...c,
...originalParamsFiltered
});
} catch (e) {
throw new Error(
`${errorHasMessage(e) ? e.message : e} in clip ${i + 1}`
);
}
}, this);
} catch (e) {
clipsFailed = e;
}
this.initializerTask.then(() => {
if (clipsFailed) {
throw clipsFailed;
}
this.hasLoaded = true;
this.eventCb("loaded", {});
}).catch((e) => {
this.hasFailed = e;
this.eventCb("error", { e });
});
}
/** Set the global transport tempo in BPM. */
static setTransportTempo(valueBpm) {
Tone.Transport.bpm.value = valueBpm;
}
/** Resume the audio context and start the global transport. */
static startTransport() {
Tone.start();
Tone.Transport.start();
}
/**
* Stop the global transport.
* @param deleteEvents - If true (default), cancels all scheduled transport events.
*/
static stopTransport(deleteEvents = true) {
Tone.Transport.stop();
if (deleteEvents) {
Tone.Transport.cancel();
}
}
/** Set the volume (in dB) of this channel's instrument and external output. */
setVolume(volume) {
if (this.instrument) {
this.instrument.volume.value = volume;
}
if (this.external) {
this.external.setVolume?.(volume);
}
}
/**
* Start the clip at the given index, stopping any other active clip first.
* @param idx - Clip index in this channel
* @param position - Transport time to start at; defaults to the next aligned position
*/
startClip(idx, position) {
const clip2 = this.channelClips[idx];
position = position || (position === 0 ? 0 : getNextPos(clip2));
if (this.activePatternIdx > -1 && this.activePatternIdx !== idx) {
this.stopClip(this.activePatternIdx, position);
}
if (clip2 && clip2.state !== "started") {
this.counterResetTask = Tone.Transport.scheduleOnce(
() => {
this.clipNoteCount = 0;
},
position
);
this.activePatternIdx = idx;
clip2?.start(position);
}
}
/**
* Stop the clip at the given index.
* @param idx - Clip index in this channel
* @param position - Transport time to stop at; defaults to the next aligned position
*/
stopClip(idx, position) {
const clip2 = this.channelClips[idx];
position = position || (position === 0 ? 0 : getNextPos(clip2));
clip2?.stop(position);
if (idx === this.activePatternIdx) {
this.activePatternIdx = -1;
}
}
/**
* Add a clip to this channel. If the clip has a pattern, a Tone.Sequence is
* created; otherwise an empty (null) slot is reserved.
* @param clipParams - Clip configuration
* @param idx - Slot index; defaults to the next available position
*/
addClip(clipParams, idx) {
idx = idx || this.channelClips.length;
if (clipParams.pattern) {
this.channelClips[idx] = clip(
{
...clipParams
},
this
);
const seq = this.channelClips[idx];
if (seq && clipParams.align) seq.align = clipParams.align;
if (seq && clipParams.alignOffset)
seq.alignOffset = clipParams.alignOffset;
} else {
this.channelClips[idx] = null;
}
}
/**
* @param {Object} ClipParams clip parameters
* @return {Function} function that can be used as the callback in Tone.Sequence https://tonejs.github.io/docs/Sequence
*/
getSeqFn(params) {
return buildSequenceCallback(
params,
this,
(p) => this.playerCb(p),
(e, p) => this.eventCb(e, p)
);
}
/** Invoke the user-provided event callback, if set. */
eventCb(event, params) {
if (typeof this.eventCbFn === "function") {
params.channel = this;
this.eventCbFn(event, params);
}
}
/** Invoke the user-provided player observer callback, if set. */
playerCb(params) {
if (typeof this.playerCbFn === "function") {
params.channel = this;
this.playerCbFn(params);
}
}
/** All clips (sequences) belonging to this channel. */
get clips() {
return this.channelClips;
}
/** Index of the currently playing clip, or -1 if none. */
get activeClipIdx() {
return this.activePatternIdx;
}
};
// src/session.ts
var Session = class {
/** Create a session, optionally pre-populated with channels. */
constructor(arr) {
arr = arr || [];
this.sessionChannels = arr.map((ch, i) => {
ch.idx = ch.idx || i;
ch.idx = this.uniqueIdx(this.sessionChannels, ch.idx);
return new Channel(ch);
});
}
/** Return a unique channel index, generating a new one if `idx` is taken or missing. */
uniqueIdx(channels, idx) {
if (!channels) {
return idx || 0;
}
const idxs = channels.reduce((acc, c) => {
return !acc.find((i) => i === c.idx) && acc.concat(c.idx) || acc;
}, []);
if (!idx || idxs.find((i) => i === idx)) {
let newIdx = channels.length;
while (idxs.find((i) => i === newIdx)) {
newIdx = newIdx + 1;
}
return newIdx;
}
return idx;
}
/** Create a new channel with a unique index and add it to the session. */
createChannel(ch) {
ch.idx = this.uniqueIdx(this.sessionChannels, ch.idx);
const newChannel = new Channel(ch);
this.sessionChannels.push(newChannel);
return newChannel;
}
/** All channels in this session. */
get channels() {
return this.sessionChannels;
}
/** Set the global transport tempo in BPM. */
setTransportTempo(valueBpm) {
Channel.setTransportTempo(valueBpm);
}
/** Resume the audio context and start the global transport. */
startTransport() {
Channel.startTransport();
}
/**
* Stop the global transport.
* @param deleteEvents - If true (default), cancels all scheduled transport events.
*/
stopTransport(deleteEvents = true) {
Channel.stopTransport(deleteEvents);
}
/** Start the clip at the given index across all channels simultaneously. */
startRow(idx) {
this.sessionChannels.forEach((ch) => {
ch.startClip(idx);
});
}
/**
* Schedule clip playback across channels using a song-structure pattern.
* Each channel pattern is a string where each character is a clip index
* (or `-` for silence, `_` to sustain the previous clip).
*/
play(params) {
const channelPatterns = params.channelPatterns;
const clipDuration = params.clipDuration || "4:0:0";
const clipDurationInSeconds = Tone.Time(clipDuration).toSeconds();
const stopClips = (clips, time) => {
clips.forEach((c) => {
c.stop(time);
});
};
const startClips = (channelIdx, clipIdx, time) => {
if (clipIdx === "-") return [];
const clips = this.channels.filter((c) => c.idx === channelIdx).map((c) => c.clips[Number(clipIdx)]).filter((c) => c != null);
for (const c of clips) c.start(time);
return clips;
};
channelPatterns.forEach(({ channelIdx, pattern }) => {
let clips = [];
let time = 0;
let prevClipIdx = "-";
pattern.split("").forEach((clipIdx) => {
if (clipIdx !== prevClipIdx && clipIdx !== "_") {
stopClips(clips, time);
clips = startClips(channelIdx, clipIdx, time);
}
prevClipIdx = clipIdx;
time += clipDurationInSeconds;
});
stopClips(clips, time);
});
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Session,
arp,
chord,
chords,
clip,
getChordDegrees,
getChordsByProgression,
midi,
mode,
modes,
progression,
scale,
scales
});
//# sourceMappingURL=browser.cjs.map