@dawcore/components 0.0.4 → 0.0.7

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.mjs CHANGED
@@ -755,7 +755,12 @@ DawStopButtonElement = __decorateClass([
755
755
  // src/elements/daw-editor.ts
756
756
  import { LitElement as LitElement8, html as html7, css as css7 } from "lit";
757
757
  import { customElement as customElement10, property as property6, state as state3 } from "lit/decorators.js";
758
- import { createClipFromSeconds as createClipFromSeconds2, createTrack as createTrack2, clipPixelWidth } from "@waveform-playlist/core";
758
+ import {
759
+ createClip as createClip2,
760
+ createClipFromSeconds as createClipFromSeconds2,
761
+ createTrack as createTrack2,
762
+ clipPixelWidth
763
+ } from "@waveform-playlist/core";
759
764
 
760
765
  // src/workers/peaksWorker.ts
761
766
  import WaveformData from "waveform-data";
@@ -1617,18 +1622,22 @@ var ViewportController = class {
1617
1622
  };
1618
1623
 
1619
1624
  // src/controllers/audio-resume-controller.ts
1620
- import { resumeGlobalAudioContext } from "@waveform-playlist/playout";
1621
1625
  var AudioResumeController = class {
1622
1626
  constructor(host) {
1623
1627
  this._target = null;
1624
1628
  this._attached = false;
1625
1629
  this._generation = 0;
1626
1630
  this._onGesture = (e) => {
1627
- resumeGlobalAudioContext().catch((err) => {
1628
- console.warn(
1629
- "[dawcore] AudioResumeController: eager resume failed, will retry on play: " + String(err)
1630
- );
1631
- });
1631
+ const ctx = this._host.audioContext;
1632
+ if (ctx.state === "closed") {
1633
+ console.warn("[dawcore] AudioResumeController: AudioContext is closed, cannot resume.");
1634
+ } else if (ctx.state === "suspended") {
1635
+ ctx.resume().catch((err) => {
1636
+ console.warn(
1637
+ "[dawcore] AudioResumeController: eager resume failed, will retry on play: " + String(err)
1638
+ );
1639
+ });
1640
+ }
1632
1641
  const otherType = e.type === "pointerdown" ? "keydown" : "pointerdown";
1633
1642
  this._target?.removeEventListener(otherType, this._onGesture, {
1634
1643
  capture: true
@@ -1696,13 +1705,12 @@ var AudioResumeController = class {
1696
1705
  };
1697
1706
 
1698
1707
  // src/controllers/recording-controller.ts
1699
- import { getGlobalContext } from "@waveform-playlist/playout";
1700
1708
  import { recordingProcessorUrl } from "@waveform-playlist/worklets";
1701
1709
  import { appendPeaks, concatenateAudioData, createAudioBuffer } from "@waveform-playlist/recording";
1702
1710
  var RecordingController = class {
1703
1711
  constructor(host) {
1704
1712
  this._sessions = /* @__PURE__ */ new Map();
1705
- this._workletLoaded = false;
1713
+ this._workletLoadedCtx = null;
1706
1714
  this._host = host;
1707
1715
  host.addController(this);
1708
1716
  }
@@ -1712,6 +1720,7 @@ var RecordingController = class {
1712
1720
  for (const trackId of [...this._sessions.keys()]) {
1713
1721
  this._cleanupSession(trackId);
1714
1722
  }
1723
+ this._workletLoadedCtx = null;
1715
1724
  }
1716
1725
  get isRecording() {
1717
1726
  return this._sessions.size > 0;
@@ -1730,21 +1739,25 @@ var RecordingController = class {
1730
1739
  return;
1731
1740
  }
1732
1741
  const bits = options.bits ?? 16;
1733
- const context = getGlobalContext();
1734
- const rawCtx = context.rawContext;
1735
- this._host.resolveAudioContextSampleRate(rawCtx.sampleRate);
1736
1742
  try {
1737
- if (!this._workletLoaded) {
1743
+ const rawCtx = this._host.audioContext;
1744
+ this._host.resolveAudioContextSampleRate(rawCtx.sampleRate);
1745
+ if (!this._workletLoadedCtx || this._workletLoadedCtx !== rawCtx) {
1738
1746
  await rawCtx.audioWorklet.addModule(recordingProcessorUrl);
1739
- this._workletLoaded = true;
1747
+ this._workletLoadedCtx = rawCtx;
1740
1748
  }
1741
- const channelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount ?? 1;
1749
+ const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount;
1750
+ if (detectedChannelCount === void 0 && options.channelCount !== void 0) {
1751
+ console.warn(
1752
+ "[dawcore] Could not detect stream channel count, using fallback: " + options.channelCount
1753
+ );
1754
+ }
1755
+ const channelCount = detectedChannelCount ?? options.channelCount ?? 1;
1742
1756
  const startSample = options.startSample ?? Math.floor(this._host._currentTime * this._host.effectiveSampleRate);
1743
1757
  const outputLatency = rawCtx.outputLatency ?? 0;
1744
- const lookAhead = context.lookAhead ?? 0;
1745
- const latencySamples = Math.floor((outputLatency + lookAhead) * rawCtx.sampleRate);
1746
- const source = context.createMediaStreamSource(stream);
1747
- const workletNode = context.createAudioWorkletNode("recording-processor", {
1758
+ const latencySamples = Math.floor(outputLatency * rawCtx.sampleRate);
1759
+ const source = rawCtx.createMediaStreamSource(stream);
1760
+ const workletNode = new AudioWorkletNode(rawCtx, "recording-processor", {
1748
1761
  channelCount,
1749
1762
  channelCountMode: "explicit"
1750
1763
  });
@@ -1845,8 +1858,7 @@ var RecordingController = class {
1845
1858
  );
1846
1859
  return;
1847
1860
  }
1848
- const context = getGlobalContext();
1849
- const stopCtx = context.rawContext;
1861
+ const stopCtx = this._host.audioContext;
1850
1862
  const channelData = session.chunks.map((chunkArr) => concatenateAudioData(chunkArr));
1851
1863
  const audioBuffer = createAudioBuffer(
1852
1864
  stopCtx,
@@ -2818,6 +2830,9 @@ var DawEditorElement = class extends LitElement8 {
2818
2830
  this._selectionStartTime = 0;
2819
2831
  this._selectionEndTime = 0;
2820
2832
  this._currentTime = 0;
2833
+ /** Consumer-provided AudioContext. When set, used for decode, playback, and recording. */
2834
+ this._externalAudioContext = null;
2835
+ this._ownedAudioContext = null;
2821
2836
  this._engine = null;
2822
2837
  this._enginePromise = null;
2823
2838
  this._audioCache = /* @__PURE__ */ new Map();
@@ -2963,6 +2978,31 @@ var DawEditorElement = class extends LitElement8 {
2963
2978
  this._samplesPerPixel = clamped;
2964
2979
  this.requestUpdate("samplesPerPixel", old);
2965
2980
  }
2981
+ /** Set an AudioContext to use for all audio operations. Must be set before tracks load. */
2982
+ set audioContext(ctx) {
2983
+ if (ctx && ctx.state === "closed") {
2984
+ console.warn("[dawcore] Provided AudioContext is already closed. Ignoring.");
2985
+ return;
2986
+ }
2987
+ if (this._engine) {
2988
+ console.warn(
2989
+ "[dawcore] audioContext set after engine is built. The engine will continue using the previous context."
2990
+ );
2991
+ }
2992
+ this._externalAudioContext = ctx;
2993
+ }
2994
+ get audioContext() {
2995
+ if (this._externalAudioContext) return this._externalAudioContext;
2996
+ if (!this._ownedAudioContext) {
2997
+ this._ownedAudioContext = new AudioContext({ sampleRate: this.sampleRate });
2998
+ if (this._ownedAudioContext.sampleRate !== this.sampleRate) {
2999
+ console.warn(
3000
+ "[dawcore] Requested sampleRate " + this.sampleRate + " but AudioContext is running at " + this._ownedAudioContext.sampleRate
3001
+ );
3002
+ }
3003
+ }
3004
+ return this._ownedAudioContext;
3005
+ }
2966
3006
  get _clipHandler() {
2967
3007
  return this.interactiveClips ? this._clipPointer : null;
2968
3008
  }
@@ -3068,6 +3108,12 @@ var DawEditorElement = class extends LitElement8 {
3068
3108
  } catch (err) {
3069
3109
  console.warn("[dawcore] Error disposing engine: " + String(err));
3070
3110
  }
3111
+ if (this._ownedAudioContext) {
3112
+ this._ownedAudioContext.close().catch((err) => {
3113
+ console.warn("[dawcore] Error closing AudioContext: " + String(err));
3114
+ });
3115
+ this._ownedAudioContext = null;
3116
+ }
3071
3117
  }
3072
3118
  willUpdate(changedProperties) {
3073
3119
  if (changedProperties.has("eagerResume")) {
@@ -3171,7 +3217,15 @@ var DawEditorElement = class extends LitElement8 {
3171
3217
  let waveformData = null;
3172
3218
  if (waveformDataPromise) {
3173
3219
  try {
3174
- waveformData = await waveformDataPromise;
3220
+ const wd = await waveformDataPromise;
3221
+ const contextRate = this.audioContext.sampleRate;
3222
+ if (wd.sample_rate === contextRate) {
3223
+ waveformData = wd;
3224
+ } else {
3225
+ console.warn(
3226
+ "[dawcore] Pre-computed peaks at " + wd.sample_rate + " Hz do not match AudioContext at " + contextRate + " Hz \u2014 ignoring " + clipDesc.peaksSrc + ", generating from audio"
3227
+ );
3228
+ }
3175
3229
  } catch (err) {
3176
3230
  console.warn(
3177
3231
  "[dawcore] Failed to load peaks from " + clipDesc.peaksSrc + ": " + String(err) + " \u2014 falling back to AudioBuffer generation"
@@ -3179,15 +3233,16 @@ var DawEditorElement = class extends LitElement8 {
3179
3233
  }
3180
3234
  }
3181
3235
  if (waveformData) {
3182
- const clip2 = createClipFromSeconds2({
3236
+ const wdRate = waveformData.sample_rate;
3237
+ const clip2 = createClip2({
3183
3238
  waveformData,
3184
- startTime: clipDesc.start,
3185
- duration: clipDesc.duration || waveformData.duration,
3186
- offset: clipDesc.offset,
3239
+ startSample: Math.round(clipDesc.start * wdRate),
3240
+ durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
3241
+ offsetSamples: Math.round(clipDesc.offset * wdRate),
3187
3242
  gain: clipDesc.gain,
3188
3243
  name: clipDesc.name,
3189
- sampleRate: waveformData.sample_rate,
3190
- sourceDuration: waveformData.duration
3244
+ sampleRate: wdRate,
3245
+ sourceDurationSamples: Math.ceil(waveformData.duration * wdRate)
3191
3246
  });
3192
3247
  const effectiveScale = Math.max(this.samplesPerPixel, waveformData.scale);
3193
3248
  const peakData2 = extractPeaks(
@@ -3307,8 +3362,7 @@ var DawEditorElement = class extends LitElement8 {
3307
3362
  );
3308
3363
  }
3309
3364
  const arrayBuffer = await response.arrayBuffer();
3310
- const { getGlobalAudioContext } = await import("@waveform-playlist/playout");
3311
- return getGlobalAudioContext().decodeAudioData(arrayBuffer);
3365
+ return this.audioContext.decodeAudioData(arrayBuffer);
3312
3366
  })();
3313
3367
  this._audioCache.set(src, promise);
3314
3368
  try {
@@ -3349,11 +3403,11 @@ var DawEditorElement = class extends LitElement8 {
3349
3403
  return this._enginePromise;
3350
3404
  }
3351
3405
  async _buildEngine() {
3352
- const [{ PlaylistEngine }, { createToneAdapter }] = await Promise.all([
3406
+ const [{ PlaylistEngine }, { NativePlayoutAdapter }] = await Promise.all([
3353
3407
  import("@waveform-playlist/engine"),
3354
- import("@waveform-playlist/playout")
3408
+ import("@dawcore/transport")
3355
3409
  ]);
3356
- const adapter = createToneAdapter();
3410
+ const adapter = new NativePlayoutAdapter(this.audioContext);
3357
3411
  const engine = new PlaylistEngine({
3358
3412
  adapter,
3359
3413
  sampleRate: this.effectiveSampleRate,