@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 +43 -0
- package/dist/index.d.ts +189 -3
- package/dist/index.js +251 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
436
|
-
|
|
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
|
|
1586
|
+
const step = Math.floor(e.step);
|
|
1587
|
+
const list = byStep.get(step);
|
|
1578
1588
|
if (list) list.push(e);
|
|
1579
|
-
else byStep.set(
|
|
1580
|
-
last = Math.max(last, e.step);
|
|
1589
|
+
else byStep.set(step, [e]);
|
|
1581
1590
|
}
|
|
1582
|
-
length = len ??
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|