@weasel-js/audio 1.11.1 → 1.13.0-rc.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/dist/index.d.ts CHANGED
@@ -133,8 +133,9 @@ interface GraphNodeDef {
133
133
  readonly output: AudioNode;
134
134
  /** `rampMs` is always passed; the graph supplies its fade when the caller gives none. */
135
135
  readonly params: Readonly<Record<string, (value: number, rampMs: number) => void>>;
136
- /** Sources only. `when` is context seconds. Called when the graph starts, or on add/replace while it runs. A source must be startable again after `stop`. */
137
- start?(when: number): void;
136
+ /** Sources only. `when` is context seconds. Called when the graph starts, or on add/replace while it runs. A source must be startable again after `stop`.
137
+ * A source that stops on its own, as a non-looping file reaching its end does, calls `ended` so the graph can start it again. */
138
+ start?(when: number, ended?: () => void): void;
138
139
  /** Sources only. Stop at `when` (context seconds). */
139
140
  stop?(when: number): void;
140
141
  /** Called once, after the node has faded out of the graph. */
package/dist/index.js CHANGED
@@ -425,7 +425,9 @@ function createFader(ctx, timers, from, to) {
425
425
  gain.connect(to);
426
426
  let cancel = null;
427
427
  let closing = false;
428
+ let closed = false;
428
429
  let cutDone = false;
430
+ const dones = /* @__PURE__ */ new Set();
429
431
  const cut = () => {
430
432
  cancel?.();
431
433
  cancel = null;
@@ -440,14 +442,20 @@ function createFader(ctx, timers, from, to) {
440
442
  writeParam(ctx, gain.gain, 1, ms);
441
443
  },
442
444
  close(ms, done) {
443
- if (closing || cutDone) return;
445
+ if (closed) done?.();
446
+ if (closed || cutDone) return;
447
+ if (done) dones.add(done);
448
+ if (closing) return;
444
449
  closing = true;
445
450
  const length = ctx.state === "running" ? ms : 0;
446
451
  writeParam(ctx, gain.gain, 0, length);
447
452
  cancel = atAudioTime(ctx, timers, ctx.currentTime + length / 1e3, () => {
448
453
  cancel = null;
449
454
  cut();
450
- done?.();
455
+ closed = true;
456
+ const all = [...dones];
457
+ dones.clear();
458
+ for (const d of all) d();
451
459
  });
452
460
  },
453
461
  cut
@@ -481,11 +489,18 @@ function tapWrapper(ctx, w, opts) {
481
489
  return t;
482
490
  }
483
491
  function startWrapper(w, when) {
484
- if (w.running || !w.def.start) return;
492
+ const { def } = w;
493
+ if (w.running || !def.start) return;
485
494
  w.running = true;
486
- w.def.start(when);
495
+ def.start(when, () => {
496
+ if (w.def === def) w.running = false;
497
+ });
487
498
  }
488
499
  function stopWrapper(w, when) {
500
+ for (const o of w.outgoing) {
501
+ if (o.running) o.def.stop?.(when);
502
+ o.running = false;
503
+ }
489
504
  if (!w.running) return;
490
505
  w.running = false;
491
506
  w.def.stop?.(when);
@@ -528,10 +543,7 @@ function replaceDef(ctx, timers, w, next, ms, playing, hooks) {
528
543
  w.outFader = openFader(ctx, timers, next.output, w.out, ms);
529
544
  w.def = next;
530
545
  w.running = false;
531
- if (playing && next.start) {
532
- w.running = true;
533
- next.start(ctx.currentTime);
534
- }
546
+ if (playing) startWrapper(w, ctx.currentTime);
535
547
  let open = old.faders.length;
536
548
  for (const f of old.faders) {
537
549
  f.close(ms, () => {
@@ -3043,13 +3055,67 @@ function impulseResponse(ctx, seconds, opts = {}) {
3043
3055
  return buffer;
3044
3056
  }
3045
3057
 
3058
+ // src/graph/playhead.ts
3059
+ function createPlayhead(duration, loop, initialRate) {
3060
+ let pos = 0;
3061
+ let from = 0;
3062
+ let until = 0;
3063
+ let t0 = 0;
3064
+ let r0 = initialRate;
3065
+ let t1 = 0;
3066
+ let r1 = initialRate;
3067
+ const rateAt = (t) => {
3068
+ if (t >= t1) return r1;
3069
+ return t <= t0 ? r0 : r0 + (r1 - r0) * (t - t0) / (t1 - t0);
3070
+ };
3071
+ const distance = (t) => t <= t1 ? (t - t0) * (r0 + rateAt(t)) / 2 : (t1 - t0) * (r0 + r1) / 2 + (t - t1) * r1;
3072
+ const wrap = (p) => loop ? (p % duration + duration) % duration : Math.min(Math.max(p, 0), duration);
3073
+ const at = (t) => {
3074
+ const x = Math.min(Math.max(t, from), until);
3075
+ return x <= from ? wrap(pos) : wrap(pos + distance(x) - distance(from));
3076
+ };
3077
+ const hold = (t, rate) => {
3078
+ t0 = t;
3079
+ t1 = t;
3080
+ r0 = rate;
3081
+ r1 = rate;
3082
+ };
3083
+ return {
3084
+ start(when, now) {
3085
+ pos = at(until);
3086
+ from = Math.max(when, now);
3087
+ until = Infinity;
3088
+ hold(now, r1);
3089
+ return pos;
3090
+ },
3091
+ stop(when, now) {
3092
+ until = Math.max(when, now, from);
3093
+ },
3094
+ rate(value, rampMs, now) {
3095
+ pos = at(now);
3096
+ from = Math.min(Math.max(now, from), until);
3097
+ r0 = rateAt(now);
3098
+ t0 = now;
3099
+ t1 = now + Math.max(0, rampMs) / 1e3;
3100
+ r1 = value;
3101
+ },
3102
+ rewind(now) {
3103
+ pos = 0;
3104
+ from = now;
3105
+ until = now;
3106
+ }
3107
+ };
3108
+ }
3109
+
3046
3110
  // src/graph/sources.ts
3047
3111
  function sourceShell(ctx, make) {
3048
3112
  const output = ctx.createGain();
3049
3113
  let live = null;
3114
+ const ending = /* @__PURE__ */ new Set();
3050
3115
  return {
3051
3116
  output,
3052
3117
  live: () => live,
3118
+ sounding: () => live ? [live, ...ending] : [...ending],
3053
3119
  /** `onEnd` runs if the node ends while it is still the live one: it played out. */
3054
3120
  begin(when, setup, offset, onEnd) {
3055
3121
  const node = make();
@@ -3070,7 +3136,11 @@ function sourceShell(ctx, make) {
3070
3136
  if (!live) return;
3071
3137
  const node = live;
3072
3138
  live = null;
3073
- node.onended = () => node.disconnect();
3139
+ ending.add(node);
3140
+ node.onended = () => {
3141
+ ending.delete(node);
3142
+ node.disconnect();
3143
+ };
3074
3144
  node.stop(when);
3075
3145
  },
3076
3146
  dispose() {
@@ -3088,42 +3158,34 @@ function createFileSourceNode(ctx, opts) {
3088
3158
  const { buffer } = opts;
3089
3159
  const loop = opts.loop ?? true;
3090
3160
  let rate = 1;
3091
- let offset = 0;
3092
- let startedAt = 0;
3161
+ const head = createPlayhead(buffer.duration, loop, rate);
3093
3162
  const shell = sourceShell(ctx, () => ctx.createBufferSource());
3094
- const advance = (to) => {
3095
- const next = offset + Math.max(0, to - startedAt) * rate;
3096
- offset = loop ? next % buffer.duration : Math.min(next, buffer.duration);
3097
- startedAt = to;
3098
- };
3099
3163
  return {
3100
3164
  inputs: {},
3101
3165
  output: shell.output,
3102
3166
  params: {
3103
3167
  rate(value, rampMs) {
3104
- const src = shell.live();
3105
- if (src) {
3106
- advance(ctx.currentTime);
3107
- writeParam(ctx, src.playbackRate, value, rampMs);
3108
- }
3168
+ for (const src of shell.sounding()) writeParam(ctx, src.playbackRate, value, rampMs);
3169
+ head.rate(value, rampMs, ctx.currentTime);
3109
3170
  rate = value;
3110
3171
  },
3111
3172
  level: shell.level
3112
3173
  },
3113
- start(when) {
3174
+ start(when, ended) {
3114
3175
  if (shell.live()) return;
3115
- startedAt = when;
3176
+ const offset = head.start(when, ctx.currentTime);
3116
3177
  shell.begin(when, (s) => {
3117
3178
  s.buffer = buffer;
3118
3179
  s.loop = loop;
3119
3180
  initParam(ctx, s.playbackRate, rate);
3120
3181
  }, offset, () => {
3121
- offset = 0;
3182
+ head.rewind(ctx.currentTime);
3183
+ ended?.();
3122
3184
  });
3123
3185
  },
3124
3186
  stop(when) {
3125
3187
  if (!shell.live()) return;
3126
- advance(when);
3188
+ head.stop(when, ctx.currentTime);
3127
3189
  shell.end(when);
3128
3190
  },
3129
3191
  dispose: shell.dispose
@@ -3139,25 +3201,23 @@ function createOscillatorNode(ctx, opts) {
3139
3201
  output: shell.output,
3140
3202
  params: {
3141
3203
  frequency(value, rampMs) {
3142
- const osc = shell.live();
3143
- if (osc) writeParam(ctx, osc.frequency, value, rampMs);
3204
+ for (const osc of shell.sounding()) writeParam(ctx, osc.frequency, value, rampMs);
3144
3205
  frequency = value;
3145
3206
  },
3146
3207
  detune(value, rampMs) {
3147
- const osc = shell.live();
3148
- if (osc) writeParam(ctx, osc.detune, value, rampMs);
3208
+ for (const osc of shell.sounding()) writeParam(ctx, osc.detune, value, rampMs);
3149
3209
  detune = value;
3150
3210
  },
3151
3211
  level: shell.level
3152
3212
  },
3153
- start(when) {
3213
+ start(when, ended) {
3154
3214
  if (shell.live()) return;
3155
3215
  shell.begin(when, (osc) => {
3156
3216
  if (typeof wave === "string") osc.type = wave;
3157
3217
  else osc.setPeriodicWave(periodicWave(ctx, wave));
3158
3218
  initParam(ctx, osc.frequency, frequency);
3159
3219
  initParam(ctx, osc.detune, detune);
3160
- });
3220
+ }, void 0, ended);
3161
3221
  },
3162
3222
  stop: shell.end,
3163
3223
  dispose: shell.dispose
@@ -3169,12 +3229,12 @@ function createNoiseNode(ctx, opts) {
3169
3229
  inputs: {},
3170
3230
  output: shell.output,
3171
3231
  params: { level: shell.level },
3172
- start(when) {
3232
+ start(when, ended) {
3173
3233
  if (shell.live()) return;
3174
3234
  shell.begin(when, (src) => {
3175
3235
  src.buffer = noiseBuffer(ctx, opts.color);
3176
3236
  src.loop = true;
3177
- });
3237
+ }, void 0, ended);
3178
3238
  },
3179
3239
  stop: shell.end,
3180
3240
  dispose: shell.dispose