@weasel-js/audio 1.9.4 → 1.10.0

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/README.md CHANGED
@@ -17,6 +17,10 @@ dedicated Worker instead (`createTickTimer`). The worker is built from an inline
17
17
  global, or a Content-Security-Policy without `worker-src blob:` — it falls back
18
18
  to `setTimeout`, and a hidden tab books late again.
19
19
 
20
+ The worker times the pass, but the pass itself runs on the main thread, so a
21
+ stall there longer than the lookahead starts whatever came due late. The
22
+ `onLate(lateMs, when)` option reports each one; if it fires, raise `lookahead`.
23
+
20
24
  A suspended `AudioContext` is a separate matter: its clock stops, and on resume
21
25
  the engine drops what came due meanwhile rather than firing it all at once.
22
26
 
@@ -90,6 +94,45 @@ player.start();
90
94
  player.setTempo(140);
91
95
  ```
92
96
 
97
+ A `step` can be fractional: `{ step: 2.25 }` sounds a quarter of a step after
98
+ step 2, which is how a pattern swings or a hit drags. The player's `detune` is
99
+ cents added to every event, as a number or a function of the event's step, for
100
+ a loop that drifts in pitch. `patternVoicePeaks(events, opts)` returns the most
101
+ voices the pattern holds at once on each bus, to check against `voiceLimit`
102
+ before it plays.
103
+
104
+ ## Generative scoring
105
+
106
+ `createScore` writes music from a mood instead of choosing a clip. A mood is
107
+ three numbers — `valence` (-1 dark to 1 bright), `tension` and `urgency` (0 to
108
+ 1) — and each one moves its own musical parameters: valence picks the mode, the
109
+ register and how open the chords are; tension picks the harmony and how many
110
+ layers build up; urgency picks the tempo, the pulse, the articulation and how
111
+ often the chord changes.
112
+
113
+ ```ts
114
+ const score = createScore(engine, {
115
+ mood: { valence: -1, tension: 1, urgency: 0 },
116
+ bus: 'music',
117
+ onBar: (state) => {
118
+ label.textContent = `${state.cue.name}: ${state.params.mode}, ${state.params.tempo} bpm`;
119
+ },
120
+ });
121
+ score.start();
122
+ score.setMood({ valence: 0, tension: 0.7, urgency: 1 }); // from the next bar line
123
+ ```
124
+
125
+ The corners of that space are the stock devices of documentary and news
126
+ scoring, listed in `scoreCues` with a sentence each on what they do: `dread`,
127
+ `investigation`, `wonder` and `urgency`. They work on a listener without asking
128
+ them, so the score says what it is doing: `state()` and `onBar` give the cue in
129
+ force, each cue's share of the mood (`cue.weights`), and every parameter the bar
130
+ was written from. Show it.
131
+
132
+ The layers underneath are usable on their own: `scoreParams(mood)` resolves a
133
+ mood, `scoreChord` and `scoreBar` write a chord and a bar of pattern events
134
+ from the parameters, and `readScoreCue(mood)` names the cue.
135
+
93
136
  ## Insert effects
94
137
 
95
138
  Each bus has an ordered insert chain, `engine.bus(name).inserts`, between where
package/dist/index.d.ts CHANGED
@@ -446,6 +446,10 @@ interface AudioEngineOptions {
446
446
  * replaces the default, and the missing half falls back to `setTimeout`'s. */
447
447
  setTimer?: (cb: () => void, ms: number) => unknown;
448
448
  clearTimer?: (handle: unknown) => void;
449
+ /** A voice or an `engine.schedule` callback came due `lateMs` after its
450
+ * time `when`, both in engine ms: the main thread stalled for longer than
451
+ * the lookahead covers, and the voice sounded late by that much. */
452
+ onLate?: (lateMs: number, when: number) => void;
449
453
  /** Crossfade for every insert-chain edit and bypass, in ms. Default 15. */
450
454
  insertFadeMs?: number;
451
455
  /** Uniform in [0, 1), for where in its loop a noise voice starts. Default `Math.random`. */
@@ -612,6 +616,11 @@ interface SchedulerOptions {
612
616
  lookahead?: number;
613
617
  /** Time between passes, in ms. Default 25. */
614
618
  interval?: number;
619
+ /** An event fired `lateMs` after its time `when`: the passes that should
620
+ * have reached it inside the lookahead stalled, and whatever it starts will
621
+ * sound late by that much. Never called for an event booked inside the
622
+ * lookahead or for a time already past, which no pass was meant to catch. */
623
+ onLate?: (lateMs: number, when: number) => void;
615
624
  }
616
625
  /** A queue of callbacks keyed to engine time. See `createScheduler`. */
617
626
  interface Scheduler {
@@ -664,7 +673,11 @@ interface TickTimer {
664
673
  */
665
674
  declare function createTickTimer(): TickTimer;
666
675
 
667
- /** A synth note on a step. `length` is its gate in steps, default 1. */
676
+ /** A synth note on a step. `length` is its gate in steps, default 1.
677
+ *
678
+ * Any event's `step` may be fractional: it is booked with the step it sits in
679
+ * and sounds that fraction of a step later, so `2.5` is halfway from step 2 to
680
+ * step 3. That is swing, a flam, or a hit that drags. */
668
681
  interface PatternNote extends Omit<NoteOptions, 'when' | 'duration'> {
669
682
  step: number;
670
683
  length?: number;
@@ -694,10 +707,16 @@ interface PatternPlayerOptions {
694
707
  length?: number;
695
708
  /** Default true. */
696
709
  loop?: boolean;
710
+ /** Cents added to every event's own `detune`: a number, or a function of
711
+ * the event's `step`, for a pattern that drifts in pitch as it plays. A
712
+ * function is asked as each event is booked. Default 0. */
713
+ detune?: PatternDetune;
697
714
  /** Called as each step is booked — up to a lookahead ahead of it sounding —
698
715
  * with its index in the pattern and its engine time in ms. */
699
716
  onStep?: (step: number, when: number) => void;
700
717
  }
718
+ /** See `PatternPlayerOptions.detune`. */
719
+ type PatternDetune = number | ((step: number) => number);
701
720
  /** A running sequence of steps. See `createPatternPlayer`. */
702
721
  interface PatternPlayer {
703
722
  /** Start from step 0 at engine time `when`, default now. No-op while playing. */
@@ -706,11 +725,17 @@ interface PatternPlayer {
706
725
  stop(): void;
707
726
  playing(): boolean;
708
727
  tempo(): number;
709
- /** Takes effect from the next step not yet booked. */
728
+ /** Takes effect from the next step not yet booked. Called from `onStep`,
729
+ * that is the step after the one reported, which keeps its own length. */
710
730
  setTempo(bpm: number): void;
711
731
  /** Takes effect from the next step not yet booked. */
732
+ setDetune(detune: PatternDetune): void;
733
+ /** Takes effect from the next step not yet booked. */
712
734
  setEvents(events: readonly PatternEvent[], length?: number): void;
713
735
  }
736
+ /** Steps in one pass of `events` when no length is given: through the last
737
+ * event, rounded up to a whole beat. */
738
+ declare function patternLength(events: readonly PatternEvent[], stepsPerBeat?: number): number;
714
739
  /**
715
740
  * A step sequencer over `engine`. Each step is one `engine.schedule` booking;
716
741
  * when it fires, it books that step's events at the step's exact time and then
@@ -724,6 +749,167 @@ interface PatternPlayer {
724
749
  */
725
750
  declare function createPatternPlayer(engine: AudioEngine, opts: PatternPlayerOptions): PatternPlayer;
726
751
 
752
+ /** Options for `patternVoicePeaks`. The first four mean what they mean to
753
+ * `createPatternPlayer`, and should be the values the player is given. */
754
+ interface PatternLoadOptions {
755
+ tempo: number;
756
+ stepsPerBeat?: number;
757
+ length?: number;
758
+ loop?: boolean;
759
+ /** The bus of an event that names none: the engine's first. Default 'sfx'. */
760
+ bus?: string;
761
+ /** The engine's `lookahead` in ms. Default 100. */
762
+ lookahead?: number;
763
+ /** How long a buffer hit sounds, in ms. Default: one step. */
764
+ hitMs?: (hit: PatternHit) => number;
765
+ }
766
+ /**
767
+ * The most voices a pattern holds at once on each bus, to set against the
768
+ * engine's `voiceLimit` before the pattern plays: past the limit, each new
769
+ * event steals a voice that is still ringing.
770
+ *
771
+ * A note or a noise holds its voice through its gate and its envelope's
772
+ * release. With `loop`, what rings past the end of a pass is counted against
773
+ * the start of the next.
774
+ */
775
+ declare function patternVoicePeaks(events: readonly PatternEvent[], opts: PatternLoadOptions): Record<string, number>;
776
+
777
+ /** Where a score sits on the three axes it is steered by. Values outside a
778
+ * range are read as its nearest end. */
779
+ interface ScoreMood {
780
+ /** -1 dark to 1 bright. */
781
+ valence: number;
782
+ /** 0 consonant and settled to 1 dissonant. */
783
+ tension: number;
784
+ /** 0 still to 1 driving. */
785
+ urgency: number;
786
+ }
787
+ /** The scoring devices of factual screen content that a mood can land on. */
788
+ type ScoreCueName = 'dread' | 'investigation' | 'wonder' | 'urgency';
789
+ /** One named device: the mood that produces it, and what it does to a listener
790
+ * in words, for a consumer to show beside the score. */
791
+ interface ScoreCue {
792
+ name: ScoreCueName;
793
+ mood: ScoreMood;
794
+ device: string;
795
+ }
796
+ /** The built-in cues: each names a mood and the musical device that carries it. */
797
+ declare const scoreCues: readonly ScoreCue[];
798
+ /** Which cue a mood is applying: the nearest, and how much of each. */
799
+ interface ScoreCueReading {
800
+ name: ScoreCueName;
801
+ /** Each cue's share, summing to 1. A mood at a cue's own `mood` is all that cue. */
802
+ weights: Record<ScoreCueName, number>;
803
+ }
804
+ /** Name the cue a mood is applying, and weigh the others against it by
805
+ * inverse squared distance. */
806
+ declare function readScoreCue(mood: ScoreMood): ScoreCueReading;
807
+ /** The modes a score draws on, brightest first. */
808
+ type ScoreMode = 'lydian' | 'ionian' | 'dorian' | 'aeolian' | 'phrygian';
809
+ /** Each mode's seven tones, in semitones above its tonic. */
810
+ declare const scoreModes: Record<ScoreMode, readonly number[]>;
811
+ /** The musical parameters a mood resolves to. Each follows one axis only. */
812
+ interface ScoreParams {
813
+ /** From valence. */
814
+ mode: ScoreMode;
815
+ /** MIDI note of the tonic the chords are built up from. From valence. */
816
+ register: number;
817
+ /** Octaves a chord is opened across, 1 to 3. From valence. */
818
+ spread: number;
819
+ /** How the tones above a root are chosen: thirds from the mode, stacked
820
+ * fourths and fifths, or a minor second and a tritone. From tension. */
821
+ harmony: 'triadic' | 'quartal' | 'cluster';
822
+ /** The most layers a pulsed score builds up to. From tension. */
823
+ layers: number;
824
+ /** Beats per minute. From urgency. */
825
+ tempo: number;
826
+ /** Notes to a beat; 0 is no pulse at all. From urgency. */
827
+ density: 0 | 1 | 2 | 4;
828
+ /** From urgency. */
829
+ articulation: 'sustained' | 'plucked';
830
+ /** Bars a chord lasts. From urgency. */
831
+ harmonicRhythm: number;
832
+ /** Envelope attack in ms. From urgency. */
833
+ attack: number;
834
+ }
835
+ /** Resolve a mood to the parameters `scoreBar` writes music from. */
836
+ declare function scoreParams(mood: ScoreMood): ScoreParams;
837
+
838
+ /** Steps in a bar of score: sixteenths in 4/4. */
839
+ declare const SCORE_BAR_STEPS = 16;
840
+ /** How `scoreBar` places and routes its notes. */
841
+ interface ScoreBarOptions {
842
+ /** Semitones above C for the tonic. Default 0. */
843
+ tonic?: number;
844
+ /** Default: the engine's first bus. */
845
+ bus?: string;
846
+ /** The level the notes sounding on one step add up to at most. Default 0.5. */
847
+ gain?: number;
848
+ /** Chooses the order an ostinato takes a chord's tones in. Default: rising. */
849
+ seed?: number;
850
+ }
851
+ /**
852
+ * The chord a score plays, as MIDI notes, root first. `chord` counts chords
853
+ * since the score began; a cluster stays on the tonic, and the other harmonies
854
+ * walk a four-chord progression inside the mode.
855
+ */
856
+ declare function scoreChord(params: ScoreParams, chord: number, tonic?: number): number[];
857
+ /**
858
+ * One bar of score as pattern events, for a player `SCORE_BAR_STEPS` long.
859
+ * `bar` counts bars since the score began: it picks the chord, by the harmonic
860
+ * rhythm, and how many layers a pulsed score has built up to, one more every
861
+ * two bars. The same arguments always write the same bar.
862
+ *
863
+ * With no pulse, the bar is its chord held throughout. With one, the layers
864
+ * are an ostinato over the chord's tones, a bass on the root, the ostinato
865
+ * doubled an octave up on the off pulses, and the chord held as a pad.
866
+ */
867
+ declare function scoreBar(params: ScoreParams, bar: number, opts?: ScoreBarOptions): PatternNote[];
868
+
869
+ /** What a score is playing in one bar, and why: the mood it was given, the cue
870
+ * that mood applies, and the parameters the bar was written from. */
871
+ interface ScoreState {
872
+ /** Bars since `start`. */
873
+ bar: number;
874
+ /** Bars since the cue last changed, which is what a pulsed score's layers
875
+ * build up from. */
876
+ cueBar: number;
877
+ mood: ScoreMood;
878
+ cue: ScoreCueReading;
879
+ params: ScoreParams;
880
+ }
881
+ /** Options for `createScore`. */
882
+ interface ScoreOptions extends ScoreBarOptions {
883
+ mood: ScoreMood;
884
+ /** Called as each bar's first step is booked — up to a lookahead before it
885
+ * sounds — with what the bar plays and its engine time in ms. */
886
+ onBar?: (state: ScoreState, when: number) => void;
887
+ }
888
+ /** A running generative score. See `createScore`. */
889
+ interface Score {
890
+ /** Start from bar 0 at engine time `when`, default now. No-op while playing. */
891
+ start(when?: number): void;
892
+ /** Book nothing further, and release every voice the score started. */
893
+ stop(): void;
894
+ playing(): boolean;
895
+ /** The mood last set, which the next bar will take. */
896
+ mood(): ScoreMood;
897
+ /** Takes effect at the next bar line. */
898
+ setMood(mood: ScoreMood): void;
899
+ /** The bar most recently booked. */
900
+ state(): ScoreState;
901
+ }
902
+ /**
903
+ * Music written continuously from a mood rather than chosen from clips. Each
904
+ * bar is `scoreBar` of the mood's `scoreParams`, booked through a pattern
905
+ * player, and a new mood lands on the next bar line with its tempo.
906
+ *
907
+ * These devices work on a listener without asking them, which is what they are
908
+ * for. `state()` and `onBar` name the cue in force and every parameter behind
909
+ * it, so a consumer can show what is being applied as well as play it.
910
+ */
911
+ declare function createScore(engine: AudioEngine, opts: ScoreOptions): Score;
912
+
727
913
  /** Samples of `color` noise, seeded so every context gets the same loop. The
728
914
  * end is pulled onto the start, so the loop point does not click. */
729
915
  declare function noiseSamples(color: NoiseColor, length: number): Float32Array;
@@ -758,4 +944,4 @@ declare const partialPresets: {
758
944
  };
759
945
  };
760
946
 
761
- export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BusGraph, type BusGraphOptions, type BusHandle, type CompressorEffect, type CompressorEffectOptions, type DelayEffect, type DelayEffectOptions, type Envelope, type EnvelopePoint, type FilterEffect, type FilterEffectOptions, type Glide, type Inharmonic, type InsertAddOptions, type InsertChain, type InsertChainOptions, type InsertEffect, type InsertSlot, type NoiseColor, type NoiseOptions, type NoisePatch, type NoteOptions, type PatternEvent, type PatternHit, type PatternNoise, type PatternNote, type PatternPlayer, type PatternPlayerOptions, type Pitch, type PlayOptions, type ResolvedEnvelope, type ReverbEffect, type ReverbEffectOptions, type Scheduler, type SchedulerOptions, type SoundCache, type SoundHandle, type SpatialOptions, type StealPolicy, type StreamOptions, type SynthPartial, type SynthPatch, type TickTimer, type Vec2, type VoiceDropReason, type VoiceFilter, type VoiceHandle, type VoicePool, type VoicePoolOptions, type VoiceRecord, type Waveform, type WetDryMix, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, resolveEnvelope, spatialize, toFrequency };
947
+ export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BusGraph, type BusGraphOptions, type BusHandle, type CompressorEffect, type CompressorEffectOptions, type DelayEffect, type DelayEffectOptions, type Envelope, type EnvelopePoint, type FilterEffect, type FilterEffectOptions, type Glide, type Inharmonic, type InsertAddOptions, type InsertChain, type InsertChainOptions, type InsertEffect, type InsertSlot, type NoiseColor, type NoiseOptions, type NoisePatch, type NoteOptions, type PatternDetune, type PatternEvent, type PatternHit, type PatternLoadOptions, type PatternNoise, type PatternNote, type PatternPlayer, type PatternPlayerOptions, type Pitch, type PlayOptions, type ResolvedEnvelope, type ReverbEffect, type ReverbEffectOptions, SCORE_BAR_STEPS, type Scheduler, type SchedulerOptions, type Score, type ScoreBarOptions, type ScoreCue, type ScoreCueName, type ScoreCueReading, type ScoreMode, type ScoreMood, type ScoreOptions, type ScoreParams, type ScoreState, type SoundCache, type SoundHandle, type SpatialOptions, type StealPolicy, type StreamOptions, type SynthPartial, type SynthPatch, type TickTimer, type Vec2, type VoiceDropReason, type VoiceFilter, type VoiceHandle, type VoicePool, type VoicePoolOptions, type VoiceRecord, type Waveform, type WetDryMix, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
package/dist/index.js CHANGED
@@ -397,13 +397,15 @@ function createScheduler(opts) {
397
397
  let handle = null;
398
398
  let flushQueued = false;
399
399
  const fireDue = () => {
400
- const horizon = opts.now() + lookahead;
400
+ const now = opts.now();
401
+ const horizon = now + lookahead;
401
402
  const due = [];
402
403
  const rest = [];
403
404
  for (const entry of queue) (entry.when <= horizon ? due : rest).push(entry);
404
405
  queue = rest;
405
406
  due.sort((a, b) => a.when - b.when);
406
407
  for (const entry of due) {
408
+ if (entry.waited && entry.when < now) opts.onLate?.(now - entry.when, entry.when);
407
409
  try {
408
410
  entry.fire(entry.when);
409
411
  } catch (err) {
@@ -432,8 +434,9 @@ function createScheduler(opts) {
432
434
  handle = null;
433
435
  },
434
436
  schedule(when, fire, key) {
435
- queue.push({ when, fire, key });
436
- if (running && !flushQueued && when <= opts.now() + lookahead) {
437
+ const waited = when > opts.now() + lookahead;
438
+ queue.push({ when, fire, key, waited });
439
+ if (running && !flushQueued && !waited) {
437
440
  flushQueued = true;
438
441
  queueMicrotask(flush);
439
442
  }
@@ -943,7 +946,8 @@ function createAudioEngine(opts = {}) {
943
946
  setTimer: timers.setTimer,
944
947
  clearTimer: timers.clearTimer,
945
948
  lookahead: opts.lookahead,
946
- interval: opts.tickInterval
949
+ interval: opts.tickInterval,
950
+ onLate: opts.onLate
947
951
  });
948
952
  scheduler.start();
949
953
  const graph = createBusGraph(ctx, busNames, { ...timers, fadeMs: opts.insertFadeMs });
@@ -1557,6 +1561,11 @@ function createCompressorEffect(ctx, opts = {}) {
1557
1561
 
1558
1562
  // src/patternPlayer.ts
1559
1563
  var nextPlayer = 1;
1564
+ function patternLength(events, stepsPerBeat = 4) {
1565
+ let last = -1;
1566
+ for (const e of events) last = Math.max(last, Math.floor(e.step));
1567
+ return Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
1568
+ }
1560
1569
  var checkTempo = (bpm) => {
1561
1570
  if (!(bpm > 0) || !Number.isFinite(bpm)) {
1562
1571
  throw new RangeError(`@weasel-js/audio: tempo must be a positive number of bpm, got ${bpm}`);
@@ -1568,21 +1577,22 @@ function createPatternPlayer(engine, opts) {
1568
1577
  const loop = opts.loop ?? true;
1569
1578
  const key = `\0pattern:${nextPlayer++}`;
1570
1579
  let bpm = opts.tempo;
1580
+ let drift = opts.detune ?? 0;
1571
1581
  let length = 1;
1572
1582
  let byStep = /* @__PURE__ */ new Map();
1573
1583
  const setEvents = (events, len) => {
1574
1584
  byStep = /* @__PURE__ */ new Map();
1575
- let last = -1;
1576
1585
  for (const e of events) {
1577
- const list = byStep.get(e.step);
1586
+ const step = Math.floor(e.step);
1587
+ const list = byStep.get(step);
1578
1588
  if (list) list.push(e);
1579
- else byStep.set(e.step, [e]);
1580
- last = Math.max(last, e.step);
1589
+ else byStep.set(step, [e]);
1581
1590
  }
1582
- length = len ?? Math.max(1, Math.ceil((last + 1) / stepsPerBeat)) * stepsPerBeat;
1591
+ length = len ?? patternLength(events, stepsPerBeat);
1583
1592
  };
1584
1593
  setEvents(opts.events ?? [], opts.length);
1585
1594
  let running = false;
1595
+ let inStep = false;
1586
1596
  let count = 0;
1587
1597
  let lastAt = null;
1588
1598
  let nextAt = 0;
@@ -1607,21 +1617,30 @@ function createPatternPlayer(engine, opts) {
1607
1617
  const step = count % length;
1608
1618
  for (const v of voices) if (!v.isPlaying()) voices.delete(v);
1609
1619
  for (const e of byStep.get(step) ?? []) {
1620
+ const at = when + (e.step - step) * stepMs();
1621
+ const detune = (e.detune ?? 0) + (typeof drift === "function" ? drift(e.step) : drift);
1610
1622
  if ("sound" in e) {
1611
1623
  const { step: _s, sound, ...play } = e;
1612
- voices.add(engine.play(sound, { ...play, when }));
1624
+ voices.add(engine.play(sound, { ...play, detune, when: at }));
1613
1625
  } else if ("noise" in e) {
1614
1626
  const { step: _s, length: steps = 1, ...noise } = e;
1615
- voices.add(engine.playNoise({ ...noise, when, duration: steps * stepMs() }));
1627
+ voices.add(engine.playNoise({ ...noise, detune, when: at, duration: steps * stepMs() }));
1616
1628
  } else {
1617
1629
  const { step: _s, length: steps = 1, ...note } = e;
1618
- voices.add(engine.playNote({ ...note, when, duration: steps * stepMs() }));
1630
+ voices.add(engine.playNote({ ...note, detune, when: at, duration: steps * stepMs() }));
1619
1631
  }
1620
1632
  }
1621
- opts.onStep?.(step, when);
1633
+ const ms = stepMs();
1634
+ inStep = true;
1635
+ try {
1636
+ opts.onStep?.(step, when);
1637
+ } finally {
1638
+ inStep = false;
1639
+ }
1640
+ if (!running) return;
1622
1641
  count += 1;
1623
1642
  lastAt = when;
1624
- nextAt = when + stepMs();
1643
+ nextAt = when + ms;
1625
1644
  book();
1626
1645
  };
1627
1646
  return {
@@ -1645,15 +1664,231 @@ function createPatternPlayer(engine, opts) {
1645
1664
  setTempo(next) {
1646
1665
  checkTempo(next);
1647
1666
  bpm = next;
1648
- if (!running || lastAt === null) return;
1667
+ if (!running || lastAt === null || inStep) return;
1649
1668
  engine.stopKey(key);
1650
1669
  nextAt = Math.max(lastAt + stepMs(), engine.now());
1651
1670
  book();
1652
1671
  },
1672
+ setDetune(next) {
1673
+ drift = next;
1674
+ },
1653
1675
  setEvents
1654
1676
  };
1655
1677
  }
1656
1678
 
1657
- export { createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, resolveEnvelope, spatialize, toFrequency };
1679
+ // src/patternLoad.ts
1680
+ function patternVoicePeaks(events, opts) {
1681
+ const stepsPerBeat = opts.stepsPerBeat ?? 4;
1682
+ const stepMs = 6e4 / opts.tempo / stepsPerBeat;
1683
+ const passMs = (opts.length ?? patternLength(events, stepsPerBeat)) * stepMs;
1684
+ const lead = opts.lookahead ?? 100;
1685
+ const spans = events.map((e) => {
1686
+ const sounds = "sound" in e ? opts.hitMs?.(e) ?? stepMs : (e.length ?? 1) * stepMs + resolveEnvelope(e.envelope).release;
1687
+ const at = e.step * stepMs;
1688
+ return { bus: e.bus ?? opts.bus ?? "sfx", from: at - lead, until: at + sounds };
1689
+ });
1690
+ const longest = Math.max(0, ...spans.map((s) => s.until - s.from));
1691
+ const passes = opts.loop === false ? 0 : Math.ceil(longest / passMs);
1692
+ const edges = /* @__PURE__ */ new Map();
1693
+ for (const span of spans) {
1694
+ const list = edges.get(span.bus) ?? [];
1695
+ edges.set(span.bus, list);
1696
+ for (let pass = 0; pass <= passes; pass++) {
1697
+ list.push({ at: span.from + pass * passMs, by: 1 }, { at: span.until + pass * passMs, by: -1 });
1698
+ }
1699
+ }
1700
+ const peaks = {};
1701
+ for (const [bus, list] of edges) {
1702
+ list.sort((a, b) => a.at - b.at || a.by - b.by);
1703
+ let held = 0;
1704
+ let peak = 0;
1705
+ for (const edge of list) {
1706
+ held += edge.by;
1707
+ peak = Math.max(peak, held);
1708
+ }
1709
+ peaks[bus] = peak;
1710
+ }
1711
+ return peaks;
1712
+ }
1713
+
1714
+ // src/scoreMood.ts
1715
+ var scoreCues = [
1716
+ {
1717
+ name: "dread",
1718
+ mood: { valence: -1, tension: 1, urgency: 0 },
1719
+ device: "Sustained low tones a minor second and a tritone apart, with no pulse."
1720
+ },
1721
+ {
1722
+ name: "investigation",
1723
+ mood: { valence: -0.4, tension: 0.5, urgency: 0.5 },
1724
+ device: "A plucked minor ostinato that adds a layer every two bars."
1725
+ },
1726
+ {
1727
+ name: "wonder",
1728
+ mood: { valence: 1, tension: 0.1, urgency: 0.1 },
1729
+ device: "Lydian chords in open voicings, swelling in with a soft attack."
1730
+ },
1731
+ {
1732
+ name: "urgency",
1733
+ mood: { valence: 0, tension: 0.7, urgency: 1 },
1734
+ device: "Driving sixteenths on stacked fourths and fifths, a new chord every bar."
1735
+ }
1736
+ ];
1737
+ var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
1738
+ var inRange = (mood) => ({
1739
+ valence: clamp(mood.valence, -1, 1),
1740
+ tension: clamp(mood.tension, 0, 1),
1741
+ urgency: clamp(mood.urgency, 0, 1)
1742
+ });
1743
+ function readScoreCue(mood) {
1744
+ const m = inRange(mood);
1745
+ const distance = scoreCues.map((cue) => ((m.valence - cue.mood.valence) / 2) ** 2 + (m.tension - cue.mood.tension) ** 2 + (m.urgency - cue.mood.urgency) ** 2);
1746
+ const exact = distance.indexOf(0);
1747
+ const raw = distance.map((d, i) => exact >= 0 ? Number(i === exact) : 1 / d);
1748
+ const total = raw.reduce((sum, w) => sum + w, 0);
1749
+ const weights = {};
1750
+ scoreCues.forEach((cue, i) => {
1751
+ weights[cue.name] = raw[i] / total;
1752
+ });
1753
+ return { name: scoreCues[raw.indexOf(Math.max(...raw))].name, weights };
1754
+ }
1755
+ var scoreModes = {
1756
+ lydian: [0, 2, 4, 6, 7, 9, 11],
1757
+ ionian: [0, 2, 4, 5, 7, 9, 11],
1758
+ dorian: [0, 2, 3, 5, 7, 9, 10],
1759
+ aeolian: [0, 2, 3, 5, 7, 8, 10],
1760
+ phrygian: [0, 1, 3, 5, 7, 8, 10]
1761
+ };
1762
+ function scoreParams(mood) {
1763
+ const { valence, tension, urgency } = inRange(mood);
1764
+ const sustained = urgency < 0.35;
1765
+ return {
1766
+ mode: valence > 0.6 ? "lydian" : valence > 0.2 ? "ionian" : valence > -0.2 ? "dorian" : valence > -0.6 ? "aeolian" : "phrygian",
1767
+ register: 36 + 12 * Math.round(valence + 1),
1768
+ spread: 1 + Math.round(2 * Math.max(0, valence)),
1769
+ harmony: tension >= 0.8 ? "cluster" : tension >= 0.55 ? "quartal" : "triadic",
1770
+ layers: 1 + Math.round(4 * tension),
1771
+ tempo: Math.round(60 + 80 * urgency),
1772
+ density: urgency < 0.15 ? 0 : urgency < 0.4 ? 1 : urgency < 0.75 ? 2 : 4,
1773
+ articulation: sustained ? "sustained" : "plucked",
1774
+ harmonicRhythm: urgency < 0.33 ? 4 : urgency < 0.66 ? 2 : 1,
1775
+ attack: sustained ? Math.round(800 * (1 - urgency)) : 5
1776
+ };
1777
+ }
1778
+
1779
+ // src/scoreBar.ts
1780
+ var SCORE_BAR_STEPS = 16;
1781
+ var PROGRESSION = [0, 5, 3, 4];
1782
+ function scoreChord(params, chord, tonic = 0) {
1783
+ const scale = scoreModes[params.mode];
1784
+ const tone = (degree2) => params.register + tonic + scale[degree2 % 7] + 12 * Math.floor(degree2 / 7);
1785
+ if (params.harmony === "cluster") {
1786
+ const root2 = tone(0);
1787
+ return [root2, root2 + 1, root2 + 6];
1788
+ }
1789
+ const degree = PROGRESSION[chord % PROGRESSION.length];
1790
+ const root = tone(degree);
1791
+ if (params.harmony === "quartal") return [root, root + 7, root + 12, root + 17];
1792
+ if (params.spread < 2) return [root, tone(degree + 2), tone(degree + 4)];
1793
+ const open = [root, tone(degree + 4), tone(degree + 2) + 12];
1794
+ if (params.spread >= 3) open.push(tone(degree + 3) + 24);
1795
+ return open;
1796
+ }
1797
+ function figure(count, seed) {
1798
+ const order = Array.from({ length: count }, (_, i) => i);
1799
+ if (seed === void 0) return order;
1800
+ let state = seed >>> 0 || 1;
1801
+ for (let i = count - 1; i > 0; i--) {
1802
+ state = Math.imul(state, 1664525) + 1013904223 >>> 0;
1803
+ const j = state % (i + 1);
1804
+ [order[i], order[j]] = [order[j], order[i]];
1805
+ }
1806
+ return order;
1807
+ }
1808
+ var PLUCK = { attack: 5, decay: 140, sustain: 0.2, release: 80 };
1809
+ function scoreBar(params, bar, opts = {}) {
1810
+ const gain = opts.gain ?? 0.5;
1811
+ const chord = scoreChord(params, Math.floor(bar / params.harmonicRhythm), opts.tonic);
1812
+ const swell = { attack: params.attack, release: params.attack };
1813
+ const held = params.articulation === "sustained";
1814
+ const note = (step, length, midi, level, envelope2) => ({
1815
+ step,
1816
+ length,
1817
+ pitch: { midi },
1818
+ gain: gain * level,
1819
+ envelope: envelope2,
1820
+ wave: held ? "sine" : "triangle",
1821
+ ...opts.bus === void 0 ? {} : { bus: opts.bus }
1822
+ });
1823
+ const pad = (level, envelope2) => chord.map((midi) => note(0, SCORE_BAR_STEPS, midi, level / chord.length, envelope2));
1824
+ if (params.density === 0) return pad(1, swell);
1825
+ const layers = Math.min(params.layers, 1 + Math.floor(bar / 2));
1826
+ const every = 4 / params.density;
1827
+ const order = figure(chord.length, opts.seed);
1828
+ const envelope = held ? swell : PLUCK;
1829
+ const events = [];
1830
+ for (let pulse = 0; pulse * every < SCORE_BAR_STEPS; pulse++) {
1831
+ const midi = chord[order[pulse % order.length]];
1832
+ events.push(note(pulse * every, every, midi, 0.4, envelope));
1833
+ if (layers >= 3 && pulse % 2 === 1) events.push(note(pulse * every, every, midi + 12, 0.15, envelope));
1834
+ }
1835
+ if (layers >= 2) {
1836
+ for (const step of [0, 8]) events.push(note(step, 8, chord[0] - 12, 0.3, { attack: 5, release: 120 }));
1837
+ }
1838
+ if (layers >= 4) events.push(...pad(0.15, { attack: 120, release: 200 }));
1839
+ return events;
1840
+ }
1841
+
1842
+ // src/score.ts
1843
+ function createScore(engine, opts) {
1844
+ const { mood: initial, onBar, ...barOpts } = opts;
1845
+ let target = initial;
1846
+ const stateAt = (bar, before) => {
1847
+ const cue = readScoreCue(target);
1848
+ return {
1849
+ bar,
1850
+ cueBar: before && before.cue.name === cue.name ? before.cueBar + 1 : 0,
1851
+ mood: target,
1852
+ cue,
1853
+ params: scoreParams(target)
1854
+ };
1855
+ };
1856
+ const eventsOf = (state) => scoreBar(state.params, state.cueBar, barOpts);
1857
+ let current = stateAt(0, null);
1858
+ let next = current;
1859
+ const player = createPatternPlayer(engine, {
1860
+ tempo: current.params.tempo,
1861
+ length: SCORE_BAR_STEPS,
1862
+ events: eventsOf(current),
1863
+ onStep(step, when) {
1864
+ if (step === 0) {
1865
+ current = next;
1866
+ onBar?.(current, when);
1867
+ }
1868
+ if (step !== SCORE_BAR_STEPS - 1) return;
1869
+ next = stateAt(current.bar + 1, current);
1870
+ player.setTempo(next.params.tempo);
1871
+ player.setEvents(eventsOf(next), SCORE_BAR_STEPS);
1872
+ }
1873
+ });
1874
+ return {
1875
+ start(when) {
1876
+ if (player.playing()) return;
1877
+ current = next = stateAt(0, null);
1878
+ player.setTempo(current.params.tempo);
1879
+ player.setEvents(eventsOf(current), SCORE_BAR_STEPS);
1880
+ player.start(when);
1881
+ },
1882
+ stop: () => player.stop(),
1883
+ playing: () => player.playing(),
1884
+ mood: () => target,
1885
+ setMood(mood) {
1886
+ target = mood;
1887
+ },
1888
+ state: () => current
1889
+ };
1890
+ }
1891
+
1892
+ export { SCORE_BAR_STEPS, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };
1658
1893
  //# sourceMappingURL=index.js.map
1659
1894
  //# sourceMappingURL=index.js.map