@dawcore/components 0.0.17 → 0.0.19

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.mts CHANGED
@@ -3,11 +3,18 @@ import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost
3
3
  import { MidiNoteData, SpectrogramConfig, FadeType, Peaks, Bits, PeakData, MeterEntry, SnapTo, ColorMapValue, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
4
  import WaveformData from 'waveform-data';
5
5
  import { PlayoutAdapter, PlaylistEngine } from '@waveform-playlist/engine';
6
+ import { MidiLoadOptions, MidiLoadResult } from '@dawcore/midi';
6
7
  import { ClipRegistration, CanvasRegistration, ViewportState } from '@dawcore/spectrogram';
7
8
 
8
9
  declare class DawClipElement extends LitElement {
9
10
  src: string;
10
11
  peaksSrc: string;
12
+ /**
13
+ * Timeline position in seconds. JS property only — NOT reflected to the
14
+ * `start` attribute (Lit's `reflect` defaults to false). Tests that need
15
+ * to assert clip position must read this property directly; reading
16
+ * `el.getAttribute('start')` returns `null` regardless of correctness.
17
+ */
11
18
  start: number;
12
19
  duration: number;
13
20
  offset: number;
@@ -824,7 +831,29 @@ interface LoadFilesResult {
824
831
  }>;
825
832
  }
826
833
 
827
- declare class DawEditorElement extends LitElement {
834
+ /**
835
+ * MIDI loading logic extracted from daw-editor. Operates on the editor via a
836
+ * narrow host interface (`addTrack` + `querySelectorAll`) — `<daw-editor>`
837
+ * satisfies it without any new public surface.
838
+ *
839
+ * Numbered steps below match the Data Flow diagram in
840
+ * `docs/specs/2026-05-23-dawcore-load-midi-design.md`.
841
+ */
842
+
843
+ /**
844
+ * Minimal host surface needed by `loadMidiImpl`. `<daw-editor>` satisfies this
845
+ * structurally. `querySelectorAll` is needed for cleanup-on-failure so the
846
+ * loader can identify `<daw-track>` elements appended during this call (both
847
+ * those whose `addTrack` resolved and those that rejected after `_loadTrack`
848
+ * fired `daw-track-error` — the latter aren't in the `addTrack` resolution
849
+ * value, so we need DOM observation to find them).
850
+ */
851
+ interface MidiLoaderHost {
852
+ addTrack(config: TrackConfig): Promise<DawTrackElement>;
853
+ querySelectorAll(selector: string): NodeListOf<Element>;
854
+ }
855
+
856
+ declare class DawEditorElement extends LitElement implements MidiLoaderHost {
828
857
  get samplesPerPixel(): number;
829
858
  set samplesPerPixel(value: number);
830
859
  private _samplesPerPixel;
@@ -979,6 +1008,15 @@ declare class DawEditorElement extends LitElement {
979
1008
  connectedCallback(): void;
980
1009
  disconnectedCallback(): void;
981
1010
  willUpdate(changedProperties: Map<string, unknown>): void;
1011
+ /**
1012
+ * Cache of the last ViewportState forwarded to the spectrogram controller.
1013
+ * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
1014
+ * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
1015
+ * viewport. Skip the cross-controller call when nothing changed.
1016
+ *
1017
+ * The orchestrator dedupes too, but this avoids the call entirely.
1018
+ */
1019
+ private _lastSpectrogramViewport;
982
1020
  protected updated(_changed: Map<string, unknown>): void;
983
1021
  private _onTrackConnected;
984
1022
  private _onTrackRemoved;
@@ -1057,6 +1095,20 @@ declare class DawEditorElement extends LitElement {
1057
1095
  private _onDragLeave;
1058
1096
  private _onDrop;
1059
1097
  loadFiles(files: FileList | File[]): Promise<LoadFilesResult>;
1098
+ /**
1099
+ * Imperatively load a `.mid` file (URL or File) and create N `<daw-track>`
1100
+ * elements — one per note-bearing MIDI track. On any per-track failure,
1101
+ * every `<daw-track>` appended during the call is removed (both successful
1102
+ * and failed) so the editor returns to its pre-call state.
1103
+ *
1104
+ * `options.signal` is forwarded to `fetch()` only for URL sources; aborting
1105
+ * after parsing does not cancel in-flight `addTrack` calls.
1106
+ *
1107
+ * Requires the optional `@dawcore/midi` peer dep — throws with an install
1108
+ * hint (and `console.warn`s the original error) when the dynamic import
1109
+ * fails for any reason.
1110
+ */
1111
+ loadMidi(source: string | File, options?: MidiLoadOptions): Promise<MidiLoadResult>;
1060
1112
  /**
1061
1113
  * Build the engine if it hasn't been built yet. Lets consumers obtain a
1062
1114
  * non-null `editor.engine` before any track has been loaded — useful for
package/dist/index.d.ts CHANGED
@@ -3,11 +3,18 @@ import { LitElement, PropertyValues, ReactiveController, ReactiveControllerHost
3
3
  import { MidiNoteData, SpectrogramConfig, FadeType, Peaks, Bits, PeakData, MeterEntry, SnapTo, ColorMapValue, ClipTrack, KeyboardShortcut } from '@waveform-playlist/core';
4
4
  import WaveformData from 'waveform-data';
5
5
  import { PlayoutAdapter, PlaylistEngine } from '@waveform-playlist/engine';
6
+ import { MidiLoadOptions, MidiLoadResult } from '@dawcore/midi';
6
7
  import { ClipRegistration, CanvasRegistration, ViewportState } from '@dawcore/spectrogram';
7
8
 
8
9
  declare class DawClipElement extends LitElement {
9
10
  src: string;
10
11
  peaksSrc: string;
12
+ /**
13
+ * Timeline position in seconds. JS property only — NOT reflected to the
14
+ * `start` attribute (Lit's `reflect` defaults to false). Tests that need
15
+ * to assert clip position must read this property directly; reading
16
+ * `el.getAttribute('start')` returns `null` regardless of correctness.
17
+ */
11
18
  start: number;
12
19
  duration: number;
13
20
  offset: number;
@@ -824,7 +831,29 @@ interface LoadFilesResult {
824
831
  }>;
825
832
  }
826
833
 
827
- declare class DawEditorElement extends LitElement {
834
+ /**
835
+ * MIDI loading logic extracted from daw-editor. Operates on the editor via a
836
+ * narrow host interface (`addTrack` + `querySelectorAll`) — `<daw-editor>`
837
+ * satisfies it without any new public surface.
838
+ *
839
+ * Numbered steps below match the Data Flow diagram in
840
+ * `docs/specs/2026-05-23-dawcore-load-midi-design.md`.
841
+ */
842
+
843
+ /**
844
+ * Minimal host surface needed by `loadMidiImpl`. `<daw-editor>` satisfies this
845
+ * structurally. `querySelectorAll` is needed for cleanup-on-failure so the
846
+ * loader can identify `<daw-track>` elements appended during this call (both
847
+ * those whose `addTrack` resolved and those that rejected after `_loadTrack`
848
+ * fired `daw-track-error` — the latter aren't in the `addTrack` resolution
849
+ * value, so we need DOM observation to find them).
850
+ */
851
+ interface MidiLoaderHost {
852
+ addTrack(config: TrackConfig): Promise<DawTrackElement>;
853
+ querySelectorAll(selector: string): NodeListOf<Element>;
854
+ }
855
+
856
+ declare class DawEditorElement extends LitElement implements MidiLoaderHost {
828
857
  get samplesPerPixel(): number;
829
858
  set samplesPerPixel(value: number);
830
859
  private _samplesPerPixel;
@@ -979,6 +1008,15 @@ declare class DawEditorElement extends LitElement {
979
1008
  connectedCallback(): void;
980
1009
  disconnectedCallback(): void;
981
1010
  willUpdate(changedProperties: Map<string, unknown>): void;
1011
+ /**
1012
+ * Cache of the last ViewportState forwarded to the spectrogram controller.
1013
+ * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
1014
+ * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
1015
+ * viewport. Skip the cross-controller call when nothing changed.
1016
+ *
1017
+ * The orchestrator dedupes too, but this avoids the call entirely.
1018
+ */
1019
+ private _lastSpectrogramViewport;
982
1020
  protected updated(_changed: Map<string, unknown>): void;
983
1021
  private _onTrackConnected;
984
1022
  private _onTrackRemoved;
@@ -1057,6 +1095,20 @@ declare class DawEditorElement extends LitElement {
1057
1095
  private _onDragLeave;
1058
1096
  private _onDrop;
1059
1097
  loadFiles(files: FileList | File[]): Promise<LoadFilesResult>;
1098
+ /**
1099
+ * Imperatively load a `.mid` file (URL or File) and create N `<daw-track>`
1100
+ * elements — one per note-bearing MIDI track. On any per-track failure,
1101
+ * every `<daw-track>` appended during the call is removed (both successful
1102
+ * and failed) so the editor returns to its pre-call state.
1103
+ *
1104
+ * `options.signal` is forwarded to `fetch()` only for URL sources; aborting
1105
+ * after parsing does not cancel in-flight `addTrack` calls.
1106
+ *
1107
+ * Requires the optional `@dawcore/midi` peer dep — throws with an install
1108
+ * hint (and `console.warn`s the original error) when the dynamic import
1109
+ * fails for any reason.
1110
+ */
1111
+ loadMidi(source: string | File, options?: MidiLoadOptions): Promise<MidiLoadResult>;
1060
1112
  /**
1061
1113
  * Build the engine if it hasn't been built yet. Lets consumers obtain a
1062
1114
  * non-null `editor.engine` before any track has been loaded — useful for
package/dist/index.js CHANGED
@@ -3495,6 +3495,108 @@ async function loadFiles(host, files) {
3495
3495
  return { loaded, failed };
3496
3496
  }
3497
3497
 
3498
+ // src/interactions/midi-loader.ts
3499
+ var INSTALL_HINT = "@dawcore/midi is required for loadMidi(). Install with: npm install @dawcore/midi";
3500
+ async function loadMidiImpl(host, source, options = {}) {
3501
+ const startTime = options.startTime ?? 0;
3502
+ if (!Number.isFinite(startTime) || startTime < 0) {
3503
+ throw new RangeError(
3504
+ "loadMidi: startTime must be a non-negative finite number (got " + String(options.startTime) + ")"
3505
+ );
3506
+ }
3507
+ let midiModule;
3508
+ try {
3509
+ midiModule = await import("@dawcore/midi");
3510
+ } catch (originalErr) {
3511
+ console.warn("[dawcore] @dawcore/midi dynamic import failed: " + String(originalErr));
3512
+ throw new Error(INSTALL_HINT);
3513
+ }
3514
+ const { parseMidiUrl, parseMidiFile } = midiModule;
3515
+ let parsed;
3516
+ if (typeof source === "string") {
3517
+ parsed = await parseMidiUrl(source, void 0, options.signal);
3518
+ } else {
3519
+ let buffer;
3520
+ try {
3521
+ buffer = await source.arrayBuffer();
3522
+ } catch (err) {
3523
+ throw new Error(
3524
+ 'loadMidi: failed to read File "' + source.name + '" (' + source.size + " bytes): " + String(err)
3525
+ );
3526
+ }
3527
+ parsed = parseMidiFile(buffer);
3528
+ }
3529
+ const childrenBefore = new Set(host.querySelectorAll("daw-track"));
3530
+ const settlements = await Promise.allSettled(
3531
+ parsed.tracks.map(
3532
+ (t) => host.addTrack({
3533
+ name: t.name,
3534
+ renderMode: "piano-roll",
3535
+ clips: [
3536
+ {
3537
+ midiNotes: t.notes,
3538
+ midiChannel: t.channel,
3539
+ midiProgram: t.programNumber,
3540
+ start: startTime
3541
+ }
3542
+ ]
3543
+ })
3544
+ )
3545
+ );
3546
+ const succeeded = [];
3547
+ const rejections = [];
3548
+ for (const s of settlements) {
3549
+ if (s.status === "fulfilled") {
3550
+ succeeded.push(s.value);
3551
+ } else {
3552
+ rejections.push(s.reason);
3553
+ }
3554
+ }
3555
+ if (rejections.length > 0) {
3556
+ const appendedTracks = Array.from(host.querySelectorAll("daw-track")).filter(
3557
+ (el) => !childrenBefore.has(el)
3558
+ );
3559
+ for (const el of appendedTracks) {
3560
+ try {
3561
+ el.remove();
3562
+ } catch (cleanupErr) {
3563
+ console.warn("[dawcore] loadMidi cleanup failed for a track: " + String(cleanupErr));
3564
+ }
3565
+ }
3566
+ await Promise.resolve();
3567
+ for (let i = 1; i < rejections.length; i++) {
3568
+ console.warn(
3569
+ "[dawcore] loadMidi: additional track failure (" + i + "): " + stringifyReason(rejections[i])
3570
+ );
3571
+ }
3572
+ const first = rejections[0];
3573
+ if (rejections.length > 1) {
3574
+ const message = "loadMidi: " + rejections.length + " of " + settlements.length + " tracks failed; first: " + (first instanceof Error ? first.message : stringifyReason(first));
3575
+ throw new Error(message);
3576
+ }
3577
+ throw first instanceof Error ? first : new Error(stringifyReason(first));
3578
+ }
3579
+ return {
3580
+ trackIds: succeeded.map((el) => el.trackId),
3581
+ bpm: parsed.bpm,
3582
+ timeSignature: parsed.timeSignature,
3583
+ duration: parsed.duration,
3584
+ name: parsed.name
3585
+ };
3586
+ }
3587
+ function stringifyReason(reason) {
3588
+ if (reason === null) return "null";
3589
+ if (reason === void 0) return "undefined";
3590
+ if (typeof reason === "object") {
3591
+ try {
3592
+ return JSON.stringify(reason);
3593
+ } catch {
3594
+ return Object.prototype.toString.call(reason);
3595
+ }
3596
+ }
3597
+ return String(reason);
3598
+ }
3599
+
3498
3600
  // src/interactions/recording-clip.ts
3499
3601
  var import_core7 = require("@waveform-playlist/core");
3500
3602
  function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
@@ -3828,6 +3930,15 @@ var DawEditorElement = class extends import_lit14.LitElement {
3828
3930
  v.scrollSelector = ".scroll-area";
3829
3931
  return v;
3830
3932
  })();
3933
+ /**
3934
+ * Cache of the last ViewportState forwarded to the spectrogram controller.
3935
+ * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
3936
+ * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
3937
+ * viewport. Skip the cross-controller call when nothing changed.
3938
+ *
3939
+ * The orchestrator dedupes too, but this avoids the call entirely.
3940
+ */
3941
+ this._lastSpectrogramViewport = null;
3831
3942
  // --- Track Events ---
3832
3943
  this._onTrackConnected = (e) => {
3833
3944
  const trackId = e.detail?.trackId;
@@ -4364,7 +4475,11 @@ var DawEditorElement = class extends import_lit14.LitElement {
4364
4475
  if (this._spectrogramController) {
4365
4476
  const vs = this._viewport.visibleStart;
4366
4477
  const ve = this._viewport.visibleEnd;
4478
+ const spp = this._renderSpp;
4367
4479
  if (Number.isFinite(vs) && Number.isFinite(ve)) {
4480
+ const prev = this._lastSpectrogramViewport;
4481
+ if (prev && prev.vs === vs && prev.ve === ve && prev.spp === spp) return;
4482
+ this._lastSpectrogramViewport = { vs, ve, spp };
4368
4483
  const span = ve - vs;
4369
4484
  const bufferPad = span * 0.25;
4370
4485
  this._spectrogramController.setViewport({
@@ -4372,7 +4487,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4372
4487
  visibleEndPx: ve,
4373
4488
  bufferStartPx: Math.max(0, vs - bufferPad),
4374
4489
  bufferEndPx: ve + bufferPad,
4375
- samplesPerPixel: this._renderSpp
4490
+ samplesPerPixel: spp
4376
4491
  });
4377
4492
  }
4378
4493
  }
@@ -5054,6 +5169,22 @@ var DawEditorElement = class extends import_lit14.LitElement {
5054
5169
  async loadFiles(files) {
5055
5170
  return loadFiles(this, files);
5056
5171
  }
5172
+ /**
5173
+ * Imperatively load a `.mid` file (URL or File) and create N `<daw-track>`
5174
+ * elements — one per note-bearing MIDI track. On any per-track failure,
5175
+ * every `<daw-track>` appended during the call is removed (both successful
5176
+ * and failed) so the editor returns to its pre-call state.
5177
+ *
5178
+ * `options.signal` is forwarded to `fetch()` only for URL sources; aborting
5179
+ * after parsing does not cancel in-flight `addTrack` calls.
5180
+ *
5181
+ * Requires the optional `@dawcore/midi` peer dep — throws with an install
5182
+ * hint (and `console.warn`s the original error) when the dynamic import
5183
+ * fails for any reason.
5184
+ */
5185
+ async loadMidi(source, options) {
5186
+ return loadMidiImpl(this, source, options);
5187
+ }
5057
5188
  // --- Programmatic Track API ---
5058
5189
  /**
5059
5190
  * Build the engine if it hasn't been built yet. Lets consumers obtain a