@weasel-js/audio 1.9.4 → 1.10.1

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
 
@@ -72,6 +76,21 @@ engine.playNoise({
72
76
  });
73
77
  ```
74
78
 
79
+ `renderNote` and `renderNoise` compute the same voices as samples, with no
80
+ `AudioContext`: a `Float32Array` of the whole note through its release, to put
81
+ in an `AudioBuffer` and `register`. `createNoteKernel` and `createNoiseKernel`
82
+ are the block-at-a-time form, with `release()` and `retune()`, for rendering
83
+ inside an `AudioWorkletProcessor`. A square or a sawtooth is band-limited and a
84
+ harmonic past Nyquist is left out, so they do not alias; pan, position, and
85
+ routing are not a kernel's.
86
+
87
+ ```ts
88
+ const samples = renderNote({ pitch: 'C5', wave: partialPresets.bell, duration: 1500 }, ctx.sampleRate);
89
+ const buffer = ctx.createBuffer(1, samples.length, ctx.sampleRate);
90
+ buffer.getChannelData(0).set(samples);
91
+ const bell = engine.register(buffer);
92
+ ```
93
+
75
94
  `createPatternPlayer` is a step sequencer on top. Events sit on steps — a note
76
95
  or a noise with a `length` in steps, or a buffer `sound` — and each step is
77
96
  booked through `engine.schedule` only when the lookahead window reaches it, so `setTempo` and
@@ -90,6 +109,45 @@ player.start();
90
109
  player.setTempo(140);
91
110
  ```
92
111
 
112
+ A `step` can be fractional: `{ step: 2.25 }` sounds a quarter of a step after
113
+ step 2, which is how a pattern swings or a hit drags. The player's `detune` is
114
+ cents added to every event, as a number or a function of the event's step, for
115
+ a loop that drifts in pitch. `patternVoicePeaks(events, opts)` returns the most
116
+ voices the pattern holds at once on each bus, to check against `voiceLimit`
117
+ before it plays.
118
+
119
+ ## Generative scoring
120
+
121
+ `createScore` writes music from a mood instead of choosing a clip. A mood is
122
+ three numbers — `valence` (-1 dark to 1 bright), `tension` and `urgency` (0 to
123
+ 1) — and each one moves its own musical parameters: valence picks the mode, the
124
+ register and how open the chords are; tension picks the harmony and how many
125
+ layers build up; urgency picks the tempo, the pulse, the articulation and how
126
+ often the chord changes.
127
+
128
+ ```ts
129
+ const score = createScore(engine, {
130
+ mood: { valence: -1, tension: 1, urgency: 0 },
131
+ bus: 'music',
132
+ onBar: (state) => {
133
+ label.textContent = `${state.cue.name}: ${state.params.mode}, ${state.params.tempo} bpm`;
134
+ },
135
+ });
136
+ score.start();
137
+ score.setMood({ valence: 0, tension: 0.7, urgency: 1 }); // from the next bar line
138
+ ```
139
+
140
+ The corners of that space are the stock devices of documentary and news
141
+ scoring, listed in `scoreCues` with a sentence each on what they do: `dread`,
142
+ `investigation`, `wonder` and `urgency`. They work on a listener without asking
143
+ them, so the score says what it is doing: `state()` and `onBar` give the cue in
144
+ force, each cue's share of the mood (`cue.weights`), and every parameter the bar
145
+ was written from. Show it.
146
+
147
+ The layers underneath are usable on their own: `scoreParams(mood)` resolves a
148
+ mood, `scoreChord` and `scoreBar` write a chord and a bar of pattern events
149
+ from the parameters, and `readScoreCue(mood)` names the cue.
150
+
93
151
  ## Insert effects
94
152
 
95
153
  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,50 @@ 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
+ /** What of a `playNote` call shapes its samples. Routing, position, and timing
948
+ * belong to whatever plays them. */
949
+ type KernelNote = Pick<NoteOptions, 'pitch' | 'wave' | 'envelope' | 'glide' | 'filter' | 'duration' | 'gain' | 'detune'>;
950
+ /** What of a `playNoise` call shapes its samples. */
951
+ type KernelNoise = Pick<NoiseOptions, 'noise' | 'envelope' | 'filter' | 'duration' | 'gain' | 'rate' | 'detune'>;
952
+ /** One voice, rendered a block at a time. */
953
+ interface VoiceKernel {
954
+ /** Add the voice's next samples into `out[from..to)`, the whole of `out` by
955
+ * default. False once the voice has ended; it adds nothing from then on. */
956
+ render(out: Float32Array, from?: number, to?: number): boolean;
957
+ /** Note-off at the next sample: the envelopes release from wherever they
958
+ * have got to. Does nothing once the gate has closed. */
959
+ release(): void;
960
+ /** What `setRate` and `setDetune` do to a sounding voice. `detune` is cents. */
961
+ retune(rate: number, detune: number): void;
962
+ }
963
+ /** A `playNote` voice. Throws on a bad pitch or partial, as `playNote` does. */
964
+ declare function createNoteKernel(note: KernelNote, sampleRate: number): VoiceKernel;
965
+ /** A `playNoise` voice. `random` picks where in the loop it starts. */
966
+ declare function createNoiseKernel(noise: KernelNoise, sampleRate: number, random?: () => number): VoiceKernel;
967
+ /** A whole note as samples, through its release: `duration + release` ms at
968
+ * `sampleRate`. Put them in an `AudioBuffer` and `engine.register` it to play
969
+ * the note as a sound. */
970
+ declare function renderNote(note: KernelNote, sampleRate: number): Float32Array;
971
+ /** A whole noise voice as samples, through its release. */
972
+ declare function renderNoise(noise: KernelNoise, sampleRate: number, random?: () => number): Float32Array;
973
+
974
+ /** A biquad's coefficients, already divided by `a0`. */
975
+ interface BiquadCoefficients {
976
+ b0: number;
977
+ b1: number;
978
+ b2: number;
979
+ a1: number;
980
+ a2: number;
981
+ }
982
+ /**
983
+ * The coefficients a `BiquadFilterNode` of `type` runs at `frequency`, by the
984
+ * formulas in the Web Audio specification. `Q` is in dB for a lowpass or a
985
+ * highpass and a plain quality factor for the rest, as it is on the node, and
986
+ * `gain` is dB, read by the shelves and the peaking filter only. Pass `into`
987
+ * to have them written to an object you keep, which allocates nothing.
988
+ */
989
+ declare function biquadCoefficients(type: BiquadFilterType, frequency: number, sampleRate: number, Q?: number, gain?: number, into?: BiquadCoefficients): BiquadCoefficients;
990
+ /** How much a filter with coefficients `c` passes at `hz`, in dB. */
991
+ declare function biquadResponseDb(c: BiquadCoefficients, hz: number, sampleRate: number): number;
992
+
993
+ export { type Acquisition, type AnalyserTap, type AnalyserTapOptions, type AudioEngine, type AudioEngineOptions, type BiquadCoefficients, 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 KernelNoise, type KernelNote, 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 VoiceKernel, type VoicePool, type VoicePoolOptions, type VoiceRecord, type Waveform, type WetDryMix, biquadCoefficients, biquadResponseDb, createAnalyserTap, createAudioEngine, createBusGraph, createCompressorEffect, createDelayEffect, createFilterEffect, createNoiseKernel, createNoteKernel, createPatternPlayer, createReverbEffect, createScheduler, createScore, createSoundCache, createTickTimer, createVoicePool, envelopeLevel, envelopePoints, midiToFrequency, noiseSamples, noteToMidi, partialPresets, patternLength, patternVoicePeaks, readScoreCue, renderNoise, renderNote, resolveEnvelope, scoreBar, scoreChord, scoreCues, scoreModes, scoreParams, spatialize, toFrequency };