@waveform-playlist/spectrogram 13.0.0 → 13.0.2

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
@@ -389,7 +389,7 @@ var SpectrogramSettingsModal = ({
389
389
  // src/SpectrogramProvider.tsx
390
390
  import { useState as useState2, useEffect as useEffect2, useRef as useRef2, useCallback, useMemo } from "react";
391
391
  import {
392
- MAX_CANVAS_WIDTH
392
+ MAX_CANVAS_WIDTH as MAX_CANVAS_WIDTH2
393
393
  } from "@waveform-playlist/core";
394
394
  import {
395
395
  getColorMap,
@@ -397,11 +397,11 @@ import {
397
397
  createSpectrogramWorkerPool,
398
398
  SpectrogramAbortError
399
399
  } from "@dawcore/spectrogram";
400
+
401
+ // src/spectrogram-helpers.ts
400
402
  import {
401
- SpectrogramIntegrationProvider
402
- } from "@waveform-playlist/browser";
403
- import { usePlaylistData, usePlaylistControls } from "@waveform-playlist/browser";
404
- import { jsx as jsx3 } from "react/jsx-runtime";
403
+ MAX_CANVAS_WIDTH
404
+ } from "@waveform-playlist/core";
405
405
  function extractChunkNumber(canvasId) {
406
406
  const match = canvasId.match(/chunk(\d+)$/);
407
407
  if (!match) {
@@ -410,6 +410,116 @@ function extractChunkNumber(canvasId) {
410
410
  }
411
411
  return parseInt(match[1], 10);
412
412
  }
413
+ function parseCanvasId(canvasId) {
414
+ const match = canvasId.match(/^(.+)-ch(\d+)-chunk\d+$/);
415
+ if (!match) return null;
416
+ return { clipId: match[1], channelIndex: parseInt(match[2], 10) };
417
+ }
418
+ function groupContiguousIndices(channelInfo, indices) {
419
+ if (indices.length === 0) return [];
420
+ const groups = [];
421
+ let currentGroup = [indices[0]];
422
+ let prevChunk = extractChunkNumber(channelInfo.canvasIds[indices[0]]);
423
+ for (let i = 1; i < indices.length; i++) {
424
+ const chunk = extractChunkNumber(channelInfo.canvasIds[indices[i]]);
425
+ if (chunk === prevChunk + 1) {
426
+ currentGroup.push(indices[i]);
427
+ } else {
428
+ groups.push(currentGroup);
429
+ currentGroup = [indices[i]];
430
+ }
431
+ prevChunk = chunk;
432
+ }
433
+ groups.push(currentGroup);
434
+ return groups;
435
+ }
436
+ function classifyChunkTiers(channelInfo, clipPixelOffset = 0, viewport) {
437
+ if (!viewport) {
438
+ return {
439
+ viewportIndices: channelInfo.canvasWidths.map((_, i) => i),
440
+ bufferIndices: [],
441
+ remainingIndices: []
442
+ };
443
+ }
444
+ const { scrollLeft, viewportWidth } = viewport;
445
+ const buffer = viewportWidth * 1.5;
446
+ const bufferStart = Math.max(0, scrollLeft - buffer);
447
+ const bufferEnd = scrollLeft + viewportWidth + buffer;
448
+ const viewportIndices = [];
449
+ const bufferIndices = [];
450
+ const remainingIndices = [];
451
+ for (let i = 0; i < channelInfo.canvasWidths.length; i++) {
452
+ const chunkNumber = extractChunkNumber(channelInfo.canvasIds[i]);
453
+ const chunkLeft = chunkNumber * MAX_CANVAS_WIDTH + clipPixelOffset;
454
+ const chunkRight = chunkLeft + channelInfo.canvasWidths[i];
455
+ if (chunkRight > scrollLeft && chunkLeft < scrollLeft + viewportWidth) {
456
+ viewportIndices.push(i);
457
+ } else if (chunkRight > bufferStart && chunkLeft < bufferEnd) {
458
+ bufferIndices.push(i);
459
+ } else {
460
+ remainingIndices.push(i);
461
+ }
462
+ }
463
+ return { viewportIndices, bufferIndices, remainingIndices };
464
+ }
465
+ function computeChunkSampleRange({
466
+ channelInfo,
467
+ indices,
468
+ fftSize,
469
+ offsetSamples,
470
+ durationSamples,
471
+ samplesPerPixel
472
+ }) {
473
+ const chunkNumbers = indices.map((i) => extractChunkNumber(channelInfo.canvasIds[i]));
474
+ const minChunk = Math.min(...chunkNumbers);
475
+ const maxChunk = Math.max(...chunkNumbers);
476
+ const maxChunkIdx = indices[chunkNumbers.indexOf(maxChunk)];
477
+ const lastChunkWidth = channelInfo.canvasWidths[maxChunkIdx];
478
+ const startPx = minChunk * MAX_CANVAS_WIDTH;
479
+ const endPx = maxChunk * MAX_CANVAS_WIDTH + lastChunkWidth;
480
+ const rangeStartSample = offsetSamples + Math.floor(startPx * samplesPerPixel);
481
+ const rangeEndSample = Math.min(
482
+ offsetSamples + durationSamples,
483
+ offsetSamples + Math.ceil(endPx * samplesPerPixel)
484
+ );
485
+ const paddedStart = Math.max(offsetSamples, rangeStartSample - fftSize);
486
+ const paddedEnd = Math.min(offsetSamples + durationSamples, rangeEndSample + fftSize);
487
+ return { paddedStart, paddedEnd };
488
+ }
489
+ function resolveRenderMode(override, trackRenderMode) {
490
+ return override?.renderMode ?? trackRenderMode ?? "waveform";
491
+ }
492
+ function toComputeConfig(cfg) {
493
+ return {
494
+ fftSize: cfg?.fftSize,
495
+ hopSize: cfg?.hopSize,
496
+ windowFunction: cfg?.windowFunction,
497
+ alpha: cfg?.alpha,
498
+ zeroPaddingFactor: cfg?.zeroPaddingFactor
499
+ };
500
+ }
501
+ function buildConfigKey(params) {
502
+ const { mode, cfg, cm, mono } = params;
503
+ return JSON.stringify({ mode, cfg, cm, mono });
504
+ }
505
+ function buildFFTKey(params) {
506
+ const { mode, mono, computeConfig } = params;
507
+ return JSON.stringify({ mode, mono, ...computeConfig });
508
+ }
509
+ function mapsDiffer(prev, current) {
510
+ if (current.size !== prev.size) return true;
511
+ for (const [key, value] of current) {
512
+ if (prev.get(key) !== value) return true;
513
+ }
514
+ return false;
515
+ }
516
+
517
+ // src/SpectrogramProvider.tsx
518
+ import {
519
+ SpectrogramIntegrationProvider
520
+ } from "@waveform-playlist/browser";
521
+ import { usePlaylistData, usePlaylistControls } from "@waveform-playlist/browser";
522
+ import { jsx as jsx3 } from "react/jsx-runtime";
413
523
  var SpectrogramProvider = ({
414
524
  config: spectrogramConfig,
415
525
  colorMap: spectrogramColorMap,
@@ -481,40 +591,21 @@ var SpectrogramProvider = ({
481
591
  const currentKeys = /* @__PURE__ */ new Map();
482
592
  const currentFFTKeys = /* @__PURE__ */ new Map();
483
593
  tracks.forEach((track) => {
484
- const mode = trackSpectrogramOverrides.get(track.id)?.renderMode ?? track.renderMode ?? "waveform";
594
+ const override = trackSpectrogramOverrides.get(track.id);
595
+ const mode = resolveRenderMode(override, track.renderMode);
485
596
  if (mode === "waveform") return;
486
- const cfg = trackSpectrogramOverrides.get(track.id)?.config ?? track.spectrogramConfig ?? spectrogramConfig;
487
- const cm = trackSpectrogramOverrides.get(track.id)?.colorMap ?? track.spectrogramColorMap ?? spectrogramColorMap;
488
- currentKeys.set(track.id, JSON.stringify({ mode, cfg, cm, mono }));
489
- const computeConfig = {
490
- fftSize: cfg?.fftSize,
491
- hopSize: cfg?.hopSize,
492
- windowFunction: cfg?.windowFunction,
493
- alpha: cfg?.alpha,
494
- zeroPaddingFactor: cfg?.zeroPaddingFactor
495
- };
496
- currentFFTKeys.set(track.id, JSON.stringify({ mode, mono, ...computeConfig }));
597
+ const cfg = override?.config ?? track.spectrogramConfig ?? spectrogramConfig;
598
+ const cm = override?.colorMap ?? track.spectrogramColorMap ?? spectrogramColorMap;
599
+ currentKeys.set(track.id, buildConfigKey({ mode, cfg, cm, mono }));
600
+ currentFFTKeys.set(
601
+ track.id,
602
+ buildFFTKey({ mode, mono, computeConfig: toComputeConfig(cfg) })
603
+ );
497
604
  });
498
605
  const prevKeys = prevSpectrogramConfigRef.current;
499
606
  const prevFFTKeys = prevSpectrogramFFTKeyRef.current;
500
- let configChanged = currentKeys.size !== prevKeys.size;
501
- if (!configChanged) {
502
- for (const [idx, key] of currentKeys) {
503
- if (prevKeys.get(idx) !== key) {
504
- configChanged = true;
505
- break;
506
- }
507
- }
508
- }
509
- let fftKeyChanged = currentFFTKeys.size !== prevFFTKeys.size;
510
- if (!fftKeyChanged) {
511
- for (const [idx, key] of currentFFTKeys) {
512
- if (prevFFTKeys.get(idx) !== key) {
513
- fftKeyChanged = true;
514
- break;
515
- }
516
- }
517
- }
607
+ const configChanged = mapsDiffer(prevKeys, currentKeys);
608
+ const fftKeyChanged = mapsDiffer(prevFFTKeys, currentFFTKeys);
518
609
  const canvasVersionChanged = spectrogramCanvasVersion !== prevCanvasVersionRef.current;
519
610
  prevCanvasVersionRef.current = spectrogramCanvasVersion;
520
611
  if (!configChanged && !canvasVersionChanged) return;
@@ -549,14 +640,15 @@ var SpectrogramProvider = ({
549
640
  const clipsNeedingFFT = [];
550
641
  const clipsNeedingDisplayOnly = [];
551
642
  tracks.forEach((track, i) => {
552
- const mode = trackSpectrogramOverrides.get(track.id)?.renderMode ?? track.renderMode ?? "waveform";
643
+ const override = trackSpectrogramOverrides.get(track.id);
644
+ const mode = resolveRenderMode(override, track.renderMode);
553
645
  if (mode === "waveform") return;
554
646
  const trackConfigChanged = configChanged && currentKeys.get(track.id) !== prevKeys.get(track.id);
555
647
  const trackFFTChanged = fftKeyChanged && currentFFTKeys.get(track.id) !== prevFFTKeys.get(track.id);
556
648
  const hasRegisteredCanvases = canvasVersionChanged && track.clips.some((clip) => spectrogramCanvasRegistryRef.current.has(clip.id));
557
649
  if (!trackConfigChanged && !hasRegisteredCanvases) return;
558
- const cfg = trackSpectrogramOverrides.get(track.id)?.config ?? track.spectrogramConfig ?? spectrogramConfig ?? {};
559
- const cm = trackSpectrogramOverrides.get(track.id)?.colorMap ?? track.spectrogramColorMap ?? spectrogramColorMap ?? "viridis";
650
+ const cfg = override?.config ?? track.spectrogramConfig ?? spectrogramConfig ?? {};
651
+ const cm = override?.colorMap ?? track.spectrogramColorMap ?? spectrogramColorMap ?? "viridis";
560
652
  for (const clip of track.clips) {
561
653
  if (!clip.audioBuffer) continue;
562
654
  const monoFlag = mono || clip.audioBuffer.numberOfChannels === 1;
@@ -601,34 +693,11 @@ var SpectrogramProvider = ({
601
693
  if (clipsNeedingFFT.length === 0 && clipsNeedingDisplayOnly.length === 0) return;
602
694
  const getVisibleChunkRange = (channelInfo, clipPixelOffset = 0) => {
603
695
  const container = scrollContainerRef.current;
604
- if (!container) {
605
- return {
606
- viewportIndices: channelInfo.canvasWidths.map((_, i) => i),
607
- bufferIndices: [],
608
- remainingIndices: []
609
- };
610
- }
611
- const scrollLeft = container.scrollLeft;
612
- const viewportWidth = container.clientWidth;
613
- const buffer = viewportWidth * 1.5;
614
- const bufferStart = Math.max(0, scrollLeft - buffer);
615
- const bufferEnd = scrollLeft + viewportWidth + buffer;
616
- const viewportIndices = [];
617
- const bufferIndices = [];
618
- const remainingIndices = [];
619
- for (let i = 0; i < channelInfo.canvasWidths.length; i++) {
620
- const chunkNumber = extractChunkNumber(channelInfo.canvasIds[i]);
621
- const chunkLeft = chunkNumber * MAX_CANVAS_WIDTH + clipPixelOffset;
622
- const chunkRight = chunkLeft + channelInfo.canvasWidths[i];
623
- if (chunkRight > scrollLeft && chunkLeft < scrollLeft + viewportWidth) {
624
- viewportIndices.push(i);
625
- } else if (chunkRight > bufferStart && chunkLeft < bufferEnd) {
626
- bufferIndices.push(i);
627
- } else {
628
- remainingIndices.push(i);
629
- }
630
- }
631
- return { viewportIndices, bufferIndices, remainingIndices };
696
+ return classifyChunkTiers(
697
+ channelInfo,
698
+ clipPixelOffset,
699
+ container ? { scrollLeft: container.scrollLeft, viewportWidth: container.clientWidth } : null
700
+ );
632
701
  };
633
702
  const renderChunkSubset = async (api, cacheKey, channelInfo, indices, item, channelIndex, gen) => {
634
703
  if (indices.length === 0) return;
@@ -637,7 +706,7 @@ var SpectrogramProvider = ({
637
706
  const globalPixelOffsets = [];
638
707
  for (const idx of indices) {
639
708
  const chunkNumber = extractChunkNumber(channelInfo.canvasIds[idx]);
640
- globalPixelOffsets.push(chunkNumber * MAX_CANVAS_WIDTH);
709
+ globalPixelOffsets.push(chunkNumber * MAX_CANVAS_WIDTH2);
641
710
  }
642
711
  const colorLUT = getColorMap(item.colorMap);
643
712
  await api.renderChunks(
@@ -661,24 +730,14 @@ var SpectrogramProvider = ({
661
730
  );
662
731
  };
663
732
  const computeFFTForChunks = async (api, channelInfo, indices, item, gen) => {
664
- const chunkNumbers = indices.map((i) => extractChunkNumber(channelInfo.canvasIds[i]));
665
- const minChunk = Math.min(...chunkNumbers);
666
- const maxChunk = Math.max(...chunkNumbers);
667
- const maxChunkIdx = indices[chunkNumbers.indexOf(maxChunk)];
668
- const lastChunkWidth = channelInfo.canvasWidths[maxChunkIdx];
669
- const startPx = minChunk * MAX_CANVAS_WIDTH;
670
- const endPx = maxChunk * MAX_CANVAS_WIDTH + lastChunkWidth;
671
- const windowSize = item.config.fftSize ?? 2048;
672
- const rangeStartSample = item.offsetSamples + Math.floor(startPx * samplesPerPixel);
673
- const rangeEndSample = Math.min(
674
- item.offsetSamples + item.durationSamples,
675
- item.offsetSamples + Math.ceil(endPx * samplesPerPixel)
676
- );
677
- const paddedStart = Math.max(item.offsetSamples, rangeStartSample - windowSize);
678
- const paddedEnd = Math.min(
679
- item.offsetSamples + item.durationSamples,
680
- rangeEndSample + windowSize
681
- );
733
+ const { paddedStart, paddedEnd } = computeChunkSampleRange({
734
+ channelInfo,
735
+ indices,
736
+ fftSize: item.config.fftSize ?? 2048,
737
+ offsetSamples: item.offsetSamples,
738
+ durationSamples: item.durationSamples,
739
+ samplesPerPixel
740
+ });
682
741
  const { cacheKey } = await api.computeFFT(
683
742
  {
684
743
  clipId: item.clipId,
@@ -694,24 +753,6 @@ var SpectrogramProvider = ({
694
753
  );
695
754
  return cacheKey;
696
755
  };
697
- const groupContiguousIndices = (channelInfo, indices) => {
698
- if (indices.length === 0) return [];
699
- const groups = [];
700
- let currentGroup = [indices[0]];
701
- let prevChunk = extractChunkNumber(channelInfo.canvasIds[indices[0]]);
702
- for (let i = 1; i < indices.length; i++) {
703
- const chunk = extractChunkNumber(channelInfo.canvasIds[indices[i]]);
704
- if (chunk === prevChunk + 1) {
705
- currentGroup.push(indices[i]);
706
- } else {
707
- groups.push(currentGroup);
708
- currentGroup = [indices[i]];
709
- }
710
- prevChunk = chunk;
711
- }
712
- groups.push(currentGroup);
713
- return groups;
714
- };
715
756
  const computeAsync = async () => {
716
757
  const abortToken = { aborted: false };
717
758
  backgroundRenderAbortRef.current = abortToken;
@@ -1001,10 +1042,9 @@ var SpectrogramProvider = ({
1001
1042
  );
1002
1043
  }
1003
1044
  }
1004
- const match = canvasId.match(/^(.+)-ch(\d+)-chunk\d+$/);
1005
- if (!match) return;
1006
- const clipId = match[1];
1007
- const channelIndex = parseInt(match[2], 10);
1045
+ const parsed = parseCanvasId(canvasId);
1046
+ if (!parsed) return;
1047
+ const { clipId, channelIndex } = parsed;
1008
1048
  const registry = spectrogramCanvasRegistryRef.current;
1009
1049
  const perClip = registry.get(clipId);
1010
1050
  if (!perClip) return;