@weasel-js/audio 1.10.0 → 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
@@ -76,6 +76,21 @@ engine.playNoise({
76
76
  });
77
77
  ```
78
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
+
79
94
  `createPatternPlayer` is a step sequencer on top. Events sit on steps — a note
80
95
  or a noise with a `length` in steps, or a buffer `sound` — and each step is
81
96
  booked through `engine.schedule` only when the lookahead window reaches it, so `setTempo` and
package/dist/index.d.ts CHANGED
@@ -944,4 +944,50 @@ declare const partialPresets: {
944
944
  };
945
945
  };
946
946
 
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 };
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 };
package/dist/index.js CHANGED
@@ -1889,6 +1889,241 @@ function createScore(engine, opts) {
1889
1889
  };
1890
1890
  }
1891
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 };
1892
+ // src/biquad.ts
1893
+ function biquadCoefficients(type, frequency, sampleRate, Q = 1, gain = 0, into = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }) {
1894
+ const f = Math.min(Math.max(frequency, 1e-3), sampleRate * 0.4999);
1895
+ const w0 = 2 * Math.PI * f / sampleRate;
1896
+ const cos = Math.cos(w0);
1897
+ const sin = Math.sin(w0);
1898
+ const A = 10 ** (gain / 40);
1899
+ const alphaQ = sin / (2 * Math.max(Q, 1e-4));
1900
+ const alphaDb = sin / (2 * 10 ** (Q / 20));
1901
+ const shelf = sin * Math.SQRT2 * Math.sqrt(A);
1902
+ let b0 = (1 - cos) / 2;
1903
+ let b1 = 1 - cos;
1904
+ let b2 = b0;
1905
+ let a0 = 1 + alphaDb;
1906
+ let a1 = -2 * cos;
1907
+ let a2 = 1 - alphaDb;
1908
+ if (type === "highpass") {
1909
+ b0 = b2 = (1 + cos) / 2;
1910
+ b1 = -(1 + cos);
1911
+ } else if (type === "peaking") {
1912
+ b0 = 1 + alphaQ * A;
1913
+ b1 = -2 * cos;
1914
+ b2 = 1 - alphaQ * A;
1915
+ a0 = 1 + alphaQ / A;
1916
+ a2 = 1 - alphaQ / A;
1917
+ } else if (type === "lowshelf") {
1918
+ b0 = A * (A + 1 - (A - 1) * cos + shelf);
1919
+ b1 = 2 * A * (A - 1 - (A + 1) * cos);
1920
+ b2 = A * (A + 1 - (A - 1) * cos - shelf);
1921
+ a0 = A + 1 + (A - 1) * cos + shelf;
1922
+ a1 = -2 * (A - 1 + (A + 1) * cos);
1923
+ a2 = A + 1 + (A - 1) * cos - shelf;
1924
+ } else if (type === "highshelf") {
1925
+ b0 = A * (A + 1 + (A - 1) * cos + shelf);
1926
+ b1 = -2 * A * (A - 1 + (A + 1) * cos);
1927
+ b2 = A * (A + 1 + (A - 1) * cos - shelf);
1928
+ a0 = A + 1 - (A - 1) * cos + shelf;
1929
+ a1 = 2 * (A - 1 - (A + 1) * cos);
1930
+ a2 = A + 1 - (A - 1) * cos - shelf;
1931
+ } else if (type !== "lowpass") {
1932
+ a0 = 1 + alphaQ;
1933
+ a2 = 1 - alphaQ;
1934
+ b0 = type === "bandpass" ? alphaQ : type === "notch" ? 1 : 1 - alphaQ;
1935
+ b1 = type === "bandpass" ? 0 : -2 * cos;
1936
+ b2 = type === "bandpass" ? -alphaQ : type === "notch" ? 1 : 1 + alphaQ;
1937
+ }
1938
+ into.b0 = b0 / a0;
1939
+ into.b1 = b1 / a0;
1940
+ into.b2 = b2 / a0;
1941
+ into.a1 = a1 / a0;
1942
+ into.a2 = a2 / a0;
1943
+ return into;
1944
+ }
1945
+ function biquadResponseDb(c, hz, sampleRate) {
1946
+ const w = 2 * Math.PI * hz / sampleRate;
1947
+ const c1 = Math.cos(w);
1948
+ const s1 = Math.sin(w);
1949
+ const c2 = Math.cos(2 * w);
1950
+ const s2 = Math.sin(2 * w);
1951
+ const num = (c.b0 + c.b1 * c1 + c.b2 * c2) ** 2 + (c.b1 * s1 + c.b2 * s2) ** 2;
1952
+ const den = (1 + c.a1 * c1 + c.a2 * c2) ** 2 + (c.a1 * s1 + c.a2 * s2) ** 2;
1953
+ return 10 * Math.log10(num / den);
1954
+ }
1955
+
1956
+ // src/oscillator.ts
1957
+ var TAU = 2 * Math.PI;
1958
+ var PEAK_POINTS = 4096;
1959
+ var TRIANGLE_SUMMED_FROM = 1 / 128;
1960
+ function polyBlep(t, dt) {
1961
+ const d = Math.min(dt, 0.5);
1962
+ if (t < d) {
1963
+ const x = t / d;
1964
+ return x + x - x * x - 1;
1965
+ }
1966
+ if (t > 1 - d) {
1967
+ const x = (t - 1) / d;
1968
+ return x * x + x + x + 1;
1969
+ }
1970
+ return 0;
1971
+ }
1972
+ var sineShape = (phase) => Math.sin(TAU * phase);
1973
+ var STEPPED_LEVEL = 1 / 1.17898;
1974
+ var BUILT_IN = {
1975
+ sine: sineShape,
1976
+ square: (phase, dt) => STEPPED_LEVEL * ((phase < 0.5 ? 1 : -1) + polyBlep(phase, dt) - polyBlep((phase + 0.5) % 1, dt)),
1977
+ sawtooth: (phase, dt) => {
1978
+ const t = (phase + 0.5) % 1;
1979
+ return STEPPED_LEVEL * (2 * t - 1 - polyBlep(t, dt));
1980
+ },
1981
+ triangle: (phase, dt) => {
1982
+ if (dt < TRIANGLE_SUMMED_FROM) return phase < 0.25 ? 4 * phase : phase < 0.75 ? 2 - 4 * phase : 4 * phase - 4;
1983
+ let v = 0;
1984
+ let sign = 1;
1985
+ for (let k = 1; k * dt < 0.5; k += 2) {
1986
+ v += sign * Math.sin(TAU * k * phase) / (k * k);
1987
+ sign = -sign;
1988
+ }
1989
+ return v * 8 / (Math.PI * Math.PI);
1990
+ }
1991
+ };
1992
+ function harmonicShape(levels) {
1993
+ const sum = (phase, count) => {
1994
+ let v = 0;
1995
+ for (let k = 1; k <= count; k += 1) v += levels[k - 1] * Math.sin(TAU * k * phase);
1996
+ return v;
1997
+ };
1998
+ let peak = 0;
1999
+ for (let i = 0; i < PEAK_POINTS; i += 1) peak = Math.max(peak, Math.abs(sum(i / PEAK_POINTS, levels.length)));
2000
+ const scale = peak > 0 ? 1 / peak : 0;
2001
+ return (phase, dt) => scale * sum(phase, Math.min(levels.length, Math.ceil(0.5 / dt) - 1));
2002
+ }
2003
+ function waveShape(wave) {
2004
+ return typeof wave === "string" ? BUILT_IN[wave] : harmonicShape(wave);
2005
+ }
2006
+
2007
+ // src/voiceKernel.ts
2008
+ var tuning = (rate, detune) => rate * 2 ** (detune / 1200);
2009
+ function filterStage(spec, sampleRate) {
2010
+ const type = spec.type ?? "lowpass";
2011
+ const amount = spec.amount ?? 0;
2012
+ const env = spec.envelope && amount !== 0 ? resolveEnvelope(spec.envelope) : null;
2013
+ const coefficients = biquadCoefficients(type, spec.frequency, sampleRate, spec.Q, spec.gain);
2014
+ let cutoff = spec.frequency;
2015
+ let z1 = 0;
2016
+ let z2 = 0;
2017
+ return (x, t, gate) => {
2018
+ if (env) {
2019
+ const hz = spec.frequency + amount * envelopeLevel(env, t, gate);
2020
+ if (hz !== cutoff) {
2021
+ cutoff = hz;
2022
+ biquadCoefficients(type, hz, sampleRate, spec.Q, spec.gain, coefficients);
2023
+ }
2024
+ }
2025
+ const y = coefficients.b0 * x + z1;
2026
+ z1 = coefficients.b1 * x - coefficients.a1 * y + z2;
2027
+ z2 = coefficients.b2 * x - coefficients.a2 * y;
2028
+ return y;
2029
+ };
2030
+ }
2031
+ function createKernel(patch, sampleRate, source) {
2032
+ const env = resolveEnvelope(patch.envelope);
2033
+ const gain = patch.gain ?? 1;
2034
+ const filter = patch.filter ? filterStage(patch.filter, sampleRate) : null;
2035
+ const msPerSample = 1e3 / sampleRate;
2036
+ let gate = patch.duration;
2037
+ let tune = tuning(patch.rate ?? 1, patch.detune ?? 0);
2038
+ let n = 0;
2039
+ let ended = false;
2040
+ return {
2041
+ render(out, from = 0, to = out.length) {
2042
+ if (ended) return false;
2043
+ for (let i = from; i < to; i += 1) {
2044
+ const t = n * msPerSample;
2045
+ if (gate !== void 0 && t >= gate + env.release) {
2046
+ ended = true;
2047
+ return false;
2048
+ }
2049
+ const raw = source(t, tune);
2050
+ out[i] += gain * envelopeLevel(env, t, gate) * (filter ? filter(raw, t, gate) : raw);
2051
+ n += 1;
2052
+ }
2053
+ return true;
2054
+ },
2055
+ release() {
2056
+ const t = n * msPerSample;
2057
+ if (gate === void 0 || t < gate) gate = t;
2058
+ },
2059
+ retune(rate, detune) {
2060
+ tune = tuning(rate, detune);
2061
+ }
2062
+ };
2063
+ }
2064
+ function createNoteKernel(note, sampleRate) {
2065
+ const from = toFrequency(note.pitch);
2066
+ const glideTo = note.glide ? toFrequency(note.glide.to) : void 0;
2067
+ const glideMs = note.glide?.ms ?? note.duration ?? 100;
2068
+ const linear = note.glide?.curve === "linear";
2069
+ const wave = note.wave ?? "sine";
2070
+ const shape = isInharmonic(wave) ? sineShape : waveShape(wave);
2071
+ const tones = isInharmonic(wave) ? resolvePartials(wave).map((p) => ({ ratio: p.ratio, gain: p.gain, decay: p.decay, phase: 0 })) : [{ ratio: 1, gain: 1, phase: 0 }];
2072
+ return createKernel(note, sampleRate, (t, tune) => {
2073
+ let hz = from;
2074
+ if (glideTo !== void 0) {
2075
+ const k = glideMs > 0 ? Math.min(1, t / glideMs) : 1;
2076
+ hz = linear ? from + (glideTo - from) * k : from * (glideTo / from) ** k;
2077
+ }
2078
+ const cycles = hz * tune / sampleRate;
2079
+ let sum = 0;
2080
+ for (const tone of tones) {
2081
+ const dt = cycles * tone.ratio;
2082
+ if (dt >= 0.5) continue;
2083
+ const level = tone.decay === void 0 ? tone.gain : tone.gain * Math.exp(-t / tone.decay);
2084
+ sum += level * shape(tone.phase, dt);
2085
+ tone.phase = (tone.phase + dt) % 1;
2086
+ }
2087
+ return sum;
2088
+ });
2089
+ }
2090
+ var loops = /* @__PURE__ */ new Map();
2091
+ function noiseLoop(color, sampleRate) {
2092
+ const key = `${color}:${sampleRate}`;
2093
+ let loop = loops.get(key);
2094
+ if (!loop) {
2095
+ loop = noiseSamples(color, Math.round(sampleRate * NOISE_SECONDS));
2096
+ loops.set(key, loop);
2097
+ }
2098
+ return loop;
2099
+ }
2100
+ function createNoiseKernel(noise, sampleRate, random = Math.random) {
2101
+ const loop = noiseLoop(noise.noise, sampleRate);
2102
+ let at = Math.floor(random() * loop.length);
2103
+ return createKernel(noise, sampleRate, (_t, tune) => {
2104
+ const i = Math.floor(at);
2105
+ const a = loop[i];
2106
+ const sample = a + (loop[(i + 1) % loop.length] - a) * (at - i);
2107
+ at = (at + tune) % loop.length;
2108
+ return sample;
2109
+ });
2110
+ }
2111
+ function renderWhole(kernel, patch, sampleRate, what) {
2112
+ if (patch.duration === void 0) {
2113
+ throw new RangeError(`@weasel-js/audio: ${what} needs a duration to know where the voice ends`);
2114
+ }
2115
+ const total = patch.duration + resolveEnvelope(patch.envelope).release;
2116
+ const out = new Float32Array(Math.round(total * sampleRate / 1e3));
2117
+ kernel.render(out);
2118
+ return out;
2119
+ }
2120
+ function renderNote(note, sampleRate) {
2121
+ return renderWhole(createNoteKernel(note, sampleRate), note, sampleRate, "renderNote");
2122
+ }
2123
+ function renderNoise(noise, sampleRate, random = Math.random) {
2124
+ return renderWhole(createNoiseKernel(noise, sampleRate, random), noise, sampleRate, "renderNoise");
2125
+ }
2126
+
2127
+ export { SCORE_BAR_STEPS, 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 };
1893
2128
  //# sourceMappingURL=index.js.map
1894
2129
  //# sourceMappingURL=index.js.map