UNPKG

scribbletune

Version:

Create music with JavaScript and Node.js!

1,185 lines (1,172 loc) 36.9 kB
"use strict"; 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