@react-synth/synth 0.0.6-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1701 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import React, { createContext, useRef, useEffect, useMemo, useContext, useId, Children, isValidElement, cloneElement } from "react";
3
+ import { AudioContext } from "node-web-audio-api";
4
+ import { readFile } from "fs/promises";
5
+ import { fileURLToPath } from "url";
6
+ import { createRequire } from "module";
7
+ import { dirname, join } from "path";
8
+ let schedulerInstance;
9
+ let audioContextInstance;
10
+ class Scheduler {
11
+ context;
12
+ _bpm;
13
+ startTime;
14
+ events = /* @__PURE__ */ new Map();
15
+ timerID = null;
16
+ running = false;
17
+ lookahead = 0.1;
18
+ scheduleInterval = 25;
19
+ /** Grace period before actually removing a loop - allows React HMR to re-add it */
20
+ removalGracePeriod = 100;
21
+ constructor(bpm) {
22
+ if (!audioContextInstance) {
23
+ audioContextInstance = new AudioContext();
24
+ }
25
+ this.context = audioContextInstance;
26
+ this._bpm = bpm;
27
+ this.startTime = this.context.currentTime;
28
+ }
29
+ // --- Public API ---
30
+ get audioContext() {
31
+ return this.context;
32
+ }
33
+ get bpm() {
34
+ return this._bpm;
35
+ }
36
+ set bpm(value) {
37
+ this._bpm = value;
38
+ }
39
+ start() {
40
+ if (this.running) return;
41
+ this.startTime = this.context.currentTime;
42
+ this.running = true;
43
+ for (const event of this.events.values()) {
44
+ if (event.intervalBeats > 0) {
45
+ event.nextBeatTime = 0;
46
+ event.active = true;
47
+ }
48
+ }
49
+ this.timerID = setInterval(() => this.schedule(), this.scheduleInterval);
50
+ }
51
+ stop() {
52
+ if (this.timerID) {
53
+ clearInterval(this.timerID);
54
+ this.timerID = null;
55
+ }
56
+ this.running = false;
57
+ console.debug("[scheduler] stopped");
58
+ }
59
+ clear() {
60
+ this.events.clear();
61
+ }
62
+ addLoop(id, intervalBeats, callback, startBeat) {
63
+ const existing = this.events.get(id);
64
+ if (existing && existing.intervalBeats === intervalBeats) {
65
+ existing.callback = callback;
66
+ existing.active = true;
67
+ existing.pendingRemoval = void 0;
68
+ return;
69
+ }
70
+ const currentBeat = this.getCurrentBeat();
71
+ let nextBeat;
72
+ if (startBeat !== void 0) {
73
+ nextBeat = startBeat;
74
+ } else {
75
+ nextBeat = Math.ceil(currentBeat / intervalBeats) * intervalBeats;
76
+ if (nextBeat <= currentBeat) {
77
+ nextBeat += intervalBeats;
78
+ }
79
+ }
80
+ this.events.delete(id);
81
+ this.events.set(id, {
82
+ id,
83
+ nextBeatTime: nextBeat,
84
+ intervalBeats,
85
+ callback,
86
+ active: true
87
+ });
88
+ console.debug(
89
+ `[scheduler] loop "${id}" added, interval=${intervalBeats} beats, start=${nextBeat.toFixed(2)}`
90
+ );
91
+ }
92
+ remove(id) {
93
+ const event = this.events.get(id);
94
+ if (event) {
95
+ event.pendingRemoval = Date.now();
96
+ }
97
+ }
98
+ beatsToSeconds(beats) {
99
+ return beats * 60 / this._bpm;
100
+ }
101
+ // --- Private ---
102
+ secondsToBeats(seconds) {
103
+ return seconds * this._bpm / 60;
104
+ }
105
+ getCurrentBeat() {
106
+ const elapsed = this.context.currentTime - this.startTime;
107
+ return this.secondsToBeats(elapsed);
108
+ }
109
+ beatToAudioTime(beatTime) {
110
+ return this.startTime + this.beatsToSeconds(beatTime);
111
+ }
112
+ schedule() {
113
+ const currentAudioTime = this.context.currentTime;
114
+ const currentBeat = this.getCurrentBeat();
115
+ const scheduleUntilBeat = currentBeat + this.secondsToBeats(this.lookahead);
116
+ const now = Date.now();
117
+ for (const event of this.events.values()) {
118
+ if (event.pendingRemoval !== void 0) continue;
119
+ if (!event.active) continue;
120
+ while (event.nextBeatTime < scheduleUntilBeat) {
121
+ const audioTime = this.beatToAudioTime(event.nextBeatTime);
122
+ if (audioTime >= currentAudioTime) {
123
+ event.callback(audioTime, event.nextBeatTime);
124
+ }
125
+ if (event.intervalBeats > 0) {
126
+ event.nextBeatTime += event.intervalBeats;
127
+ } else {
128
+ event.active = false;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ for (const [id, event] of this.events) {
134
+ if (!event.active && event.intervalBeats === 0) {
135
+ this.events.delete(id);
136
+ } else if (event.pendingRemoval !== void 0 && now - event.pendingRemoval > this.removalGracePeriod) {
137
+ this.events.delete(id);
138
+ console.debug(`[scheduler] loop "${id}" removed`);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ function getScheduler(bpm) {
144
+ if (!schedulerInstance) {
145
+ schedulerInstance = new Scheduler(bpm);
146
+ } else {
147
+ schedulerInstance.bpm = bpm;
148
+ }
149
+ return schedulerInstance;
150
+ }
151
+ function resetScheduler(bpm) {
152
+ if (schedulerInstance) {
153
+ schedulerInstance.stop();
154
+ schedulerInstance.clear();
155
+ }
156
+ schedulerInstance = new Scheduler(bpm);
157
+ return schedulerInstance;
158
+ }
159
+ const ScheduleNoteContext = createContext(null);
160
+ const TrackContext = createContext(null);
161
+ function Track({ bpm, children }) {
162
+ const scheduler = useRef(getScheduler(bpm));
163
+ const audioContext = scheduler.current.audioContext;
164
+ useEffect(() => {
165
+ scheduler.current.start();
166
+ }, [scheduler]);
167
+ const scheduleContextValue = useMemo(
168
+ () => ({
169
+ scheduleNote: (_id, callback) => {
170
+ const now = audioContext.currentTime + 5e-3;
171
+ callback(now, 0);
172
+ },
173
+ unscheduleNote: () => {
174
+ }
175
+ }),
176
+ [audioContext]
177
+ );
178
+ return /* @__PURE__ */ jsx(
179
+ TrackContext.Provider,
180
+ {
181
+ value: {
182
+ audioContext,
183
+ scheduler: scheduler.current
184
+ },
185
+ children: /* @__PURE__ */ jsx(ScheduleNoteContext.Provider, { value: scheduleContextValue, children })
186
+ }
187
+ );
188
+ }
189
+ function useTrack() {
190
+ const ctx = useContext(TrackContext);
191
+ if (!ctx) {
192
+ throw new Error("useTrack must be used inside a <Track> component");
193
+ }
194
+ return ctx;
195
+ }
196
+ function useScheduleNote() {
197
+ const ctx = useContext(ScheduleNoteContext);
198
+ if (!ctx) {
199
+ throw new Error("useScheduleNote must be used inside a <Track> component");
200
+ }
201
+ return ctx;
202
+ }
203
+ function Loop({
204
+ id,
205
+ interval: interval2,
206
+ children
207
+ }) {
208
+ const { scheduler } = useTrack();
209
+ const callbacksRef = useRef(/* @__PURE__ */ new Map());
210
+ useEffect(() => {
211
+ const loopId = id;
212
+ const callbacks = callbacksRef.current;
213
+ const runAllCallbacks = (audioTime, beatTime) => {
214
+ for (const callback of callbacks.values()) {
215
+ callback(audioTime, beatTime);
216
+ }
217
+ };
218
+ scheduler.addLoop(loopId, interval2, runAllCallbacks);
219
+ return () => {
220
+ scheduler.remove(loopId);
221
+ };
222
+ }, [id, interval2, scheduler]);
223
+ const scheduleContextValue = useMemo(
224
+ () => ({
225
+ scheduleNote: (noteId, callback) => {
226
+ callbacksRef.current.set(noteId, callback);
227
+ },
228
+ unscheduleNote: (noteId) => {
229
+ callbacksRef.current.delete(noteId);
230
+ }
231
+ }),
232
+ []
233
+ );
234
+ return /* @__PURE__ */ jsx(ScheduleNoteContext.Provider, { value: scheduleContextValue, children });
235
+ }
236
+ const ADSR_DEFAULTS = {
237
+ attack: 0,
238
+ attack_level: 1,
239
+ decay: 0,
240
+ sustain: 0,
241
+ sustain_level: 1,
242
+ release: 1
243
+ };
244
+ function applyADSREnvelope(gainNode, audioTime, props, amp, beatsToSeconds) {
245
+ const attack = props.attack ?? ADSR_DEFAULTS.attack;
246
+ const attack_level = props.attack_level ?? ADSR_DEFAULTS.attack_level;
247
+ const decay = props.decay ?? ADSR_DEFAULTS.decay;
248
+ const sustain = props.sustain ?? ADSR_DEFAULTS.sustain;
249
+ const sustain_level = props.sustain_level ?? ADSR_DEFAULTS.sustain_level;
250
+ const release = props.release ?? ADSR_DEFAULTS.release;
251
+ const decay_level = props.decay_level ?? sustain_level;
252
+ const attackSec = beatsToSeconds(attack);
253
+ const decaySec = beatsToSeconds(decay);
254
+ const sustainSec = beatsToSeconds(sustain);
255
+ const releaseSec = beatsToSeconds(release);
256
+ const attackAmp = attack_level * amp;
257
+ const decayAmp = decay_level * amp;
258
+ const sustainAmp = sustain_level * amp;
259
+ const attackEnd = audioTime + attackSec;
260
+ const decayEnd = attackEnd + decaySec;
261
+ const sustainEnd = decayEnd + sustainSec;
262
+ const releaseEnd = sustainEnd + releaseSec;
263
+ gainNode.gain.setValueAtTime(1e-3, audioTime);
264
+ if (attackSec > 0) {
265
+ gainNode.gain.linearRampToValueAtTime(attackAmp, attackEnd);
266
+ } else {
267
+ gainNode.gain.setValueAtTime(attackAmp, audioTime);
268
+ }
269
+ if (decaySec > 0) {
270
+ gainNode.gain.linearRampToValueAtTime(decayAmp, decayEnd);
271
+ } else {
272
+ gainNode.gain.setValueAtTime(decayAmp, attackEnd);
273
+ }
274
+ if (sustainSec > 0) {
275
+ gainNode.gain.linearRampToValueAtTime(sustainAmp, decayEnd + 1e-3);
276
+ gainNode.gain.setValueAtTime(sustainAmp, sustainEnd);
277
+ } else {
278
+ gainNode.gain.setValueAtTime(sustainAmp, decayEnd);
279
+ }
280
+ gainNode.gain.exponentialRampToValueAtTime(1e-3, releaseEnd);
281
+ return releaseEnd;
282
+ }
283
+ function isNamedPitch(src) {
284
+ return src !== null && typeof src === "object" && "name" in src && typeof src.name === "string" ? true : false;
285
+ }
286
+ function isPitch(pitch2) {
287
+ return pitch2 !== null && typeof pitch2 === "object" && "step" in pitch2 && typeof pitch2.step === "number" && "alt" in pitch2 && typeof pitch2.alt === "number" && !isNaN(pitch2.step) && !isNaN(pitch2.alt) ? true : false;
288
+ }
289
+ var FIFTHS = [0, 2, 4, -1, 1, 3, 5];
290
+ var STEPS_TO_OCTS = FIFTHS.map(
291
+ (fifths) => Math.floor(fifths * 7 / 12)
292
+ );
293
+ function coordinates(pitch2) {
294
+ const { step, alt, oct, dir = 1 } = pitch2;
295
+ const f = FIFTHS[step] + 7 * alt;
296
+ if (oct === void 0) {
297
+ return [dir * f];
298
+ }
299
+ const o = oct - STEPS_TO_OCTS[step] - 4 * alt;
300
+ return [dir * f, dir * o];
301
+ }
302
+ var FIFTHS_TO_STEPS = [3, 0, 4, 1, 5, 2, 6];
303
+ function pitch(coord) {
304
+ const [f, o, dir] = coord;
305
+ const step = FIFTHS_TO_STEPS[unaltered(f)];
306
+ const alt = Math.floor((f + 1) / 7);
307
+ if (o === void 0) {
308
+ return { step, alt, dir };
309
+ }
310
+ const oct = o + 4 * alt + STEPS_TO_OCTS[step];
311
+ return { step, alt, oct, dir };
312
+ }
313
+ function unaltered(f) {
314
+ const i = (f + 1) % 7;
315
+ return i < 0 ? 7 + i : i;
316
+ }
317
+ var fillStr$1 = (s, n) => Array(Math.abs(n) + 1).join(s);
318
+ var NoInterval = Object.freeze({
319
+ empty: true,
320
+ name: "",
321
+ num: NaN,
322
+ q: "",
323
+ type: "",
324
+ step: NaN,
325
+ alt: NaN,
326
+ dir: NaN,
327
+ simple: NaN,
328
+ semitones: NaN,
329
+ chroma: NaN,
330
+ coord: [],
331
+ oct: NaN
332
+ });
333
+ var INTERVAL_TONAL_REGEX = "([-+]?\\d+)(d{1,4}|m|M|P|A{1,4})";
334
+ var INTERVAL_SHORTHAND_REGEX = "(AA|A|P|M|m|d|dd)([-+]?\\d+)";
335
+ var REGEX$2 = new RegExp(
336
+ "^" + INTERVAL_TONAL_REGEX + "|" + INTERVAL_SHORTHAND_REGEX + "$"
337
+ );
338
+ function tokenizeInterval(str) {
339
+ const m = REGEX$2.exec(`${str}`);
340
+ if (m === null) {
341
+ return ["", ""];
342
+ }
343
+ return m[1] ? [m[1], m[2]] : [m[4], m[3]];
344
+ }
345
+ var cache$2 = {};
346
+ function interval(src) {
347
+ return typeof src === "string" ? cache$2[src] || (cache$2[src] = parse$1(src)) : isPitch(src) ? interval(pitchName$1(src)) : isNamedPitch(src) ? interval(src.name) : NoInterval;
348
+ }
349
+ var SIZES = [0, 2, 4, 5, 7, 9, 11];
350
+ var TYPES = "PMMPPMM";
351
+ function parse$1(str) {
352
+ const tokens = tokenizeInterval(str);
353
+ if (tokens[0] === "") {
354
+ return NoInterval;
355
+ }
356
+ const num = +tokens[0];
357
+ const q = tokens[1];
358
+ const step = (Math.abs(num) - 1) % 7;
359
+ const t = TYPES[step];
360
+ if (t === "M" && q === "P") {
361
+ return NoInterval;
362
+ }
363
+ const type = t === "M" ? "majorable" : "perfectable";
364
+ const name = "" + num + q;
365
+ const dir = num < 0 ? -1 : 1;
366
+ const simple = num === 8 || num === -8 ? num : dir * (step + 1);
367
+ const alt = qToAlt(type, q);
368
+ const oct = Math.floor((Math.abs(num) - 1) / 7);
369
+ const semitones = dir * (SIZES[step] + alt + 12 * oct);
370
+ const chroma = (dir * (SIZES[step] + alt) % 12 + 12) % 12;
371
+ const coord = coordinates({ step, alt, oct, dir });
372
+ return {
373
+ empty: false,
374
+ name,
375
+ num,
376
+ q,
377
+ step,
378
+ alt,
379
+ dir,
380
+ type,
381
+ simple,
382
+ semitones,
383
+ chroma,
384
+ coord,
385
+ oct
386
+ };
387
+ }
388
+ function coordToInterval(coord, forceDescending) {
389
+ const [f, o = 0] = coord;
390
+ const isDescending = f * 7 + o * 12 < 0;
391
+ const ivl = forceDescending || isDescending ? [-f, -o, -1] : [f, o, 1];
392
+ return interval(pitch(ivl));
393
+ }
394
+ function qToAlt(type, q) {
395
+ return q === "M" && type === "majorable" || q === "P" && type === "perfectable" ? 0 : q === "m" && type === "majorable" ? -1 : /^A+$/.test(q) ? q.length : /^d+$/.test(q) ? -1 * (type === "perfectable" ? q.length : q.length + 1) : 0;
396
+ }
397
+ function pitchName$1(props) {
398
+ const { step, alt, oct = 0, dir } = props;
399
+ if (!dir) {
400
+ return "";
401
+ }
402
+ const calcNum = step + 1 + 7 * oct;
403
+ const num = calcNum === 0 ? step + 1 : calcNum;
404
+ const d = dir < 0 ? "-" : "";
405
+ const type = TYPES[step] === "M" ? "majorable" : "perfectable";
406
+ const name = d + num + altToQ(type, alt);
407
+ return name;
408
+ }
409
+ function altToQ(type, alt) {
410
+ if (alt === 0) {
411
+ return type === "majorable" ? "M" : "P";
412
+ } else if (alt === -1 && type === "majorable") {
413
+ return "m";
414
+ } else if (alt > 0) {
415
+ return fillStr$1("A", alt);
416
+ } else {
417
+ return fillStr$1("d", type === "perfectable" ? alt : alt + 1);
418
+ }
419
+ }
420
+ var fillStr = (s, n) => Array(Math.abs(n) + 1).join(s);
421
+ var NoNote = Object.freeze({
422
+ empty: true,
423
+ name: "",
424
+ letter: "",
425
+ acc: "",
426
+ pc: "",
427
+ step: NaN,
428
+ alt: NaN,
429
+ chroma: NaN,
430
+ height: NaN,
431
+ coord: [],
432
+ midi: null,
433
+ freq: null
434
+ });
435
+ var cache$1 = /* @__PURE__ */ new Map();
436
+ var stepToLetter = (step) => "CDEFGAB".charAt(step);
437
+ var altToAcc = (alt) => alt < 0 ? fillStr("b", -alt) : fillStr("#", alt);
438
+ var accToAlt = (acc) => acc[0] === "b" ? -acc.length : acc.length;
439
+ function note(src) {
440
+ const stringSrc = JSON.stringify(src);
441
+ const cached = cache$1.get(stringSrc);
442
+ if (cached) {
443
+ return cached;
444
+ }
445
+ const value = typeof src === "string" ? parse(src) : isPitch(src) ? note(pitchName(src)) : isNamedPitch(src) ? note(src.name) : NoNote;
446
+ cache$1.set(stringSrc, value);
447
+ return value;
448
+ }
449
+ var REGEX$1 = /^([a-gA-G]?)(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)$/;
450
+ function tokenizeNote(str) {
451
+ const m = REGEX$1.exec(str);
452
+ return m ? [m[1].toUpperCase(), m[2].replace(/x/g, "##"), m[3], m[4]] : ["", "", "", ""];
453
+ }
454
+ function coordToNote(noteCoord) {
455
+ return note(pitch(noteCoord));
456
+ }
457
+ var mod = (n, m) => (n % m + m) % m;
458
+ var SEMI = [0, 2, 4, 5, 7, 9, 11];
459
+ function parse(noteName) {
460
+ const tokens = tokenizeNote(noteName);
461
+ if (tokens[0] === "" || tokens[3] !== "") {
462
+ return NoNote;
463
+ }
464
+ const letter = tokens[0];
465
+ const acc = tokens[1];
466
+ const octStr = tokens[2];
467
+ const step = (letter.charCodeAt(0) + 3) % 7;
468
+ const alt = accToAlt(acc);
469
+ const oct = octStr.length ? +octStr : void 0;
470
+ const coord = coordinates({ step, alt, oct });
471
+ const name = letter + acc + octStr;
472
+ const pc = letter + acc;
473
+ const chroma = (SEMI[step] + alt + 120) % 12;
474
+ const height = oct === void 0 ? mod(SEMI[step] + alt, 12) - 12 * 99 : SEMI[step] + alt + 12 * (oct + 1);
475
+ const midi = height >= 0 && height <= 127 ? height : null;
476
+ const freq2 = oct === void 0 ? null : Math.pow(2, (height - 69) / 12) * 440;
477
+ return {
478
+ empty: false,
479
+ acc,
480
+ alt,
481
+ chroma,
482
+ coord,
483
+ freq: freq2,
484
+ height,
485
+ letter,
486
+ midi,
487
+ name,
488
+ oct,
489
+ pc,
490
+ step
491
+ };
492
+ }
493
+ function pitchName(props) {
494
+ const { step, alt, oct } = props;
495
+ const letter = stepToLetter(step);
496
+ if (!letter) {
497
+ return "";
498
+ }
499
+ const pc = letter + altToAcc(alt);
500
+ return oct || oct === 0 ? pc + oct : pc;
501
+ }
502
+ function transpose(noteName, intervalName) {
503
+ const note$1 = note(noteName);
504
+ const intervalCoord = Array.isArray(intervalName) ? intervalName : interval(intervalName).coord;
505
+ if (note$1.empty || !intervalCoord || intervalCoord.length < 2) {
506
+ return "";
507
+ }
508
+ const noteCoord = note$1.coord;
509
+ const tr = noteCoord.length === 1 ? [noteCoord[0] + intervalCoord[0]] : [noteCoord[0] + intervalCoord[0], noteCoord[1] + intervalCoord[1]];
510
+ return coordToNote(tr).name;
511
+ }
512
+ function distance(fromNote, toNote) {
513
+ const from = note(fromNote);
514
+ const to = note(toNote);
515
+ if (from.empty || to.empty) {
516
+ return "";
517
+ }
518
+ const fcoord = from.coord;
519
+ const tcoord = to.coord;
520
+ const fifths = tcoord[0] - fcoord[0];
521
+ const octs = fcoord.length === 2 && tcoord.length === 2 ? tcoord[1] - fcoord[1] : -Math.floor(fifths * 7 / 12);
522
+ const forceDescending = to.height === from.height && to.midi !== null && from.oct === to.oct && from.step > to.step;
523
+ return coordToInterval([fifths, octs], forceDescending).name;
524
+ }
525
+ function rotate(times, arr) {
526
+ const len = arr.length;
527
+ const n = (times % len + len) % len;
528
+ return arr.slice(n, len).concat(arr.slice(0, n));
529
+ }
530
+ var EmptyPcset = {
531
+ empty: true,
532
+ name: "",
533
+ setNum: 0,
534
+ chroma: "000000000000",
535
+ normalized: "000000000000",
536
+ intervals: []
537
+ };
538
+ var setNumToChroma = (num2) => Number(num2).toString(2).padStart(12, "0");
539
+ var chromaToNumber = (chroma2) => parseInt(chroma2, 2);
540
+ var REGEX = /^[01]{12}$/;
541
+ function isChroma(set) {
542
+ return REGEX.test(set);
543
+ }
544
+ var isPcsetNum = (set) => typeof set === "number" && set >= 0 && set <= 4095;
545
+ var isPcset = (set) => set && isChroma(set.chroma);
546
+ var cache = { [EmptyPcset.chroma]: EmptyPcset };
547
+ function get$5(src) {
548
+ const chroma2 = isChroma(src) ? src : isPcsetNum(src) ? setNumToChroma(src) : Array.isArray(src) ? listToChroma(src) : isPcset(src) ? src.chroma : EmptyPcset.chroma;
549
+ return cache[chroma2] = cache[chroma2] || chromaToPcset(chroma2);
550
+ }
551
+ var IVLS = [
552
+ "1P",
553
+ "2m",
554
+ "2M",
555
+ "3m",
556
+ "3M",
557
+ "4P",
558
+ "5d",
559
+ "5P",
560
+ "6m",
561
+ "6M",
562
+ "7m",
563
+ "7M"
564
+ ];
565
+ function chromaToIntervals(chroma2) {
566
+ const intervals2 = [];
567
+ for (let i = 0; i < 12; i++) {
568
+ if (chroma2.charAt(i) === "1") intervals2.push(IVLS[i]);
569
+ }
570
+ return intervals2;
571
+ }
572
+ function chromaRotations(chroma2) {
573
+ const binary = chroma2.split("");
574
+ return binary.map((_, i) => rotate(i, binary).join(""));
575
+ }
576
+ function chromaToPcset(chroma2) {
577
+ const setNum = chromaToNumber(chroma2);
578
+ const normalizedNum = chromaRotations(chroma2).map(chromaToNumber).filter((n) => n >= 2048).sort()[0];
579
+ const normalized = setNumToChroma(normalizedNum);
580
+ const intervals2 = chromaToIntervals(chroma2);
581
+ return {
582
+ empty: false,
583
+ name: "",
584
+ setNum,
585
+ chroma: chroma2,
586
+ normalized,
587
+ intervals: intervals2
588
+ };
589
+ }
590
+ function listToChroma(set) {
591
+ if (set.length === 0) {
592
+ return EmptyPcset.chroma;
593
+ }
594
+ let pitch2;
595
+ const binary = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
596
+ for (let i = 0; i < set.length; i++) {
597
+ pitch2 = note(set[i]);
598
+ if (pitch2.empty) pitch2 = interval(set[i]);
599
+ if (!pitch2.empty) binary[pitch2.chroma] = 1;
600
+ }
601
+ return binary.join("");
602
+ }
603
+ var CHORDS = [
604
+ // ==Major==
605
+ ["1P 3M 5P", "major", "M ^ maj"],
606
+ ["1P 3M 5P 7M", "major seventh", "maj7 Δ ma7 M7 Maj7 ^7"],
607
+ ["1P 3M 5P 7M 9M", "major ninth", "maj9 Δ9 ^9"],
608
+ ["1P 3M 5P 7M 9M 13M", "major thirteenth", "maj13 Maj13 ^13"],
609
+ ["1P 3M 5P 6M", "sixth", "6 add6 add13 M6"],
610
+ ["1P 3M 5P 6M 9M", "sixth added ninth", "6add9 6/9 69 M69"],
611
+ ["1P 3M 6m 7M", "major seventh flat sixth", "M7b6 ^7b6"],
612
+ [
613
+ "1P 3M 5P 7M 11A",
614
+ "major seventh sharp eleventh",
615
+ "maj#4 Δ#4 Δ#11 M7#11 ^7#11 maj7#11"
616
+ ],
617
+ // ==Minor==
618
+ // '''Normal'''
619
+ ["1P 3m 5P", "minor", "m min -"],
620
+ ["1P 3m 5P 7m", "minor seventh", "m7 min7 mi7 -7"],
621
+ [
622
+ "1P 3m 5P 7M",
623
+ "minor/major seventh",
624
+ "m/ma7 m/maj7 mM7 mMaj7 m/M7 -Δ7 mΔ -^7 -maj7"
625
+ ],
626
+ ["1P 3m 5P 6M", "minor sixth", "m6 -6"],
627
+ ["1P 3m 5P 7m 9M", "minor ninth", "m9 -9"],
628
+ ["1P 3m 5P 7M 9M", "minor/major ninth", "mM9 mMaj9 -^9"],
629
+ ["1P 3m 5P 7m 9M 11P", "minor eleventh", "m11 -11"],
630
+ ["1P 3m 5P 7m 9M 13M", "minor thirteenth", "m13 -13"],
631
+ // '''Diminished'''
632
+ ["1P 3m 5d", "diminished", "dim ° o"],
633
+ ["1P 3m 5d 7d", "diminished seventh", "dim7 °7 o7"],
634
+ ["1P 3m 5d 7m", "half-diminished", "m7b5 ø -7b5 h7 h"],
635
+ // ==Dominant/Seventh==
636
+ // '''Normal'''
637
+ ["1P 3M 5P 7m", "dominant seventh", "7 dom"],
638
+ ["1P 3M 5P 7m 9M", "dominant ninth", "9"],
639
+ ["1P 3M 5P 7m 9M 13M", "dominant thirteenth", "13"],
640
+ ["1P 3M 5P 7m 11A", "lydian dominant seventh", "7#11 7#4"],
641
+ // '''Altered'''
642
+ ["1P 3M 5P 7m 9m", "dominant flat ninth", "7b9"],
643
+ ["1P 3M 5P 7m 9A", "dominant sharp ninth", "7#9"],
644
+ ["1P 3M 7m 9m", "altered", "alt7"],
645
+ // '''Suspended'''
646
+ ["1P 4P 5P", "suspended fourth", "sus4 sus"],
647
+ ["1P 2M 5P", "suspended second", "sus2"],
648
+ ["1P 4P 5P 7m", "suspended fourth seventh", "7sus4 7sus"],
649
+ ["1P 5P 7m 9M 11P", "eleventh", "11"],
650
+ [
651
+ "1P 4P 5P 7m 9m",
652
+ "suspended fourth flat ninth",
653
+ "b9sus phryg 7b9sus 7b9sus4"
654
+ ],
655
+ // ==Other==
656
+ ["1P 5P", "fifth", "5"],
657
+ ["1P 3M 5A", "augmented", "aug + +5 ^#5"],
658
+ ["1P 3m 5A", "minor augmented", "m#5 -#5 m+"],
659
+ ["1P 3M 5A 7M", "augmented seventh", "maj7#5 maj7+5 +maj7 ^7#5"],
660
+ [
661
+ "1P 3M 5P 7M 9M 11A",
662
+ "major sharp eleventh (lydian)",
663
+ "maj9#11 Δ9#11 ^9#11"
664
+ ],
665
+ // ==Legacy==
666
+ ["1P 2M 4P 5P", "", "sus24 sus4add9"],
667
+ ["1P 3M 5A 7M 9M", "", "maj9#5 Maj9#5"],
668
+ ["1P 3M 5A 7m", "", "7#5 +7 7+ 7aug aug7"],
669
+ ["1P 3M 5A 7m 9A", "", "7#5#9 7#9#5 7alt"],
670
+ ["1P 3M 5A 7m 9M", "", "9#5 9+"],
671
+ ["1P 3M 5A 7m 9M 11A", "", "9#5#11"],
672
+ ["1P 3M 5A 7m 9m", "", "7#5b9 7b9#5"],
673
+ ["1P 3M 5A 7m 9m 11A", "", "7#5b9#11"],
674
+ ["1P 3M 5A 9A", "", "+add#9"],
675
+ ["1P 3M 5A 9M", "", "M#5add9 +add9"],
676
+ ["1P 3M 5P 6M 11A", "", "M6#11 M6b5 6#11 6b5"],
677
+ ["1P 3M 5P 6M 7M 9M", "", "M7add13"],
678
+ ["1P 3M 5P 6M 9M 11A", "", "69#11"],
679
+ ["1P 3m 5P 6M 9M", "", "m69 -69"],
680
+ ["1P 3M 5P 6m 7m", "", "7b6"],
681
+ ["1P 3M 5P 7M 9A 11A", "", "maj7#9#11"],
682
+ ["1P 3M 5P 7M 9M 11A 13M", "", "M13#11 maj13#11 M13+4 M13#4"],
683
+ ["1P 3M 5P 7M 9m", "", "M7b9"],
684
+ ["1P 3M 5P 7m 11A 13m", "", "7#11b13 7b5b13"],
685
+ ["1P 3M 5P 7m 13M", "", "7add6 67 7add13"],
686
+ ["1P 3M 5P 7m 9A 11A", "", "7#9#11 7b5#9 7#9b5"],
687
+ ["1P 3M 5P 7m 9A 11A 13M", "", "13#9#11"],
688
+ ["1P 3M 5P 7m 9A 11A 13m", "", "7#9#11b13"],
689
+ ["1P 3M 5P 7m 9A 13M", "", "13#9"],
690
+ ["1P 3M 5P 7m 9A 13m", "", "7#9b13"],
691
+ ["1P 3M 5P 7m 9M 11A", "", "9#11 9+4 9#4"],
692
+ ["1P 3M 5P 7m 9M 11A 13M", "", "13#11 13+4 13#4"],
693
+ ["1P 3M 5P 7m 9M 11A 13m", "", "9#11b13 9b5b13"],
694
+ ["1P 3M 5P 7m 9m 11A", "", "7b9#11 7b5b9 7b9b5"],
695
+ ["1P 3M 5P 7m 9m 11A 13M", "", "13b9#11"],
696
+ ["1P 3M 5P 7m 9m 11A 13m", "", "7b9b13#11 7b9#11b13 7b5b9b13"],
697
+ ["1P 3M 5P 7m 9m 13M", "", "13b9"],
698
+ ["1P 3M 5P 7m 9m 13m", "", "7b9b13"],
699
+ ["1P 3M 5P 7m 9m 9A", "", "7b9#9"],
700
+ ["1P 3M 5P 9M", "", "Madd9 2 add9 add2"],
701
+ ["1P 3M 5P 9m", "", "Maddb9"],
702
+ ["1P 3M 5d", "", "Mb5"],
703
+ ["1P 3M 5d 6M 7m 9M", "", "13b5"],
704
+ ["1P 3M 5d 7M", "", "M7b5"],
705
+ ["1P 3M 5d 7M 9M", "", "M9b5"],
706
+ ["1P 3M 5d 7m", "", "7b5"],
707
+ ["1P 3M 5d 7m 9M", "", "9b5"],
708
+ ["1P 3M 7m", "", "7no5"],
709
+ ["1P 3M 7m 13m", "", "7b13"],
710
+ ["1P 3M 7m 9M", "", "9no5"],
711
+ ["1P 3M 7m 9M 13M", "", "13no5"],
712
+ ["1P 3M 7m 9M 13m", "", "9b13"],
713
+ ["1P 3m 4P 5P", "", "madd4"],
714
+ ["1P 3m 5P 6m 7M", "", "mMaj7b6"],
715
+ ["1P 3m 5P 6m 7M 9M", "", "mMaj9b6"],
716
+ ["1P 3m 5P 7m 11P", "", "m7add11 m7add4"],
717
+ ["1P 3m 5P 9M", "", "madd9"],
718
+ ["1P 3m 5d 6M 7M", "", "o7M7"],
719
+ ["1P 3m 5d 7M", "", "oM7"],
720
+ ["1P 3m 6m 7M", "", "mb6M7"],
721
+ ["1P 3m 6m 7m", "", "m7#5"],
722
+ ["1P 3m 6m 7m 9M", "", "m9#5"],
723
+ ["1P 3m 5A 7m 9M 11P", "", "m11A"],
724
+ ["1P 3m 6m 9m", "", "mb6b9"],
725
+ ["1P 2M 3m 5d 7m", "", "m9b5"],
726
+ ["1P 4P 5A 7M", "", "M7#5sus4"],
727
+ ["1P 4P 5A 7M 9M", "", "M9#5sus4"],
728
+ ["1P 4P 5A 7m", "", "7#5sus4"],
729
+ ["1P 4P 5P 7M", "", "M7sus4"],
730
+ ["1P 4P 5P 7M 9M", "", "M9sus4"],
731
+ ["1P 4P 5P 7m 9M", "", "9sus4 9sus"],
732
+ ["1P 4P 5P 7m 9M 13M", "", "13sus4 13sus"],
733
+ ["1P 4P 5P 7m 9m 13m", "", "7sus4b9b13 7b9b13sus4"],
734
+ ["1P 4P 7m 10m", "", "4 quartal"],
735
+ ["1P 5P 7m 9m 11P", "", "11b9"]
736
+ ];
737
+ var data_default$2 = CHORDS;
738
+ var NoChordType = {
739
+ ...EmptyPcset,
740
+ name: "",
741
+ quality: "Unknown",
742
+ intervals: [],
743
+ aliases: []
744
+ };
745
+ var dictionary = [];
746
+ var index$2 = {};
747
+ function get$4(type) {
748
+ return index$2[type] || NoChordType;
749
+ }
750
+ function add$2(intervals, aliases, fullName) {
751
+ const quality = getQuality(intervals);
752
+ const chord = {
753
+ ...get$5(intervals),
754
+ name: fullName || "",
755
+ quality,
756
+ intervals,
757
+ aliases
758
+ };
759
+ dictionary.push(chord);
760
+ if (chord.name) {
761
+ index$2[chord.name] = chord;
762
+ }
763
+ index$2[chord.setNum] = chord;
764
+ index$2[chord.chroma] = chord;
765
+ chord.aliases.forEach((alias) => addAlias$1(chord, alias));
766
+ }
767
+ function addAlias$1(chord, alias) {
768
+ index$2[alias] = chord;
769
+ }
770
+ function getQuality(intervals) {
771
+ const has = (interval2) => intervals.indexOf(interval2) !== -1;
772
+ return has("5A") ? "Augmented" : has("3M") ? "Major" : has("5d") ? "Diminished" : has("3m") ? "Minor" : "Unknown";
773
+ }
774
+ data_default$2.forEach(
775
+ ([ivls, fullName, names2]) => add$2(ivls.split(" "), names2.split(" "), fullName)
776
+ );
777
+ dictionary.sort((a, b) => a.setNum - b.setNum);
778
+ var subtract = combinator((a, b) => [a[0] - b[0], a[1] - b[1]]);
779
+ function combinator(fn) {
780
+ return (a, b) => {
781
+ const coordA = interval(a).coord;
782
+ const coordB = interval(b).coord;
783
+ if (coordA && coordB) {
784
+ const coord = fn(coordA, coordB);
785
+ return coordToInterval(coord).name;
786
+ }
787
+ };
788
+ }
789
+ var SCALES = [
790
+ // Basic scales
791
+ ["1P 2M 3M 5P 6M", "major pentatonic", "pentatonic"],
792
+ ["1P 2M 3M 4P 5P 6M 7M", "major", "ionian"],
793
+ ["1P 2M 3m 4P 5P 6m 7m", "minor", "aeolian"],
794
+ // Jazz common scales
795
+ ["1P 2M 3m 3M 5P 6M", "major blues"],
796
+ ["1P 3m 4P 5d 5P 7m", "minor blues", "blues"],
797
+ ["1P 2M 3m 4P 5P 6M 7M", "melodic minor"],
798
+ ["1P 2M 3m 4P 5P 6m 7M", "harmonic minor"],
799
+ ["1P 2M 3M 4P 5P 6M 7m 7M", "bebop"],
800
+ ["1P 2M 3m 4P 5d 6m 6M 7M", "diminished", "whole-half diminished"],
801
+ // Modes
802
+ ["1P 2M 3m 4P 5P 6M 7m", "dorian"],
803
+ ["1P 2M 3M 4A 5P 6M 7M", "lydian"],
804
+ ["1P 2M 3M 4P 5P 6M 7m", "mixolydian", "dominant"],
805
+ ["1P 2m 3m 4P 5P 6m 7m", "phrygian"],
806
+ ["1P 2m 3m 4P 5d 6m 7m", "locrian"],
807
+ // 5-note scales
808
+ ["1P 3M 4P 5P 7M", "ionian pentatonic"],
809
+ ["1P 3M 4P 5P 7m", "mixolydian pentatonic", "indian"],
810
+ ["1P 2M 4P 5P 6M", "ritusen"],
811
+ ["1P 2M 4P 5P 7m", "egyptian"],
812
+ ["1P 3M 4P 5d 7m", "neopolitan major pentatonic"],
813
+ ["1P 3m 4P 5P 6m", "vietnamese 1"],
814
+ ["1P 2m 3m 5P 6m", "pelog"],
815
+ ["1P 2m 4P 5P 6m", "kumoijoshi"],
816
+ ["1P 2M 3m 5P 6m", "hirajoshi"],
817
+ ["1P 2m 4P 5d 7m", "iwato"],
818
+ ["1P 2m 4P 5P 7m", "in-sen"],
819
+ ["1P 3M 4A 5P 7M", "lydian pentatonic", "chinese"],
820
+ ["1P 3m 4P 6m 7m", "malkos raga"],
821
+ ["1P 3m 4P 5d 7m", "locrian pentatonic", "minor seven flat five pentatonic"],
822
+ ["1P 3m 4P 5P 7m", "minor pentatonic", "vietnamese 2"],
823
+ ["1P 3m 4P 5P 6M", "minor six pentatonic"],
824
+ ["1P 2M 3m 5P 6M", "flat three pentatonic", "kumoi"],
825
+ ["1P 2M 3M 5P 6m", "flat six pentatonic"],
826
+ ["1P 2m 3M 5P 6M", "scriabin"],
827
+ ["1P 3M 5d 6m 7m", "whole tone pentatonic"],
828
+ ["1P 3M 4A 5A 7M", "lydian #5P pentatonic"],
829
+ ["1P 3M 4A 5P 7m", "lydian dominant pentatonic"],
830
+ ["1P 3m 4P 5P 7M", "minor #7M pentatonic"],
831
+ ["1P 3m 4d 5d 7m", "super locrian pentatonic"],
832
+ // 6-note scales
833
+ ["1P 2M 3m 4P 5P 7M", "minor hexatonic"],
834
+ ["1P 2A 3M 5P 5A 7M", "augmented"],
835
+ ["1P 2M 4P 5P 6M 7m", "piongio"],
836
+ ["1P 2m 3M 4A 6M 7m", "prometheus neopolitan"],
837
+ ["1P 2M 3M 4A 6M 7m", "prometheus"],
838
+ ["1P 2m 3M 5d 6m 7m", "mystery #1"],
839
+ ["1P 2m 3M 4P 5A 6M", "six tone symmetric"],
840
+ ["1P 2M 3M 4A 5A 6A", "whole tone", "messiaen's mode #1"],
841
+ ["1P 2m 4P 4A 5P 7M", "messiaen's mode #5"],
842
+ // 7-note scales
843
+ ["1P 2M 3M 4P 5d 6m 7m", "locrian major", "arabian"],
844
+ ["1P 2m 3M 4A 5P 6m 7M", "double harmonic lydian"],
845
+ [
846
+ "1P 2m 2A 3M 4A 6m 7m",
847
+ "altered",
848
+ "super locrian",
849
+ "diminished whole tone",
850
+ "pomeroy"
851
+ ],
852
+ ["1P 2M 3m 4P 5d 6m 7m", "locrian #2", "half-diminished", "aeolian b5"],
853
+ [
854
+ "1P 2M 3M 4P 5P 6m 7m",
855
+ "mixolydian b6",
856
+ "melodic minor fifth mode",
857
+ "hindu"
858
+ ],
859
+ ["1P 2M 3M 4A 5P 6M 7m", "lydian dominant", "lydian b7", "overtone"],
860
+ ["1P 2M 3M 4A 5A 6M 7M", "lydian augmented"],
861
+ [
862
+ "1P 2m 3m 4P 5P 6M 7m",
863
+ "dorian b2",
864
+ "phrygian #6",
865
+ "melodic minor second mode"
866
+ ],
867
+ [
868
+ "1P 2m 3m 4d 5d 6m 7d",
869
+ "ultralocrian",
870
+ "superlocrian bb7",
871
+ "superlocrian diminished"
872
+ ],
873
+ ["1P 2m 3m 4P 5d 6M 7m", "locrian 6", "locrian natural 6", "locrian sharp 6"],
874
+ ["1P 2A 3M 4P 5P 5A 7M", "augmented heptatonic"],
875
+ // Source https://en.wikipedia.org/wiki/Ukrainian_Dorian_scale
876
+ [
877
+ "1P 2M 3m 4A 5P 6M 7m",
878
+ "dorian #4",
879
+ "ukrainian dorian",
880
+ "romanian minor",
881
+ "altered dorian"
882
+ ],
883
+ ["1P 2M 3m 4A 5P 6M 7M", "lydian diminished"],
884
+ ["1P 2M 3M 4A 5A 7m 7M", "leading whole tone"],
885
+ ["1P 2M 3M 4A 5P 6m 7m", "lydian minor"],
886
+ ["1P 2m 3M 4P 5P 6m 7m", "phrygian dominant", "spanish", "phrygian major"],
887
+ ["1P 2m 3m 4P 5P 6m 7M", "balinese"],
888
+ ["1P 2m 3m 4P 5P 6M 7M", "neopolitan major"],
889
+ ["1P 2M 3M 4P 5P 6m 7M", "harmonic major"],
890
+ ["1P 2m 3M 4P 5P 6m 7M", "double harmonic major", "gypsy"],
891
+ ["1P 2M 3m 4A 5P 6m 7M", "hungarian minor"],
892
+ ["1P 2A 3M 4A 5P 6M 7m", "hungarian major"],
893
+ ["1P 2m 3M 4P 5d 6M 7m", "oriental"],
894
+ ["1P 2m 3m 3M 4A 5P 7m", "flamenco"],
895
+ ["1P 2m 3m 4A 5P 6m 7M", "todi raga"],
896
+ ["1P 2m 3M 4P 5d 6m 7M", "persian"],
897
+ ["1P 2m 3M 5d 6m 7m 7M", "enigmatic"],
898
+ [
899
+ "1P 2M 3M 4P 5A 6M 7M",
900
+ "major augmented",
901
+ "major #5",
902
+ "ionian augmented",
903
+ "ionian #5"
904
+ ],
905
+ ["1P 2A 3M 4A 5P 6M 7M", "lydian #9"],
906
+ // 8-note scales
907
+ ["1P 2m 2M 4P 4A 5P 6m 7M", "messiaen's mode #4"],
908
+ ["1P 2m 3M 4P 4A 5P 6m 7M", "purvi raga"],
909
+ ["1P 2m 3m 3M 4P 5P 6m 7m", "spanish heptatonic"],
910
+ ["1P 2M 3m 3M 4P 5P 6M 7m", "bebop minor"],
911
+ ["1P 2M 3M 4P 5P 5A 6M 7M", "bebop major"],
912
+ ["1P 2m 3m 4P 5d 5P 6m 7m", "bebop locrian"],
913
+ ["1P 2M 3m 4P 5P 6m 7m 7M", "minor bebop"],
914
+ ["1P 2M 3M 4P 5d 5P 6M 7M", "ichikosucho"],
915
+ ["1P 2M 3m 4P 5P 6m 6M 7M", "minor six diminished"],
916
+ [
917
+ "1P 2m 3m 3M 4A 5P 6M 7m",
918
+ "half-whole diminished",
919
+ "dominant diminished",
920
+ "messiaen's mode #2"
921
+ ],
922
+ ["1P 3m 3M 4P 5P 6M 7m 7M", "kafi raga"],
923
+ ["1P 2M 3M 4P 4A 5A 6A 7M", "messiaen's mode #6"],
924
+ // 9-note scales
925
+ ["1P 2M 3m 3M 4P 5d 5P 6M 7m", "composite blues"],
926
+ ["1P 2M 3m 3M 4A 5P 6m 7m 7M", "messiaen's mode #3"],
927
+ // 10-note scales
928
+ ["1P 2m 2M 3m 4P 4A 5P 6m 6M 7M", "messiaen's mode #7"],
929
+ // 12-note scales
930
+ ["1P 2m 2M 3m 3M 4P 5d 5P 6m 6M 7m 7M", "chromatic"]
931
+ ];
932
+ var data_default$1 = SCALES;
933
+ var NoScaleType = {
934
+ ...EmptyPcset,
935
+ intervals: [],
936
+ aliases: []
937
+ };
938
+ var index$1 = {};
939
+ function get$3(type) {
940
+ return index$1[type] || NoScaleType;
941
+ }
942
+ function add$1(intervals, name, aliases = []) {
943
+ const scale = { ...get$5(intervals), name, intervals, aliases };
944
+ index$1[scale.name] = scale;
945
+ index$1[scale.setNum] = scale;
946
+ index$1[scale.chroma] = scale;
947
+ scale.aliases.forEach((alias) => addAlias(scale, alias));
948
+ return scale;
949
+ }
950
+ function addAlias(scale, alias) {
951
+ index$1[alias] = scale;
952
+ }
953
+ data_default$1.forEach(
954
+ ([ivls, name, ...aliases]) => add$1(ivls.split(" "), name, aliases)
955
+ );
956
+ var NoChord = {
957
+ empty: true,
958
+ name: "",
959
+ symbol: "",
960
+ root: "",
961
+ bass: "",
962
+ rootDegree: 0,
963
+ type: "",
964
+ tonic: null,
965
+ setNum: NaN,
966
+ quality: "Unknown",
967
+ chroma: "",
968
+ normalized: "",
969
+ aliases: [],
970
+ notes: [],
971
+ intervals: []
972
+ };
973
+ function tokenize(name) {
974
+ const [letter, acc, oct, type] = tokenizeNote(name);
975
+ if (letter === "") {
976
+ return tokenizeBass("", name);
977
+ } else if (letter === "A" && type === "ug") {
978
+ return tokenizeBass("", "aug");
979
+ } else {
980
+ return tokenizeBass(letter + acc, oct + type);
981
+ }
982
+ }
983
+ function tokenizeBass(note2, chord2) {
984
+ const split = chord2.split("/");
985
+ if (split.length === 1) {
986
+ return [note2, split[0], ""];
987
+ }
988
+ const [letter, acc, oct, type] = tokenizeNote(split[1]);
989
+ if (letter !== "" && oct === "" && type === "") {
990
+ return [note2, split[0], letter + acc];
991
+ } else {
992
+ return [note2, chord2, ""];
993
+ }
994
+ }
995
+ function get$2(src) {
996
+ if (Array.isArray(src)) {
997
+ return getChord(src[1] || "", src[0], src[2]);
998
+ } else if (src === "") {
999
+ return NoChord;
1000
+ } else {
1001
+ const [tonic, type, bass] = tokenize(src);
1002
+ const chord2 = getChord(type, tonic, bass);
1003
+ return chord2.empty ? getChord(src) : chord2;
1004
+ }
1005
+ }
1006
+ function getChord(typeName, optionalTonic, optionalBass) {
1007
+ const type = get$4(typeName);
1008
+ const tonic = note(optionalTonic || "");
1009
+ const bass = note(optionalBass || "");
1010
+ if (type.empty || optionalTonic && tonic.empty || optionalBass && bass.empty) {
1011
+ return NoChord;
1012
+ }
1013
+ const bassInterval = distance(tonic.pc, bass.pc);
1014
+ const bassIndex = type.intervals.indexOf(bassInterval);
1015
+ const hasRoot = bassIndex >= 0;
1016
+ const root = hasRoot ? bass : note("");
1017
+ const rootDegree = bassIndex === -1 ? NaN : bassIndex + 1;
1018
+ const hasBass = bass.pc && bass.pc !== tonic.pc;
1019
+ const intervals = Array.from(type.intervals);
1020
+ if (hasRoot) {
1021
+ for (let i = 1; i < rootDegree; i++) {
1022
+ const num = intervals[0][0];
1023
+ const quality = intervals[0][1];
1024
+ const newNum = parseInt(num, 10) + 7;
1025
+ intervals.push(`${newNum}${quality}`);
1026
+ intervals.shift();
1027
+ }
1028
+ } else if (hasBass) {
1029
+ const ivl = subtract(distance(tonic.pc, bass.pc), "8P");
1030
+ if (ivl) intervals.unshift(ivl);
1031
+ }
1032
+ const notes2 = tonic.empty ? [] : intervals.map((i) => transpose(tonic.pc, i));
1033
+ typeName = type.aliases.indexOf(typeName) !== -1 ? typeName : type.aliases[0];
1034
+ const symbol = `${tonic.empty ? "" : tonic.pc}${typeName}${hasRoot && rootDegree > 1 ? "/" + root.pc : hasBass ? "/" + bass.pc : ""}`;
1035
+ const name = `${optionalTonic ? tonic.pc + " " : ""}${type.name}${hasRoot && rootDegree > 1 ? " over " + root.pc : hasBass ? " over " + bass.pc : ""}`;
1036
+ return {
1037
+ ...type,
1038
+ name,
1039
+ symbol,
1040
+ tonic: tonic.pc,
1041
+ type: type.name,
1042
+ root: root.pc,
1043
+ bass: hasBass ? bass.pc : "",
1044
+ intervals,
1045
+ rootDegree,
1046
+ notes: notes2
1047
+ };
1048
+ }
1049
+ var DATA = [
1050
+ [
1051
+ 0.125,
1052
+ "dl",
1053
+ ["large", "duplex longa", "maxima", "octuple", "octuple whole"]
1054
+ ],
1055
+ [0.25, "l", ["long", "longa"]],
1056
+ [0.5, "d", ["double whole", "double", "breve"]],
1057
+ [1, "w", ["whole", "semibreve"]],
1058
+ [2, "h", ["half", "minim"]],
1059
+ [4, "q", ["quarter", "crotchet"]],
1060
+ [8, "e", ["eighth", "quaver"]],
1061
+ [16, "s", ["sixteenth", "semiquaver"]],
1062
+ [32, "t", ["thirty-second", "demisemiquaver"]],
1063
+ [64, "sf", ["sixty-fourth", "hemidemisemiquaver"]],
1064
+ [128, "h", ["hundred twenty-eighth"]],
1065
+ [256, "th", ["two hundred fifty-sixth"]]
1066
+ ];
1067
+ var data_default = DATA;
1068
+ data_default.forEach(
1069
+ ([denominator, shorthand, names2]) => add()
1070
+ );
1071
+ function add(denominator, shorthand, names2) {
1072
+ }
1073
+ function midiToFreq(midi, tuning = 440) {
1074
+ return Math.pow(2, (midi - 69) / 12) * tuning;
1075
+ }
1076
+ var get$1 = note;
1077
+ var freq = (note2) => get$1(note2).freq;
1078
+ var MODES = [
1079
+ [0, 2773, 0, "ionian", "", "Maj7", "major"],
1080
+ [1, 2902, 2, "dorian", "m", "m7"],
1081
+ [2, 3418, 4, "phrygian", "m", "m7"],
1082
+ [3, 2741, -1, "lydian", "", "Maj7"],
1083
+ [4, 2774, 1, "mixolydian", "", "7"],
1084
+ [5, 2906, 3, "aeolian", "m", "m7", "minor"],
1085
+ [6, 3434, 5, "locrian", "dim", "m7b5"]
1086
+ ];
1087
+ var NoMode = {
1088
+ ...EmptyPcset,
1089
+ name: "",
1090
+ alt: 0,
1091
+ modeNum: NaN,
1092
+ triad: "",
1093
+ seventh: "",
1094
+ aliases: []
1095
+ };
1096
+ var modes = MODES.map(toMode);
1097
+ var index = {};
1098
+ modes.forEach((mode2) => {
1099
+ index[mode2.name] = mode2;
1100
+ mode2.aliases.forEach((alias) => {
1101
+ index[alias] = mode2;
1102
+ });
1103
+ });
1104
+ function get(name) {
1105
+ return typeof name === "string" ? index[name.toLowerCase()] || NoMode : name && name.name ? get(name.name) : NoMode;
1106
+ }
1107
+ function toMode(mode2) {
1108
+ const [modeNum, setNum, alt, name, triad, seventh, alias] = mode2;
1109
+ const aliases = alias ? [alias] : [];
1110
+ const chroma = Number(setNum).toString(2);
1111
+ const intervals = get$3(name).intervals;
1112
+ return {
1113
+ empty: false,
1114
+ intervals,
1115
+ modeNum,
1116
+ chroma,
1117
+ normalized: chroma,
1118
+ name,
1119
+ setNum,
1120
+ alt,
1121
+ triad,
1122
+ seventh,
1123
+ aliases
1124
+ };
1125
+ }
1126
+ function chords(chords2) {
1127
+ return (modeName, tonic) => {
1128
+ const mode2 = get(modeName);
1129
+ if (mode2.empty) return [];
1130
+ const triads2 = rotate(mode2.modeNum, chords2);
1131
+ const tonics = mode2.intervals.map((i) => transpose(tonic, i));
1132
+ return triads2.map((triad, i) => tonics[i] + triad);
1133
+ };
1134
+ }
1135
+ chords(MODES.map((x) => x[4]));
1136
+ chords(MODES.map((x) => x[5]));
1137
+ function midiToFrequency(midi) {
1138
+ return midiToFreq(midi);
1139
+ }
1140
+ function noteToFrequency(note2) {
1141
+ const freq$1 = freq(note2);
1142
+ if (freq$1 === null) {
1143
+ console.warn(`Invalid note: ${note2}, defaulting to A4 (440Hz)`);
1144
+ return 440;
1145
+ }
1146
+ return freq$1;
1147
+ }
1148
+ function resolveChordNotes(notes) {
1149
+ if (Array.isArray(notes)) {
1150
+ return notes;
1151
+ }
1152
+ const [chordName, octaveStr] = notes.split(":");
1153
+ const octave = octaveStr ? parseInt(octaveStr, 10) : 3;
1154
+ const chord = get$2(chordName);
1155
+ if (!chord.notes.length) {
1156
+ console.warn(`Unknown chord: ${chordName}, defaulting to root note`);
1157
+ return [chordName + octave];
1158
+ }
1159
+ const rootPc = chord.notes[0];
1160
+ const noteOrder = ["C", "D", "E", "F", "G", "A", "B"];
1161
+ const rootIndex = noteOrder.findIndex((n) => rootPc.startsWith(n));
1162
+ return chord.notes.map((note2) => {
1163
+ const noteIndex = noteOrder.findIndex((n) => note2.startsWith(n));
1164
+ const noteOctave = noteIndex < rootIndex ? octave + 1 : octave;
1165
+ return note2 + noteOctave;
1166
+ });
1167
+ }
1168
+ function line(start, end, steps) {
1169
+ if (steps < 1) {
1170
+ throw new Error("Line must have at least 1 step");
1171
+ }
1172
+ const increment = (end - start) / steps;
1173
+ return Array.from({ length: steps }, (_, i) => start + increment * i);
1174
+ }
1175
+ function mirror(values) {
1176
+ return [...values, ...values.toReversed()];
1177
+ }
1178
+ function resolveCutoff(cutoff, stepIndex) {
1179
+ if (typeof cutoff === "number") {
1180
+ return midiToFrequency(cutoff);
1181
+ }
1182
+ const values = line(cutoff.from, cutoff.to, cutoff.steps);
1183
+ const sequence = cutoff.mirror ? mirror(values) : values;
1184
+ const effectiveStep = cutoff.step ?? stepIndex;
1185
+ return midiToFrequency(sequence[effectiveStep % sequence.length]);
1186
+ }
1187
+ const SYNTH_PRESETS = {
1188
+ /**
1189
+ * Pure sine wave - clean, fundamental tone
1190
+ */
1191
+ sine: {
1192
+ oscillator: "sine",
1193
+ filter: { type: "lowpass", cutoff: 135.08, resonance: 1 },
1194
+ voices: { count: 1, detune: 0, spread: 0 }
1195
+ },
1196
+ /**
1197
+ * Sawtooth wave - bright, harmonically rich
1198
+ */
1199
+ saw: {
1200
+ oscillator: "sawtooth",
1201
+ filter: { type: "lowpass", cutoff: 135.08, resonance: 1 },
1202
+ voices: { count: 1, detune: 0, spread: 0 }
1203
+ },
1204
+ /**
1205
+ * Square wave - hollow, clarinet-like
1206
+ */
1207
+ square: {
1208
+ oscillator: "square",
1209
+ filter: { type: "lowpass", cutoff: 135.08, resonance: 1 },
1210
+ voices: { count: 1, detune: 0, spread: 0 }
1211
+ },
1212
+ /**
1213
+ * Triangle wave - soft, flute-like
1214
+ */
1215
+ tri: {
1216
+ oscillator: "triangle",
1217
+ filter: { type: "lowpass", cutoff: 135.08, resonance: 1 },
1218
+ voices: { count: 1, detune: 0, spread: 0 }
1219
+ },
1220
+ /**
1221
+ * Prophet - Sequential Circuits Prophet-5 inspired
1222
+ * Warm analog sound with detuned voices and resonant filter
1223
+ * Great for leads and pads
1224
+ */
1225
+ prophet: {
1226
+ oscillator: "sawtooth",
1227
+ filter: { type: "lowpass", cutoff: 102.23, resonance: 4 },
1228
+ voices: { count: 3, detune: 12, spread: 0.5 }
1229
+ },
1230
+ /**
1231
+ * Hollow - Ethereal pad sound
1232
+ * Filtered square wave with soft resonance
1233
+ * Good for atmospheric backgrounds
1234
+ */
1235
+ hollow: {
1236
+ oscillator: "square",
1237
+ filter: { type: "bandpass", cutoff: 86.37, resonance: 2 },
1238
+ voices: { count: 2, detune: 8, spread: 0.7 }
1239
+ },
1240
+ /**
1241
+ * Dark Ambience - Deep atmospheric pad
1242
+ * Low-passed sawtooth with multiple detuned voices
1243
+ */
1244
+ dark_ambience: {
1245
+ oscillator: "sawtooth",
1246
+ filter: { type: "lowpass", cutoff: 79.35, resonance: 3 },
1247
+ voices: { count: 4, detune: 15, spread: 0.8 }
1248
+ },
1249
+ /**
1250
+ * Bass - Punchy bass sound
1251
+ * Filtered sawtooth with slight detune for thickness
1252
+ */
1253
+ bass: {
1254
+ oscillator: "sawtooth",
1255
+ filter: { type: "lowpass", cutoff: 74.37, resonance: 5 },
1256
+ voices: { count: 2, detune: 5, spread: 0.2 }
1257
+ },
1258
+ /**
1259
+ * Pluck - Bright plucked string sound
1260
+ * Square wave with high cutoff for attack brightness
1261
+ */
1262
+ pluck: {
1263
+ oscillator: "square",
1264
+ filter: { type: "lowpass", cutoff: 111.08, resonance: 2 },
1265
+ voices: { count: 2, detune: 6, spread: 0.3 }
1266
+ }
1267
+ };
1268
+ function getSynthPreset(type) {
1269
+ return SYNTH_PRESETS[type];
1270
+ }
1271
+ const DEFAULT_SYNTH_CONFIG = SYNTH_PRESETS.sine;
1272
+ const SynthContext = createContext(null);
1273
+ function Synth({
1274
+ type,
1275
+ oscillator,
1276
+ filter,
1277
+ voices,
1278
+ children
1279
+ }) {
1280
+ const config = useMemo(() => {
1281
+ const preset = getSynthPreset(type);
1282
+ return {
1283
+ oscillator: oscillator ?? preset.oscillator,
1284
+ filter: {
1285
+ type: filter?.type ?? preset.filter.type,
1286
+ cutoff: filter?.cutoff ?? preset.filter.cutoff,
1287
+ resonance: filter?.resonance ?? preset.filter.resonance
1288
+ },
1289
+ voices: {
1290
+ count: voices?.count ?? preset.voices.count,
1291
+ detune: voices?.detune ?? preset.voices.detune,
1292
+ spread: voices?.spread ?? preset.voices.spread
1293
+ }
1294
+ };
1295
+ }, [type, oscillator, filter, voices]);
1296
+ return /* @__PURE__ */ jsx(SynthContext.Provider, { value: config, children });
1297
+ }
1298
+ function useSynth() {
1299
+ const ctx = useContext(SynthContext);
1300
+ return ctx ?? DEFAULT_SYNTH_CONFIG;
1301
+ }
1302
+ function Note({
1303
+ note: note2,
1304
+ amp = 0.3,
1305
+ oscillator,
1306
+ filter,
1307
+ voices,
1308
+ attack = ADSR_DEFAULTS.attack,
1309
+ attack_level = ADSR_DEFAULTS.attack_level,
1310
+ decay = ADSR_DEFAULTS.decay,
1311
+ decay_level,
1312
+ sustain = ADSR_DEFAULTS.sustain,
1313
+ sustain_level = ADSR_DEFAULTS.sustain_level,
1314
+ release = ADSR_DEFAULTS.release,
1315
+ __stepIndex
1316
+ }) {
1317
+ const uniqueId = useId();
1318
+ const { audioContext, scheduler } = useTrack();
1319
+ const scheduleNote = useScheduleNote();
1320
+ const synthConfig = useSynth();
1321
+ const oscillatorType = oscillator ?? synthConfig.oscillator;
1322
+ const filterType = filter?.type ?? synthConfig.filter.type;
1323
+ const filterCutoff = resolveCutoff(
1324
+ filter?.cutoff ?? synthConfig.filter.cutoff,
1325
+ __stepIndex ?? 0
1326
+ );
1327
+ const filterResonance = filter?.resonance ?? synthConfig.filter.resonance;
1328
+ const voiceCount = voices?.count ?? synthConfig.voices.count;
1329
+ const voiceDetune = voices?.detune ?? synthConfig.voices.detune;
1330
+ const voiceSpread = voices?.spread ?? synthConfig.voices.spread;
1331
+ const frequency = typeof note2 === "number" ? note2 : noteToFrequency(note2);
1332
+ const adsrProps = {
1333
+ attack,
1334
+ attack_level,
1335
+ decay,
1336
+ decay_level,
1337
+ sustain,
1338
+ sustain_level,
1339
+ release
1340
+ };
1341
+ useEffect(() => {
1342
+ const playNote = (audioTime) => {
1343
+ const gainNode = audioContext.createGain();
1344
+ if (filterCutoff < 2e4) {
1345
+ const filter2 = audioContext.createBiquadFilter();
1346
+ filter2.type = filterType;
1347
+ filter2.frequency.value = filterCutoff;
1348
+ filter2.Q.value = filterResonance;
1349
+ gainNode.connect(filter2);
1350
+ filter2.connect(audioContext.destination);
1351
+ } else {
1352
+ gainNode.connect(audioContext.destination);
1353
+ }
1354
+ const endTime = applyADSREnvelope(
1355
+ gainNode,
1356
+ audioTime,
1357
+ adsrProps,
1358
+ amp / voiceCount,
1359
+ // Normalize amplitude across voices
1360
+ (beats) => scheduler.beatsToSeconds(beats)
1361
+ );
1362
+ for (let i = 0; i < voiceCount; i++) {
1363
+ const oscillator2 = audioContext.createOscillator();
1364
+ oscillator2.type = oscillatorType;
1365
+ oscillator2.frequency.value = frequency;
1366
+ if (voiceCount > 1 && voiceDetune > 0) {
1367
+ const detuneOffset = (i / (voiceCount - 1) - 0.5) * voiceDetune;
1368
+ oscillator2.detune.value = detuneOffset;
1369
+ }
1370
+ if (voiceCount > 1 && voiceSpread > 0) {
1371
+ const panner = audioContext.createStereoPanner();
1372
+ const panValue = (i / (voiceCount - 1) - 0.5) * 2 * voiceSpread;
1373
+ panner.pan.value = panValue;
1374
+ oscillator2.connect(panner);
1375
+ panner.connect(gainNode);
1376
+ } else {
1377
+ oscillator2.connect(gainNode);
1378
+ }
1379
+ oscillator2.start(audioTime);
1380
+ oscillator2.stop(endTime + 0.01);
1381
+ }
1382
+ };
1383
+ scheduleNote.scheduleNote(uniqueId, playNote, __stepIndex);
1384
+ return () => {
1385
+ scheduleNote.unscheduleNote(uniqueId);
1386
+ };
1387
+ }, [
1388
+ note2,
1389
+ frequency,
1390
+ amp,
1391
+ oscillatorType,
1392
+ filterCutoff,
1393
+ filterResonance,
1394
+ filterType,
1395
+ voiceCount,
1396
+ voiceDetune,
1397
+ voiceSpread,
1398
+ adsrProps,
1399
+ audioContext,
1400
+ scheduler,
1401
+ scheduleNote,
1402
+ __stepIndex,
1403
+ uniqueId
1404
+ ]);
1405
+ return null;
1406
+ }
1407
+ const sampleCache = /* @__PURE__ */ new Map();
1408
+ function getSamplesDir() {
1409
+ try {
1410
+ const require2 = createRequire(import.meta.url);
1411
+ const packageMain = require2.resolve("@react-synth/synth");
1412
+ const packageDir = dirname(packageMain);
1413
+ return join(packageDir, "..", "src", "samples");
1414
+ } catch {
1415
+ const currentFile = fileURLToPath(import.meta.url);
1416
+ const currentDir = dirname(currentFile);
1417
+ return join(currentDir, "..", "samples");
1418
+ }
1419
+ }
1420
+ const samplesDir = getSamplesDir();
1421
+ function getSamplePath(name) {
1422
+ return join(samplesDir, `${name}.flac`);
1423
+ }
1424
+ async function loadSample(audioContext, name) {
1425
+ if (sampleCache.has(name)) {
1426
+ return sampleCache.get(name);
1427
+ }
1428
+ const path = getSamplePath(name);
1429
+ const fileData = await readFile(path);
1430
+ const audioBuffer = await audioContext.decodeAudioData(
1431
+ fileData.buffer
1432
+ );
1433
+ sampleCache.set(name, audioBuffer);
1434
+ return audioBuffer;
1435
+ }
1436
+ function Sample({
1437
+ name,
1438
+ amp = 1,
1439
+ cutoff,
1440
+ rate = 1,
1441
+ pan = 0,
1442
+ __stepIndex
1443
+ }) {
1444
+ const uniqueId = useId();
1445
+ const { audioContext } = useTrack();
1446
+ const scheduleNote = useScheduleNote();
1447
+ const filterCutoff = cutoff !== void 0 ? resolveCutoff(cutoff, __stepIndex ?? 0) : 2e4;
1448
+ useEffect(() => {
1449
+ let buffer = null;
1450
+ loadSample(audioContext, name).then((loaded) => {
1451
+ buffer = loaded;
1452
+ });
1453
+ const playSample = (audioTime) => {
1454
+ if (!buffer) return;
1455
+ const source = audioContext.createBufferSource();
1456
+ source.buffer = buffer;
1457
+ source.playbackRate.value = rate;
1458
+ let currentNode = source;
1459
+ if (filterCutoff < 2e4) {
1460
+ const filter = audioContext.createBiquadFilter();
1461
+ filter.type = "lowpass";
1462
+ filter.frequency.value = filterCutoff;
1463
+ currentNode.connect(filter);
1464
+ currentNode = filter;
1465
+ }
1466
+ if (pan !== 0) {
1467
+ const panner = audioContext.createStereoPanner();
1468
+ panner.pan.value = pan;
1469
+ currentNode.connect(panner);
1470
+ currentNode = panner;
1471
+ }
1472
+ const gainNode = audioContext.createGain();
1473
+ gainNode.gain.value = amp;
1474
+ currentNode.connect(gainNode);
1475
+ gainNode.connect(audioContext.destination);
1476
+ source.start(audioTime);
1477
+ };
1478
+ scheduleNote.scheduleNote(uniqueId, playSample, __stepIndex);
1479
+ return () => {
1480
+ scheduleNote.unscheduleNote(uniqueId);
1481
+ };
1482
+ }, [
1483
+ audioContext,
1484
+ name,
1485
+ scheduleNote,
1486
+ uniqueId,
1487
+ __stepIndex,
1488
+ amp,
1489
+ filterCutoff,
1490
+ rate,
1491
+ pan
1492
+ ]);
1493
+ return null;
1494
+ }
1495
+ function Chord({
1496
+ notes,
1497
+ amp = 0.3,
1498
+ oscillator,
1499
+ filter,
1500
+ voices,
1501
+ attack = ADSR_DEFAULTS.attack,
1502
+ attack_level = ADSR_DEFAULTS.attack_level,
1503
+ decay = ADSR_DEFAULTS.decay,
1504
+ decay_level,
1505
+ sustain = ADSR_DEFAULTS.sustain,
1506
+ sustain_level = ADSR_DEFAULTS.sustain_level,
1507
+ release = ADSR_DEFAULTS.release,
1508
+ __stepIndex
1509
+ }) {
1510
+ const uniqueId = useId();
1511
+ const { audioContext, scheduler } = useTrack();
1512
+ const { scheduleNote, unscheduleNote } = useScheduleNote();
1513
+ const synthConfig = useSynth();
1514
+ const oscillatorType = oscillator ?? synthConfig.oscillator;
1515
+ const filterType = filter?.type ?? synthConfig.filter.type;
1516
+ const filterCutoff = resolveCutoff(
1517
+ filter?.cutoff ?? synthConfig.filter.cutoff,
1518
+ __stepIndex ?? 0
1519
+ );
1520
+ const filterResonance = filter?.resonance ?? synthConfig.filter.resonance;
1521
+ const voiceCount = voices?.count ?? synthConfig.voices.count;
1522
+ const voiceDetune = voices?.detune ?? synthConfig.voices.detune;
1523
+ const voiceSpread = voices?.spread ?? synthConfig.voices.spread;
1524
+ const resolvedNotes = resolveChordNotes(notes);
1525
+ const frequencies = resolvedNotes.map(
1526
+ (note2) => typeof note2 === "number" ? note2 : noteToFrequency(note2)
1527
+ );
1528
+ const adsrProps = {
1529
+ attack,
1530
+ attack_level,
1531
+ decay,
1532
+ decay_level,
1533
+ sustain,
1534
+ sustain_level,
1535
+ release
1536
+ };
1537
+ const totalOscillators = frequencies.length * voiceCount;
1538
+ useEffect(() => {
1539
+ const playChord = (audioTime) => {
1540
+ const gainNode = audioContext.createGain();
1541
+ if (filterCutoff < 2e4) {
1542
+ const filter2 = audioContext.createBiquadFilter();
1543
+ filter2.type = filterType;
1544
+ filter2.frequency.value = filterCutoff;
1545
+ filter2.Q.value = filterResonance;
1546
+ gainNode.connect(filter2);
1547
+ filter2.connect(audioContext.destination);
1548
+ } else {
1549
+ gainNode.connect(audioContext.destination);
1550
+ }
1551
+ const endTime = applyADSREnvelope(
1552
+ gainNode,
1553
+ audioTime,
1554
+ adsrProps,
1555
+ amp / totalOscillators,
1556
+ // Normalize amplitude across all oscillators
1557
+ (beats) => scheduler.beatsToSeconds(beats)
1558
+ );
1559
+ for (const frequency of frequencies) {
1560
+ for (let i = 0; i < voiceCount; i++) {
1561
+ const oscillator2 = audioContext.createOscillator();
1562
+ oscillator2.type = oscillatorType;
1563
+ oscillator2.frequency.value = frequency;
1564
+ if (voiceCount > 1 && voiceDetune > 0) {
1565
+ const detuneOffset = (i / (voiceCount - 1) - 0.5) * voiceDetune;
1566
+ oscillator2.detune.value = detuneOffset;
1567
+ }
1568
+ if (voiceCount > 1 && voiceSpread > 0) {
1569
+ const panner = audioContext.createStereoPanner();
1570
+ const panValue = (i / (voiceCount - 1) - 0.5) * 2 * voiceSpread;
1571
+ panner.pan.value = panValue;
1572
+ oscillator2.connect(panner);
1573
+ panner.connect(gainNode);
1574
+ } else {
1575
+ oscillator2.connect(gainNode);
1576
+ }
1577
+ oscillator2.start(audioTime);
1578
+ oscillator2.stop(endTime + 0.01);
1579
+ }
1580
+ }
1581
+ };
1582
+ scheduleNote(uniqueId, playChord, __stepIndex);
1583
+ return () => {
1584
+ unscheduleNote(uniqueId);
1585
+ };
1586
+ }, [
1587
+ notes,
1588
+ frequencies,
1589
+ amp,
1590
+ oscillatorType,
1591
+ filterCutoff,
1592
+ filterResonance,
1593
+ filterType,
1594
+ voiceCount,
1595
+ voiceDetune,
1596
+ voiceSpread,
1597
+ totalOscillators,
1598
+ adsrProps,
1599
+ audioContext,
1600
+ scheduler,
1601
+ scheduleNote,
1602
+ unscheduleNote,
1603
+ __stepIndex,
1604
+ uniqueId
1605
+ ]);
1606
+ return null;
1607
+ }
1608
+ function Sequence({
1609
+ interval: interval2,
1610
+ children,
1611
+ __stepIndex
1612
+ }) {
1613
+ const uniqueId = useId();
1614
+ const { scheduler } = useTrack();
1615
+ const { scheduleNote, unscheduleNote } = useScheduleNote();
1616
+ const childArray = Children.toArray(children);
1617
+ const totalSteps = childArray.length;
1618
+ const stepCallbacks = React.useRef(/* @__PURE__ */ new Map());
1619
+ const idToStepIndex = useRef(/* @__PURE__ */ new Map());
1620
+ useEffect(() => {
1621
+ const sequenceCallback = (audioTime, beatTime) => {
1622
+ for (let i = 0; i < totalSteps; i++) {
1623
+ const stepOffset = scheduler.beatsToSeconds(i * interval2);
1624
+ const stepAudioTime = audioTime + stepOffset;
1625
+ const stepBeatTime = beatTime + i * interval2;
1626
+ const callback = stepCallbacks.current.get(i);
1627
+ if (callback) {
1628
+ callback(stepAudioTime, stepBeatTime);
1629
+ }
1630
+ }
1631
+ };
1632
+ scheduleNote(`seq-${uniqueId}`, sequenceCallback, __stepIndex);
1633
+ return () => {
1634
+ unscheduleNote(`seq-${uniqueId}`);
1635
+ };
1636
+ }, [
1637
+ interval2,
1638
+ totalSteps,
1639
+ scheduler,
1640
+ uniqueId,
1641
+ scheduleNote,
1642
+ unscheduleNote,
1643
+ __stepIndex
1644
+ ]);
1645
+ const scheduleContextValue = useMemo(
1646
+ () => ({
1647
+ scheduleNote: (noteId, callback, stepIndex) => {
1648
+ if (stepIndex !== void 0) {
1649
+ idToStepIndex.current.set(noteId, stepIndex);
1650
+ stepCallbacks.current.set(stepIndex, callback);
1651
+ }
1652
+ },
1653
+ unscheduleNote: (noteId) => {
1654
+ const stepIndex = idToStepIndex.current.get(noteId);
1655
+ if (stepIndex !== void 0) {
1656
+ stepCallbacks.current.delete(stepIndex);
1657
+ idToStepIndex.current.delete(noteId);
1658
+ }
1659
+ }
1660
+ }),
1661
+ []
1662
+ );
1663
+ const childrenWithIndex = childArray.map((child, index2) => {
1664
+ if (isValidElement(child)) {
1665
+ return cloneElement(child, {
1666
+ key: `step-${index2}`,
1667
+ __stepIndex: index2
1668
+ });
1669
+ }
1670
+ return child;
1671
+ });
1672
+ return /* @__PURE__ */ jsx(ScheduleNoteContext.Provider, { value: scheduleContextValue, children: childrenWithIndex });
1673
+ }
1674
+ export {
1675
+ ADSR_DEFAULTS,
1676
+ Chord,
1677
+ DEFAULT_SYNTH_CONFIG,
1678
+ Loop,
1679
+ Note,
1680
+ SYNTH_PRESETS,
1681
+ Sample,
1682
+ ScheduleNoteContext,
1683
+ Scheduler,
1684
+ Sequence,
1685
+ Synth,
1686
+ Track,
1687
+ applyADSREnvelope,
1688
+ getScheduler,
1689
+ getSynthPreset,
1690
+ line,
1691
+ midiToFrequency,
1692
+ mirror,
1693
+ noteToFrequency,
1694
+ resetScheduler,
1695
+ resolveChordNotes,
1696
+ resolveCutoff,
1697
+ useScheduleNote,
1698
+ useSynth,
1699
+ useTrack
1700
+ };
1701
+ //# sourceMappingURL=index.js.map