sceyt-chat-react-uikit 1.8.0-beta.5 → 1.8.0-beta.6

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.
Files changed (3) hide show
  1. package/index.js +1169 -273
  2. package/index.modern.js +1169 -273
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -32188,15 +32188,16 @@ class EventEmitter {
32188
32188
  if (!this.listeners[event]) {
32189
32189
  this.listeners[event] = new Set();
32190
32190
  }
32191
- this.listeners[event].add(listener);
32192
32191
  if (options === null || options === void 0 ? void 0 : options.once) {
32193
- const unsubscribeOnce = () => {
32194
- this.un(event, unsubscribeOnce);
32195
- this.un(event, listener);
32192
+ // Create a wrapper that removes itself after being called once
32193
+ const onceWrapper = (...args) => {
32194
+ this.un(event, onceWrapper);
32195
+ listener(...args);
32196
32196
  };
32197
- this.on(event, unsubscribeOnce);
32198
- return unsubscribeOnce;
32197
+ this.listeners[event].add(onceWrapper);
32198
+ return () => this.un(event, onceWrapper);
32199
32199
  }
32200
+ this.listeners[event].add(listener);
32200
32201
  return () => this.un(event, listener);
32201
32202
  }
32202
32203
  /** Unsubscribe from an event */
@@ -32266,8 +32267,13 @@ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _argume
32266
32267
  function decode(audioData, sampleRate) {
32267
32268
  return __awaiter(this, void 0, void 0, function* () {
32268
32269
  const audioCtx = new AudioContext({ sampleRate });
32269
- const decode = audioCtx.decodeAudioData(audioData);
32270
- return decode.finally(() => audioCtx.close());
32270
+ try {
32271
+ return yield audioCtx.decodeAudioData(audioData);
32272
+ }
32273
+ finally {
32274
+ // Ensure AudioContext is always closed, even on synchronous errors
32275
+ audioCtx.close();
32276
+ }
32271
32277
  });
32272
32278
  }
32273
32279
  /** Normalize peaks to -1..1 */
@@ -32291,17 +32297,36 @@ function normalize(channelData) {
32291
32297
  }
32292
32298
  /** Create an audio buffer from pre-decoded audio data */
32293
32299
  function createBuffer(channelData, duration) {
32300
+ // Validate inputs
32301
+ if (!channelData || channelData.length === 0) {
32302
+ throw new Error('channelData must be a non-empty array');
32303
+ }
32304
+ if (duration <= 0) {
32305
+ throw new Error('duration must be greater than 0');
32306
+ }
32294
32307
  // If a single array of numbers is passed, make it an array of arrays
32295
32308
  if (typeof channelData[0] === 'number')
32296
32309
  channelData = [channelData];
32310
+ // Validate channel data after conversion
32311
+ if (!channelData[0] || channelData[0].length === 0) {
32312
+ throw new Error('channelData must contain non-empty channel arrays');
32313
+ }
32297
32314
  // Normalize to -1..1
32298
32315
  normalize(channelData);
32316
+ // Convert to Float32Array for consistency
32317
+ const float32Channels = channelData.map((channel) => channel instanceof Float32Array ? channel : Float32Array.from(channel));
32299
32318
  return {
32300
32319
  duration,
32301
- length: channelData[0].length,
32302
- sampleRate: channelData[0].length / duration,
32303
- numberOfChannels: channelData.length,
32304
- getChannelData: (i) => channelData === null || channelData === void 0 ? void 0 : channelData[i],
32320
+ length: float32Channels[0].length,
32321
+ sampleRate: float32Channels[0].length / duration,
32322
+ numberOfChannels: float32Channels.length,
32323
+ getChannelData: (i) => {
32324
+ const channel = float32Channels[i];
32325
+ if (!channel) {
32326
+ throw new Error(`Channel ${i} not found`);
32327
+ }
32328
+ return channel;
32329
+ },
32305
32330
  copyFromChannel: AudioBuffer.prototype.copyFromChannel,
32306
32331
  copyToChannel: AudioBuffer.prototype.copyToChannel,
32307
32332
  };
@@ -32370,28 +32395,26 @@ function watchProgress(response, progressCallback) {
32370
32395
  const contentLength = Number(response.headers.get('Content-Length')) || 0;
32371
32396
  let receivedLength = 0;
32372
32397
  // Process the data
32373
- const processChunk = (value) => __awaiter$1(this, void 0, void 0, function* () {
32398
+ const processChunk = (value) => {
32374
32399
  // Add to the received length
32375
32400
  receivedLength += (value === null || value === void 0 ? void 0 : value.length) || 0;
32376
32401
  const percentage = Math.round((receivedLength / contentLength) * 100);
32377
32402
  progressCallback(percentage);
32378
- });
32379
- const read = () => __awaiter$1(this, void 0, void 0, function* () {
32380
- let data;
32381
- try {
32382
- data = yield reader.read();
32383
- }
32384
- catch (_a) {
32385
- // Ignore errors because we can only handle the main response
32386
- return;
32387
- }
32388
- // Continue reading data until done
32389
- if (!data.done) {
32403
+ };
32404
+ // Use iteration instead of recursion to avoid stack issues
32405
+ try {
32406
+ while (true) {
32407
+ const data = yield reader.read();
32408
+ if (data.done) {
32409
+ break;
32410
+ }
32390
32411
  processChunk(data.value);
32391
- yield read();
32392
32412
  }
32393
- });
32394
- read();
32413
+ }
32414
+ catch (err) {
32415
+ // Ignore errors because we can only handle the main response
32416
+ console.warn('Progress tracking error:', err);
32417
+ }
32395
32418
  });
32396
32419
  }
32397
32420
  function fetchBlob(url, progressCallback, requestInit) {
@@ -32410,6 +32433,121 @@ const Fetcher = {
32410
32433
  fetchBlob,
32411
32434
  };
32412
32435
 
32436
+ /**
32437
+ * Reactive primitives for managing state in WaveSurfer
32438
+ *
32439
+ * This module provides signal-based reactivity similar to SolidJS signals.
32440
+ * Signals are reactive values that notify subscribers when they change.
32441
+ */
32442
+ /**
32443
+ * Create a reactive signal that notifies subscribers when its value changes
32444
+ *
32445
+ * @example
32446
+ * ```typescript
32447
+ * const count = signal(0)
32448
+ * count.subscribe(val => console.log('Count:', val))
32449
+ * count.set(5) // Logs: Count: 5
32450
+ * ```
32451
+ */
32452
+ function signal(initialValue) {
32453
+ let _value = initialValue;
32454
+ const subscribers = new Set();
32455
+ return {
32456
+ get value() {
32457
+ return _value;
32458
+ },
32459
+ set(newValue) {
32460
+ // Only update and notify if value actually changed
32461
+ if (!Object.is(_value, newValue)) {
32462
+ _value = newValue;
32463
+ subscribers.forEach((fn) => fn(_value));
32464
+ }
32465
+ },
32466
+ update(fn) {
32467
+ this.set(fn(_value));
32468
+ },
32469
+ subscribe(callback) {
32470
+ subscribers.add(callback);
32471
+ return () => subscribers.delete(callback);
32472
+ },
32473
+ };
32474
+ }
32475
+ /**
32476
+ * Create a computed value that automatically updates when its dependencies change
32477
+ *
32478
+ * @example
32479
+ * ```typescript
32480
+ * const count = signal(0)
32481
+ * const doubled = computed(() => count.value * 2, [count])
32482
+ * console.log(doubled.value) // 0
32483
+ * count.set(5)
32484
+ * console.log(doubled.value) // 10
32485
+ * ```
32486
+ */
32487
+ function computed(fn, dependencies) {
32488
+ const result = signal(fn());
32489
+ // Subscribe to all dependencies immediately
32490
+ // This ensures the computed value stays in sync even if no one is subscribed to it
32491
+ dependencies.forEach((dep) => dep.subscribe(() => {
32492
+ const newValue = fn();
32493
+ // Update the result signal, which will notify our subscribers if value changed
32494
+ if (!Object.is(result.value, newValue)) {
32495
+ result.set(newValue);
32496
+ }
32497
+ }));
32498
+ // Return a read-only signal that proxies the result
32499
+ return {
32500
+ get value() {
32501
+ return result.value;
32502
+ },
32503
+ subscribe(callback) {
32504
+ // Just subscribe to result changes
32505
+ return result.subscribe(callback);
32506
+ },
32507
+ };
32508
+ }
32509
+ /**
32510
+ * Run a side effect automatically when dependencies change
32511
+ *
32512
+ * @param fn - Effect function. Can return a cleanup function.
32513
+ * @param dependencies - Signals that trigger the effect when they change
32514
+ * @returns Unsubscribe function that stops the effect and runs cleanup
32515
+ *
32516
+ * @example
32517
+ * ```typescript
32518
+ * const count = signal(0)
32519
+ * effect(() => {
32520
+ * console.log('Count is:', count.value)
32521
+ * return () => console.log('Cleanup')
32522
+ * }, [count])
32523
+ * count.set(5) // Logs: Cleanup, Count is: 5
32524
+ * ```
32525
+ */
32526
+ function effect(fn, dependencies) {
32527
+ let cleanup;
32528
+ const run = () => {
32529
+ // Run cleanup from previous execution
32530
+ if (cleanup) {
32531
+ cleanup();
32532
+ cleanup = undefined;
32533
+ }
32534
+ // Run effect and capture new cleanup
32535
+ cleanup = fn();
32536
+ };
32537
+ // Subscribe to all dependencies
32538
+ const unsubscribes = dependencies.map((dep) => dep.subscribe(run));
32539
+ // Run effect immediately
32540
+ run();
32541
+ // Return function that unsubscribes and runs cleanup
32542
+ return () => {
32543
+ if (cleanup) {
32544
+ cleanup();
32545
+ cleanup = undefined;
32546
+ }
32547
+ unsubscribes.forEach((unsub) => unsub());
32548
+ };
32549
+ }
32550
+
32413
32551
  var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
32414
32552
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
32415
32553
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -32420,9 +32558,33 @@ var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _argu
32420
32558
  });
32421
32559
  };
32422
32560
  class Player extends EventEmitter {
32561
+ // Expose reactive state as writable signals
32562
+ // These are writable to allow WaveSurfer to compose them into centralized state
32563
+ get isPlayingSignal() {
32564
+ return this._isPlaying;
32565
+ }
32566
+ get currentTimeSignal() {
32567
+ return this._currentTime;
32568
+ }
32569
+ get durationSignal() {
32570
+ return this._duration;
32571
+ }
32572
+ get volumeSignal() {
32573
+ return this._volume;
32574
+ }
32575
+ get mutedSignal() {
32576
+ return this._muted;
32577
+ }
32578
+ get playbackRateSignal() {
32579
+ return this._playbackRate;
32580
+ }
32581
+ get seekingSignal() {
32582
+ return this._seeking;
32583
+ }
32423
32584
  constructor(options) {
32424
32585
  super();
32425
32586
  this.isExternalMedia = false;
32587
+ this.reactiveMediaEventCleanups = [];
32426
32588
  if (options.media) {
32427
32589
  this.media = options.media;
32428
32590
  this.isExternalMedia = true;
@@ -32430,6 +32592,16 @@ class Player extends EventEmitter {
32430
32592
  else {
32431
32593
  this.media = document.createElement('audio');
32432
32594
  }
32595
+ // Initialize reactive state
32596
+ this._isPlaying = signal(false);
32597
+ this._currentTime = signal(0);
32598
+ this._duration = signal(0);
32599
+ this._volume = signal(this.media.volume);
32600
+ this._muted = signal(this.media.muted);
32601
+ this._playbackRate = signal(this.media.playbackRate || 1);
32602
+ this._seeking = signal(false);
32603
+ // Setup reactive media event handlers
32604
+ this.setupReactiveMediaEvents();
32433
32605
  // Controls
32434
32606
  if (options.mediaControls) {
32435
32607
  this.media.controls = true;
@@ -32447,6 +32619,48 @@ class Player extends EventEmitter {
32447
32619
  }, { once: true });
32448
32620
  }
32449
32621
  }
32622
+ /**
32623
+ * Setup reactive media event handlers that update signals
32624
+ * This bridges the imperative HTMLMediaElement API to reactive state
32625
+ */
32626
+ setupReactiveMediaEvents() {
32627
+ // Playing state
32628
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('play', () => {
32629
+ this._isPlaying.set(true);
32630
+ }));
32631
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('pause', () => {
32632
+ this._isPlaying.set(false);
32633
+ }));
32634
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('ended', () => {
32635
+ this._isPlaying.set(false);
32636
+ }));
32637
+ // Time tracking
32638
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('timeupdate', () => {
32639
+ this._currentTime.set(this.media.currentTime);
32640
+ }));
32641
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('durationchange', () => {
32642
+ this._duration.set(this.media.duration || 0);
32643
+ }));
32644
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('loadedmetadata', () => {
32645
+ this._duration.set(this.media.duration || 0);
32646
+ }));
32647
+ // Seeking state
32648
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('seeking', () => {
32649
+ this._seeking.set(true);
32650
+ }));
32651
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('seeked', () => {
32652
+ this._seeking.set(false);
32653
+ }));
32654
+ // Volume and muted
32655
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('volumechange', () => {
32656
+ this._volume.set(this.media.volume);
32657
+ this._muted.set(this.media.muted);
32658
+ }));
32659
+ // Playback rate
32660
+ this.reactiveMediaEventCleanups.push(this.onMediaEvent('ratechange', () => {
32661
+ this._playbackRate.set(this.media.playbackRate);
32662
+ }));
32663
+ }
32450
32664
  onMediaEvent(event, callback, options) {
32451
32665
  this.media.addEventListener(event, callback, options);
32452
32666
  return () => this.media.removeEventListener(event, callback, options);
@@ -32483,17 +32697,27 @@ class Player extends EventEmitter {
32483
32697
  }
32484
32698
  }
32485
32699
  destroy() {
32700
+ // Cleanup reactive media event listeners
32701
+ this.reactiveMediaEventCleanups.forEach((cleanup) => cleanup());
32702
+ this.reactiveMediaEventCleanups = [];
32486
32703
  if (this.isExternalMedia)
32487
32704
  return;
32488
32705
  this.media.pause();
32489
- this.media.remove();
32490
32706
  this.revokeSrc();
32491
32707
  this.media.removeAttribute('src');
32492
32708
  // Load resets the media element to its initial state
32493
32709
  this.media.load();
32710
+ // Remove from DOM after cleanup
32711
+ this.media.remove();
32494
32712
  }
32495
32713
  setMediaElement(element) {
32714
+ // Cleanup reactive event listeners from old media element
32715
+ this.reactiveMediaEventCleanups.forEach((cleanup) => cleanup());
32716
+ this.reactiveMediaEventCleanups = [];
32717
+ // Set new media element
32496
32718
  this.media = element;
32719
+ // Reinitialize reactive event listeners on new media element
32720
+ this.setupReactiveMediaEvents();
32497
32721
  }
32498
32722
  /** Start playing the audio */
32499
32723
  play() {
@@ -32573,23 +32797,312 @@ class Player extends EventEmitter {
32573
32797
  }
32574
32798
  }
32575
32799
 
32576
- function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mouseButton = 0, touchDelay = 100) {
32577
- if (!element)
32578
- return () => void 0;
32800
+ const DEFAULT_HEIGHT = 128;
32801
+ const MAX_CANVAS_WIDTH = 8000;
32802
+ const MAX_NODES = 10;
32803
+ function clampToUnit(value) {
32804
+ if (value < 0)
32805
+ return 0;
32806
+ if (value > 1)
32807
+ return 1;
32808
+ return value;
32809
+ }
32810
+ function calculateBarRenderConfig({ width, height, length, options, pixelRatio, }) {
32811
+ const halfHeight = height / 2;
32812
+ const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
32813
+ const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
32814
+ const barRadius = options.barRadius || 0;
32815
+ const barMinHeight = options.barMinHeight ? options.barMinHeight * pixelRatio : 0;
32816
+ const spacing = barWidth + barGap || 1;
32817
+ const barIndexScale = length > 0 ? width / spacing / length : 0;
32818
+ return {
32819
+ halfHeight,
32820
+ barWidth,
32821
+ barGap,
32822
+ barRadius,
32823
+ barMinHeight,
32824
+ barIndexScale,
32825
+ barSpacing: spacing,
32826
+ };
32827
+ }
32828
+ function calculateBarHeights({ maxTop, maxBottom, halfHeight, vScale, barMinHeight = 0, barAlign, }) {
32829
+ let topHeight = Math.round(maxTop * halfHeight * vScale);
32830
+ const bottomHeight = Math.round(maxBottom * halfHeight * vScale);
32831
+ let totalHeight = topHeight + bottomHeight || 1;
32832
+ if (totalHeight < barMinHeight) {
32833
+ totalHeight = barMinHeight;
32834
+ if (!barAlign) {
32835
+ topHeight = totalHeight / 2;
32836
+ }
32837
+ }
32838
+ return { topHeight, totalHeight };
32839
+ }
32840
+ function resolveBarYPosition({ barAlign, halfHeight, topHeight, totalHeight, canvasHeight, }) {
32841
+ if (barAlign === 'top')
32842
+ return 0;
32843
+ if (barAlign === 'bottom')
32844
+ return canvasHeight - totalHeight;
32845
+ return halfHeight - topHeight;
32846
+ }
32847
+ function calculateBarSegments({ channelData, barIndexScale, barSpacing, barWidth, halfHeight, vScale, canvasHeight, barAlign, barMinHeight, }) {
32848
+ const topChannel = channelData[0] || [];
32849
+ const bottomChannel = channelData[1] || topChannel;
32850
+ const length = topChannel.length;
32851
+ const segments = [];
32852
+ let prevX = 0;
32853
+ let maxTop = 0;
32854
+ let maxBottom = 0;
32855
+ for (let i = 0; i <= length; i++) {
32856
+ const x = Math.round(i * barIndexScale);
32857
+ if (x > prevX) {
32858
+ const { topHeight, totalHeight } = calculateBarHeights({
32859
+ maxTop,
32860
+ maxBottom,
32861
+ halfHeight,
32862
+ vScale,
32863
+ barMinHeight,
32864
+ barAlign,
32865
+ });
32866
+ const y = resolveBarYPosition({
32867
+ barAlign,
32868
+ halfHeight,
32869
+ topHeight,
32870
+ totalHeight,
32871
+ canvasHeight,
32872
+ });
32873
+ segments.push({
32874
+ x: prevX * barSpacing,
32875
+ y,
32876
+ width: barWidth,
32877
+ height: totalHeight,
32878
+ });
32879
+ prevX = x;
32880
+ maxTop = 0;
32881
+ maxBottom = 0;
32882
+ }
32883
+ const magnitudeTop = Math.abs(topChannel[i] || 0);
32884
+ const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
32885
+ if (magnitudeTop > maxTop)
32886
+ maxTop = magnitudeTop;
32887
+ if (magnitudeBottom > maxBottom)
32888
+ maxBottom = magnitudeBottom;
32889
+ }
32890
+ return segments;
32891
+ }
32892
+ function getRelativePointerPosition(rect, clientX, clientY) {
32893
+ const x = clientX - rect.left;
32894
+ const y = clientY - rect.top;
32895
+ const relativeX = x / rect.width;
32896
+ const relativeY = y / rect.height;
32897
+ return [relativeX, relativeY];
32898
+ }
32899
+ function resolveChannelHeight({ optionsHeight, optionsSplitChannels, parentHeight, numberOfChannels, defaultHeight = DEFAULT_HEIGHT, }) {
32900
+ if (optionsHeight == null)
32901
+ return defaultHeight;
32902
+ const numericHeight = Number(optionsHeight);
32903
+ if (!isNaN(numericHeight))
32904
+ return numericHeight;
32905
+ if (optionsHeight === 'auto') {
32906
+ const height = parentHeight || defaultHeight;
32907
+ if (optionsSplitChannels === null || optionsSplitChannels === void 0 ? void 0 : optionsSplitChannels.every((channel) => !channel.overlay)) {
32908
+ return height / numberOfChannels;
32909
+ }
32910
+ return height;
32911
+ }
32912
+ return defaultHeight;
32913
+ }
32914
+ function getPixelRatio(devicePixelRatio) {
32915
+ return Math.max(1, devicePixelRatio || 1);
32916
+ }
32917
+ function shouldRenderBars(options) {
32918
+ return Boolean(options.barWidth || options.barGap || options.barAlign);
32919
+ }
32920
+ function resolveColorValue(color, devicePixelRatio, canvasHeight) {
32921
+ if (!Array.isArray(color))
32922
+ return color || '';
32923
+ if (color.length === 0)
32924
+ return '#999';
32925
+ if (color.length < 2)
32926
+ return color[0] || '';
32927
+ const canvasElement = document.createElement('canvas');
32928
+ const ctx = canvasElement.getContext('2d');
32929
+ const gradientHeight = canvasHeight !== null && canvasHeight !== void 0 ? canvasHeight : canvasElement.height * devicePixelRatio;
32930
+ const gradient = ctx.createLinearGradient(0, 0, 0, gradientHeight || devicePixelRatio);
32931
+ const colorStopPercentage = 1 / (color.length - 1);
32932
+ color.forEach((value, index) => {
32933
+ gradient.addColorStop(index * colorStopPercentage, value);
32934
+ });
32935
+ return gradient;
32936
+ }
32937
+ function calculateWaveformLayout({ duration, minPxPerSec = 0, parentWidth, fillParent, pixelRatio, }) {
32938
+ const scrollWidth = Math.ceil(duration * minPxPerSec);
32939
+ const isScrollable = scrollWidth > parentWidth;
32940
+ const useParentWidth = Boolean(fillParent && !isScrollable);
32941
+ const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
32942
+ return {
32943
+ scrollWidth,
32944
+ isScrollable,
32945
+ useParentWidth,
32946
+ width,
32947
+ };
32948
+ }
32949
+ function clampWidthToBarGrid(width, options) {
32950
+ if (!shouldRenderBars(options))
32951
+ return width;
32952
+ const barWidth = options.barWidth || 0.5;
32953
+ const barGap = options.barGap || barWidth / 2;
32954
+ const totalBarWidth = barWidth + barGap;
32955
+ if (totalBarWidth === 0)
32956
+ return width;
32957
+ return Math.floor(width / totalBarWidth) * totalBarWidth;
32958
+ }
32959
+ function calculateSingleCanvasWidth({ clientWidth, totalWidth, options, }) {
32960
+ const baseWidth = Math.min(MAX_CANVAS_WIDTH, clientWidth, totalWidth);
32961
+ return clampWidthToBarGrid(baseWidth, options);
32962
+ }
32963
+ function sliceChannelData({ channelData, offset, clampedWidth, totalWidth, }) {
32964
+ return channelData.map((channel) => {
32965
+ const start = Math.floor((offset / totalWidth) * channel.length);
32966
+ const end = Math.floor(((offset + clampedWidth) / totalWidth) * channel.length);
32967
+ return channel.slice(start, end);
32968
+ });
32969
+ }
32970
+ function shouldClearCanvases(currentNodeCount) {
32971
+ return currentNodeCount > MAX_NODES;
32972
+ }
32973
+ function getLazyRenderRange({ scrollLeft, totalWidth, numCanvases, }) {
32974
+ if (totalWidth === 0)
32975
+ return [0];
32976
+ const viewPosition = scrollLeft / totalWidth;
32977
+ const startCanvas = Math.floor(viewPosition * numCanvases);
32978
+ return [startCanvas - 1, startCanvas, startCanvas + 1];
32979
+ }
32980
+ function calculateVerticalScale({ channelData, barHeight, normalize, maxPeak, }) {
32981
+ var _a;
32982
+ const baseScale = barHeight || 1;
32983
+ if (!normalize)
32984
+ return baseScale;
32985
+ const firstChannel = channelData[0];
32986
+ if (!firstChannel || firstChannel.length === 0)
32987
+ return baseScale;
32988
+ // Use fixed max peak if provided, otherwise calculate from data
32989
+ let max = maxPeak !== null && maxPeak !== void 0 ? maxPeak : 0;
32990
+ if (!maxPeak) {
32991
+ for (let i = 0; i < firstChannel.length; i++) {
32992
+ const value = (_a = firstChannel[i]) !== null && _a !== void 0 ? _a : 0;
32993
+ const magnitude = Math.abs(value);
32994
+ if (magnitude > max)
32995
+ max = magnitude;
32996
+ }
32997
+ }
32998
+ if (!max)
32999
+ return baseScale;
33000
+ return baseScale / max;
33001
+ }
33002
+ function calculateLinePaths({ channelData, width, height, vScale, }) {
33003
+ const halfHeight = height / 2;
33004
+ const primaryChannel = channelData[0] || [];
33005
+ const secondaryChannel = channelData[1] || primaryChannel;
33006
+ const channels = [primaryChannel, secondaryChannel];
33007
+ return channels.map((channel, index) => {
33008
+ const length = channel.length;
33009
+ const hScale = length ? width / length : 0;
33010
+ const baseY = halfHeight;
33011
+ const direction = index === 0 ? -1 : 1;
33012
+ const path = [{ x: 0, y: baseY }];
33013
+ let prevX = 0;
33014
+ let max = 0;
33015
+ for (let i = 0; i <= length; i++) {
33016
+ const x = Math.round(i * hScale);
33017
+ if (x > prevX) {
33018
+ const heightDelta = Math.round(max * halfHeight * vScale) || 1;
33019
+ const y = baseY + heightDelta * direction;
33020
+ path.push({ x: prevX, y });
33021
+ prevX = x;
33022
+ max = 0;
33023
+ }
33024
+ const value = Math.abs(channel[i] || 0);
33025
+ if (value > max)
33026
+ max = value;
33027
+ }
33028
+ path.push({ x: prevX, y: baseY });
33029
+ return path;
33030
+ });
33031
+ }
33032
+ function roundToHalfAwayFromZero(value) {
33033
+ const scaled = value * 2;
33034
+ const rounded = scaled < 0 ? Math.floor(scaled) : Math.ceil(scaled);
33035
+ return rounded / 2;
33036
+ }
33037
+
33038
+ /**
33039
+ * Event stream utilities for converting DOM events to reactive signals
33040
+ *
33041
+ * These utilities allow composing event handling using reactive primitives.
33042
+ */
33043
+ /**
33044
+ * Cleanup a stream created with event stream utilities
33045
+ *
33046
+ * This removes event listeners and unsubscribes from sources.
33047
+ */
33048
+ function cleanup(stream) {
33049
+ const cleanupFn = stream._cleanup;
33050
+ if (typeof cleanupFn === 'function') {
33051
+ cleanupFn();
33052
+ }
33053
+ }
33054
+
33055
+ /**
33056
+ * Reactive drag stream utilities
33057
+ *
33058
+ * Provides declarative drag handling using reactive streams.
33059
+ * Automatically handles mouseup cleanup and supports constraints.
33060
+ */
33061
+ /**
33062
+ * Create a reactive drag stream from an element
33063
+ *
33064
+ * Emits drag events (start, move, end) as the user drags the element.
33065
+ * Automatically handles pointer capture, multi-touch prevention, and cleanup.
33066
+ *
33067
+ * @example
33068
+ * ```typescript
33069
+ * const dragSignal = createDragStream(element)
33070
+ *
33071
+ * effect(() => {
33072
+ * const drag = dragSignal.value
33073
+ * if (drag?.type === 'move') {
33074
+ * console.log('Dragging:', drag.deltaX, drag.deltaY)
33075
+ * }
33076
+ * }, [dragSignal])
33077
+ * ```
33078
+ *
33079
+ * @param element - Element to make draggable
33080
+ * @param options - Drag configuration options
33081
+ * @returns Signal emitting drag events and cleanup function
33082
+ */
33083
+ function createDragStream(element, options = {}) {
33084
+ const { threshold = 3, mouseButton = 0, touchDelay = 100 } = options;
33085
+ const dragSignal = signal(null);
33086
+ const activePointers = new Map();
32579
33087
  const isTouchDevice = matchMedia('(pointer: coarse)').matches;
32580
33088
  let unsubscribeDocument = () => void 0;
32581
33089
  const onPointerDown = (event) => {
32582
33090
  if (event.button !== mouseButton)
32583
33091
  return;
32584
- event.preventDefault();
32585
- event.stopPropagation();
33092
+ activePointers.set(event.pointerId, event);
33093
+ if (activePointers.size > 1) {
33094
+ return;
33095
+ }
32586
33096
  let startX = event.clientX;
32587
33097
  let startY = event.clientY;
32588
33098
  let isDragging = false;
32589
33099
  const touchStartTime = Date.now();
33100
+ const rect = element.getBoundingClientRect();
33101
+ const { left, top } = rect;
32590
33102
  const onPointerMove = (event) => {
32591
- event.preventDefault();
32592
- event.stopPropagation();
33103
+ if (event.defaultPrevented || activePointers.size > 1) {
33104
+ return;
33105
+ }
32593
33106
  if (isTouchDevice && Date.now() - touchStartTime < touchDelay)
32594
33107
  return;
32595
33108
  const x = event.clientX;
@@ -32597,29 +33110,45 @@ function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mouseButt
32597
33110
  const dx = x - startX;
32598
33111
  const dy = y - startY;
32599
33112
  if (isDragging || Math.abs(dx) > threshold || Math.abs(dy) > threshold) {
32600
- const rect = element.getBoundingClientRect();
32601
- const { left, top } = rect;
33113
+ event.preventDefault();
33114
+ event.stopPropagation();
32602
33115
  if (!isDragging) {
32603
- onStart === null || onStart === void 0 ? void 0 : onStart(startX - left, startY - top);
33116
+ // Emit start event
33117
+ dragSignal.set({
33118
+ type: 'start',
33119
+ x: startX - left,
33120
+ y: startY - top,
33121
+ });
32604
33122
  isDragging = true;
32605
33123
  }
32606
- onDrag(dx, dy, x - left, y - top);
33124
+ // Emit move event
33125
+ dragSignal.set({
33126
+ type: 'move',
33127
+ x: x - left,
33128
+ y: y - top,
33129
+ deltaX: dx,
33130
+ deltaY: dy,
33131
+ });
32607
33132
  startX = x;
32608
33133
  startY = y;
32609
33134
  }
32610
33135
  };
32611
33136
  const onPointerUp = (event) => {
33137
+ activePointers.delete(event.pointerId);
32612
33138
  if (isDragging) {
32613
33139
  const x = event.clientX;
32614
33140
  const y = event.clientY;
32615
- const rect = element.getBoundingClientRect();
32616
- const { left, top } = rect;
32617
- onEnd === null || onEnd === void 0 ? void 0 : onEnd(x - left, y - top);
33141
+ // Emit end event
33142
+ dragSignal.set({
33143
+ type: 'end',
33144
+ x: x - left,
33145
+ y: y - top,
33146
+ });
32618
33147
  }
32619
33148
  unsubscribeDocument();
32620
33149
  };
32621
33150
  const onPointerLeave = (e) => {
32622
- // Listen to events only on the document and not on inner elements
33151
+ activePointers.delete(e.pointerId);
32623
33152
  if (!e.relatedTarget || e.relatedTarget === document.documentElement) {
32624
33153
  onPointerUp(e);
32625
33154
  }
@@ -32631,6 +33160,9 @@ function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mouseButt
32631
33160
  }
32632
33161
  };
32633
33162
  const onTouchMove = (event) => {
33163
+ if (event.defaultPrevented || activePointers.size > 1) {
33164
+ return;
33165
+ }
32634
33166
  if (isDragging) {
32635
33167
  event.preventDefault();
32636
33168
  }
@@ -32653,9 +33185,114 @@ function makeDraggable(element, onDrag, onStart, onEnd, threshold = 3, mouseButt
32653
33185
  };
32654
33186
  };
32655
33187
  element.addEventListener('pointerdown', onPointerDown);
32656
- return () => {
33188
+ const cleanupFn = () => {
32657
33189
  unsubscribeDocument();
32658
33190
  element.removeEventListener('pointerdown', onPointerDown);
33191
+ activePointers.clear();
33192
+ cleanup(dragSignal);
33193
+ };
33194
+ return {
33195
+ signal: dragSignal,
33196
+ cleanup: cleanupFn,
33197
+ };
33198
+ }
33199
+
33200
+ /**
33201
+ * Reactive scroll stream utilities
33202
+ *
33203
+ * Provides declarative scroll handling using reactive streams.
33204
+ * Automatically handles scroll event optimization and cleanup.
33205
+ */
33206
+ // ============================================================================
33207
+ // Pure Scroll Calculation Functions
33208
+ // ============================================================================
33209
+ /**
33210
+ * Calculate visible percentages from scroll data
33211
+ * Pure function - no side effects
33212
+ *
33213
+ * @param scrollData - Current scroll dimensions
33214
+ * @returns Start and end positions as percentages (0-1)
33215
+ */
33216
+ function calculateScrollPercentages(scrollData) {
33217
+ const { scrollLeft, scrollWidth, clientWidth } = scrollData;
33218
+ if (scrollWidth === 0) {
33219
+ return { startX: 0, endX: 1 };
33220
+ }
33221
+ const startX = scrollLeft / scrollWidth;
33222
+ const endX = (scrollLeft + clientWidth) / scrollWidth;
33223
+ return {
33224
+ startX: Math.max(0, Math.min(1, startX)),
33225
+ endX: Math.max(0, Math.min(1, endX)),
33226
+ };
33227
+ }
33228
+ /**
33229
+ * Calculate scroll bounds in pixels
33230
+ * Pure function - no side effects
33231
+ *
33232
+ * @param scrollData - Current scroll dimensions
33233
+ * @returns Left and right scroll bounds in pixels
33234
+ */
33235
+ function calculateScrollBounds(scrollData) {
33236
+ return {
33237
+ left: scrollData.scrollLeft,
33238
+ right: scrollData.scrollLeft + scrollData.clientWidth,
33239
+ };
33240
+ }
33241
+ /**
33242
+ * Create a reactive scroll stream from an element
33243
+ *
33244
+ * Emits scroll data as the user scrolls the element.
33245
+ * Automatically computes derived values (percentages, bounds).
33246
+ *
33247
+ * @example
33248
+ * ```typescript
33249
+ * const scrollStream = createScrollStream(container)
33250
+ *
33251
+ * effect(() => {
33252
+ * const { startX, endX } = scrollStream.percentages.value
33253
+ * console.log('Visible:', startX, 'to', endX)
33254
+ * }, [scrollStream.percentages])
33255
+ *
33256
+ * scrollStream.cleanup()
33257
+ * ```
33258
+ *
33259
+ * @param element - Scrollable element
33260
+ * @returns Scroll stream with signals and cleanup
33261
+ */
33262
+ function createScrollStream(element) {
33263
+ // Create signals
33264
+ const scrollData = signal({
33265
+ scrollLeft: element.scrollLeft,
33266
+ scrollWidth: element.scrollWidth,
33267
+ clientWidth: element.clientWidth,
33268
+ });
33269
+ // Computed derived values
33270
+ const percentages = computed(() => {
33271
+ return calculateScrollPercentages(scrollData.value);
33272
+ }, [scrollData]);
33273
+ const bounds = computed(() => {
33274
+ return calculateScrollBounds(scrollData.value);
33275
+ }, [scrollData]);
33276
+ // Update scroll data on scroll event
33277
+ const onScroll = () => {
33278
+ scrollData.set({
33279
+ scrollLeft: element.scrollLeft,
33280
+ scrollWidth: element.scrollWidth,
33281
+ clientWidth: element.clientWidth,
33282
+ });
33283
+ };
33284
+ // Attach scroll listener
33285
+ element.addEventListener('scroll', onScroll, { passive: true });
33286
+ // Cleanup function
33287
+ const cleanupFn = () => {
33288
+ element.removeEventListener('scroll', onScroll);
33289
+ cleanup(scrollData);
33290
+ };
33291
+ return {
33292
+ scrollData,
33293
+ percentages,
33294
+ bounds,
33295
+ cleanup: cleanupFn,
32659
33296
  };
32660
33297
  }
32661
33298
 
@@ -32690,6 +33327,8 @@ class Renderer extends EventEmitter {
32690
33327
  this.isDragging = false;
32691
33328
  this.subscriptions = [];
32692
33329
  this.unsubscribeOnScroll = [];
33330
+ this.dragStream = null;
33331
+ this.scrollStream = null;
32693
33332
  this.subscriptions = [];
32694
33333
  this.options = options;
32695
33334
  const parent = this.parentFromOptionsContainer(options.container);
@@ -32721,35 +33360,30 @@ class Renderer extends EventEmitter {
32721
33360
  return parent;
32722
33361
  }
32723
33362
  initEvents() {
32724
- const getClickPosition = (e) => {
32725
- const rect = this.wrapper.getBoundingClientRect();
32726
- const x = e.clientX - rect.left;
32727
- const y = e.clientY - rect.top;
32728
- const relativeX = x / rect.width;
32729
- const relativeY = y / rect.height;
32730
- return [relativeX, relativeY];
32731
- };
32732
33363
  // Add a click listener
32733
33364
  this.wrapper.addEventListener('click', (e) => {
32734
- const [x, y] = getClickPosition(e);
33365
+ const rect = this.wrapper.getBoundingClientRect();
33366
+ const [x, y] = getRelativePointerPosition(rect, e.clientX, e.clientY);
32735
33367
  this.emit('click', x, y);
32736
33368
  });
32737
33369
  // Add a double click listener
32738
33370
  this.wrapper.addEventListener('dblclick', (e) => {
32739
- const [x, y] = getClickPosition(e);
33371
+ const rect = this.wrapper.getBoundingClientRect();
33372
+ const [x, y] = getRelativePointerPosition(rect, e.clientX, e.clientY);
32740
33373
  this.emit('dblclick', x, y);
32741
33374
  });
32742
33375
  // Drag
32743
33376
  if (this.options.dragToSeek === true || typeof this.options.dragToSeek === 'object') {
32744
33377
  this.initDrag();
32745
33378
  }
32746
- // Add a scroll listener
32747
- this.scrollContainer.addEventListener('scroll', () => {
32748
- const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer;
32749
- const startX = scrollLeft / scrollWidth;
32750
- const endX = (scrollLeft + clientWidth) / scrollWidth;
32751
- this.emit('scroll', startX, endX, scrollLeft, scrollLeft + clientWidth);
32752
- });
33379
+ // Add a scroll listener using reactive stream
33380
+ this.scrollStream = createScrollStream(this.scrollContainer);
33381
+ const unsubscribeScroll = effect(() => {
33382
+ const { startX, endX } = this.scrollStream.percentages.value;
33383
+ const { left, right } = this.scrollStream.bounds.value;
33384
+ this.emit('scroll', startX, endX, left, right);
33385
+ }, [this.scrollStream.percentages, this.scrollStream.bounds]);
33386
+ this.subscriptions.push(unsubscribeScroll);
32753
33387
  // Re-render the waveform on container resize
32754
33388
  if (typeof ResizeObserver === 'function') {
32755
33389
  const delay = this.createDelay(100);
@@ -32767,39 +33401,32 @@ class Renderer extends EventEmitter {
32767
33401
  return;
32768
33402
  this.lastContainerWidth = width;
32769
33403
  this.reRender();
33404
+ this.emit('resize');
32770
33405
  }
32771
33406
  initDrag() {
32772
- this.subscriptions.push(makeDraggable(this.wrapper,
32773
- // On drag
32774
- (_, __, x) => {
32775
- this.emit('drag', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
32776
- },
32777
- // On start drag
32778
- (x) => {
32779
- this.isDragging = true;
32780
- this.emit('dragstart', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
32781
- },
32782
- // On end drag
32783
- (x) => {
32784
- this.isDragging = false;
32785
- this.emit('dragend', Math.max(0, Math.min(1, x / this.wrapper.getBoundingClientRect().width)));
32786
- }));
32787
- }
32788
- getHeight(optionsHeight, optionsSplitChannel) {
32789
- var _a;
32790
- const defaultHeight = 128;
32791
- const numberOfChannels = ((_a = this.audioData) === null || _a === void 0 ? void 0 : _a.numberOfChannels) || 1;
32792
- if (optionsHeight == null)
32793
- return defaultHeight;
32794
- if (!isNaN(Number(optionsHeight)))
32795
- return Number(optionsHeight);
32796
- if (optionsHeight === 'auto') {
32797
- const height = this.parent.clientHeight || defaultHeight;
32798
- if (optionsSplitChannel === null || optionsSplitChannel === void 0 ? void 0 : optionsSplitChannel.every((channel) => !channel.overlay))
32799
- return height / numberOfChannels;
32800
- return height;
32801
- }
32802
- return defaultHeight;
33407
+ // Don't initialize drag if it's already set up
33408
+ if (this.dragStream)
33409
+ return;
33410
+ this.dragStream = createDragStream(this.wrapper);
33411
+ const unsubscribeDrag = effect(() => {
33412
+ const drag = this.dragStream.signal.value;
33413
+ if (!drag)
33414
+ return;
33415
+ const width = this.wrapper.getBoundingClientRect().width;
33416
+ const relX = clampToUnit(drag.x / width);
33417
+ if (drag.type === 'start') {
33418
+ this.isDragging = true;
33419
+ this.emit('dragstart', relX);
33420
+ }
33421
+ else if (drag.type === 'move') {
33422
+ this.emit('drag', relX);
33423
+ }
33424
+ else if (drag.type === 'end') {
33425
+ this.isDragging = false;
33426
+ this.emit('dragend', relX);
33427
+ }
33428
+ }, [this.dragStream.signal]);
33429
+ this.subscriptions.push(unsubscribeDrag);
32803
33430
  }
32804
33431
  initHtml() {
32805
33432
  const div = document.createElement('div');
@@ -32836,6 +33463,7 @@ class Renderer extends EventEmitter {
32836
33463
  }
32837
33464
  :host .canvases {
32838
33465
  min-height: ${this.getHeight(this.options.height, this.options.splitChannels)}px;
33466
+ pointer-events: none;
32839
33467
  }
32840
33468
  :host .canvases > div {
32841
33469
  position: relative;
@@ -32912,150 +33540,134 @@ class Renderer extends EventEmitter {
32912
33540
  this.setScroll(scrollStart);
32913
33541
  }
32914
33542
  destroy() {
32915
- var _a, _b;
33543
+ var _a;
32916
33544
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
32917
33545
  this.container.remove();
32918
- (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
32919
- (_b = this.unsubscribeOnScroll) === null || _b === void 0 ? void 0 : _b.forEach((unsubscribe) => unsubscribe());
33546
+ if (this.resizeObserver) {
33547
+ this.resizeObserver.disconnect();
33548
+ this.resizeObserver = null;
33549
+ }
33550
+ (_a = this.unsubscribeOnScroll) === null || _a === void 0 ? void 0 : _a.forEach((unsubscribe) => unsubscribe());
32920
33551
  this.unsubscribeOnScroll = [];
33552
+ if (this.dragStream) {
33553
+ this.dragStream.cleanup();
33554
+ this.dragStream = null;
33555
+ }
33556
+ if (this.scrollStream) {
33557
+ this.scrollStream.cleanup();
33558
+ this.scrollStream = null;
33559
+ }
32921
33560
  }
32922
33561
  createDelay(delayMs = 10) {
32923
33562
  let timeout;
32924
- let reject;
33563
+ let rejectFn;
32925
33564
  const onClear = () => {
32926
- if (timeout)
33565
+ if (timeout) {
32927
33566
  clearTimeout(timeout);
32928
- if (reject)
32929
- reject();
33567
+ timeout = undefined;
33568
+ }
33569
+ if (rejectFn) {
33570
+ rejectFn();
33571
+ rejectFn = undefined;
33572
+ }
32930
33573
  };
32931
33574
  this.timeouts.push(onClear);
32932
33575
  return () => {
32933
- return new Promise((resolveFn, rejectFn) => {
33576
+ return new Promise((resolve, reject) => {
33577
+ // Clear any pending delay
32934
33578
  onClear();
32935
- reject = rejectFn;
33579
+ // Store reject function for cleanup
33580
+ rejectFn = reject;
33581
+ // Set new timeout
32936
33582
  timeout = setTimeout(() => {
32937
33583
  timeout = undefined;
32938
- reject = undefined;
32939
- resolveFn();
33584
+ rejectFn = undefined;
33585
+ resolve();
32940
33586
  }, delayMs);
32941
33587
  });
32942
33588
  };
32943
33589
  }
32944
- // Convert array of color values to linear gradient
32945
- convertColorValues(color) {
32946
- if (!Array.isArray(color))
32947
- return color || '';
32948
- if (color.length < 2)
32949
- return color[0] || '';
32950
- const canvasElement = document.createElement('canvas');
32951
- const ctx = canvasElement.getContext('2d');
32952
- const gradientHeight = canvasElement.height * (window.devicePixelRatio || 1);
32953
- const gradient = ctx.createLinearGradient(0, 0, 0, gradientHeight);
32954
- const colorStopPercentage = 1 / (color.length - 1);
32955
- color.forEach((color, index) => {
32956
- const offset = index * colorStopPercentage;
32957
- gradient.addColorStop(offset, color);
33590
+ getHeight(optionsHeight, optionsSplitChannel) {
33591
+ var _a;
33592
+ const numberOfChannels = ((_a = this.audioData) === null || _a === void 0 ? void 0 : _a.numberOfChannels) || 1;
33593
+ return resolveChannelHeight({
33594
+ optionsHeight,
33595
+ optionsSplitChannels: optionsSplitChannel,
33596
+ parentHeight: this.parent.clientHeight,
33597
+ numberOfChannels,
33598
+ defaultHeight: DEFAULT_HEIGHT,
32958
33599
  });
32959
- return gradient;
33600
+ }
33601
+ convertColorValues(color, ctx) {
33602
+ return resolveColorValue(color, this.getPixelRatio(), ctx === null || ctx === void 0 ? void 0 : ctx.canvas.height);
32960
33603
  }
32961
33604
  getPixelRatio() {
32962
- return Math.max(1, window.devicePixelRatio || 1);
33605
+ return getPixelRatio(window.devicePixelRatio);
32963
33606
  }
32964
33607
  renderBarWaveform(channelData, options, ctx, vScale) {
32965
- const topChannel = channelData[0];
32966
- const bottomChannel = channelData[1] || channelData[0];
32967
- const length = topChannel.length;
32968
33608
  const { width, height } = ctx.canvas;
32969
- const halfHeight = height / 2;
32970
- const pixelRatio = this.getPixelRatio();
32971
- const barWidth = options.barWidth ? options.barWidth * pixelRatio : 1;
32972
- const barGap = options.barGap ? options.barGap * pixelRatio : options.barWidth ? barWidth / 2 : 0;
32973
- const barRadius = options.barRadius || 0;
32974
- const barIndexScale = width / (barWidth + barGap) / length;
32975
- const rectFn = barRadius && 'roundRect' in ctx ? 'roundRect' : 'rect';
33609
+ const { halfHeight, barWidth, barRadius, barIndexScale, barSpacing, barMinHeight } = calculateBarRenderConfig({
33610
+ width,
33611
+ height,
33612
+ length: (channelData[0] || []).length,
33613
+ options,
33614
+ pixelRatio: this.getPixelRatio(),
33615
+ });
33616
+ const segments = calculateBarSegments({
33617
+ channelData,
33618
+ barIndexScale,
33619
+ barSpacing,
33620
+ barWidth,
33621
+ halfHeight,
33622
+ vScale,
33623
+ canvasHeight: height,
33624
+ barAlign: options.barAlign,
33625
+ barMinHeight,
33626
+ });
32976
33627
  ctx.beginPath();
32977
- let prevX = 0;
32978
- let maxTop = 0;
32979
- let maxBottom = 0;
32980
- for (let i = 0; i <= length; i++) {
32981
- const x = Math.round(i * barIndexScale);
32982
- if (x > prevX) {
32983
- const topBarHeight = Math.round(maxTop * halfHeight * vScale);
32984
- const bottomBarHeight = Math.round(maxBottom * halfHeight * vScale);
32985
- const barHeight = topBarHeight + bottomBarHeight || 1;
32986
- // Vertical alignment
32987
- let y = halfHeight - topBarHeight;
32988
- if (options.barAlign === 'top') {
32989
- y = 0;
32990
- }
32991
- else if (options.barAlign === 'bottom') {
32992
- y = height - barHeight;
32993
- }
32994
- ctx[rectFn](prevX * (barWidth + barGap), y, barWidth, barHeight, barRadius);
32995
- prevX = x;
32996
- maxTop = 0;
32997
- maxBottom = 0;
33628
+ for (const segment of segments) {
33629
+ if (barRadius && 'roundRect' in ctx) {
33630
+ ctx.roundRect(segment.x, segment.y, segment.width, segment.height, barRadius);
33631
+ }
33632
+ else {
33633
+ ctx.rect(segment.x, segment.y, segment.width, segment.height);
32998
33634
  }
32999
- const magnitudeTop = Math.abs(topChannel[i] || 0);
33000
- const magnitudeBottom = Math.abs(bottomChannel[i] || 0);
33001
- if (magnitudeTop > maxTop)
33002
- maxTop = magnitudeTop;
33003
- if (magnitudeBottom > maxBottom)
33004
- maxBottom = magnitudeBottom;
33005
33635
  }
33006
33636
  ctx.fill();
33007
33637
  ctx.closePath();
33008
33638
  }
33009
33639
  renderLineWaveform(channelData, _options, ctx, vScale) {
33010
- const drawChannel = (index) => {
33011
- const channel = channelData[index] || channelData[0];
33012
- const length = channel.length;
33013
- const { height } = ctx.canvas;
33014
- const halfHeight = height / 2;
33015
- const hScale = ctx.canvas.width / length;
33016
- ctx.moveTo(0, halfHeight);
33017
- let prevX = 0;
33018
- let max = 0;
33019
- for (let i = 0; i <= length; i++) {
33020
- const x = Math.round(i * hScale);
33021
- if (x > prevX) {
33022
- const h = Math.round(max * halfHeight * vScale) || 1;
33023
- const y = halfHeight + h * (index === 0 ? -1 : 1);
33024
- ctx.lineTo(prevX, y);
33025
- prevX = x;
33026
- max = 0;
33027
- }
33028
- const value = Math.abs(channel[i] || 0);
33029
- if (value > max)
33030
- max = value;
33031
- }
33032
- ctx.lineTo(prevX, halfHeight);
33033
- };
33640
+ const { width, height } = ctx.canvas;
33641
+ const paths = calculateLinePaths({ channelData, width, height, vScale });
33034
33642
  ctx.beginPath();
33035
- drawChannel(0);
33036
- drawChannel(1);
33643
+ for (const path of paths) {
33644
+ if (!path.length)
33645
+ continue;
33646
+ ctx.moveTo(path[0].x, path[0].y);
33647
+ for (let i = 1; i < path.length; i++) {
33648
+ const point = path[i];
33649
+ ctx.lineTo(point.x, point.y);
33650
+ }
33651
+ }
33037
33652
  ctx.fill();
33038
33653
  ctx.closePath();
33039
33654
  }
33040
33655
  renderWaveform(channelData, options, ctx) {
33041
- ctx.fillStyle = this.convertColorValues(options.waveColor);
33042
- // Custom rendering function
33656
+ ctx.fillStyle = this.convertColorValues(options.waveColor, ctx);
33043
33657
  if (options.renderFunction) {
33044
33658
  options.renderFunction(channelData, ctx);
33045
33659
  return;
33046
33660
  }
33047
- // Vertical scaling
33048
- let vScale = options.barHeight || 1;
33049
- if (options.normalize) {
33050
- const max = Array.from(channelData[0]).reduce((max, value) => Math.max(max, Math.abs(value)), 0);
33051
- vScale = max ? 1 / max : 1;
33052
- }
33053
- // Render waveform as bars
33054
- if (options.barWidth || options.barGap || options.barAlign) {
33661
+ const vScale = calculateVerticalScale({
33662
+ channelData,
33663
+ barHeight: options.barHeight,
33664
+ normalize: options.normalize,
33665
+ maxPeak: options.maxPeak,
33666
+ });
33667
+ if (shouldRenderBars(options)) {
33055
33668
  this.renderBarWaveform(channelData, options, ctx, vScale);
33056
33669
  return;
33057
33670
  }
33058
- // Render waveform as a polyline
33059
33671
  this.renderLineWaveform(channelData, options, ctx, vScale);
33060
33672
  }
33061
33673
  renderSingleCanvas(data, options, width, height, offset, canvasContainer, progressContainer) {
@@ -33068,7 +33680,13 @@ class Renderer extends EventEmitter {
33068
33680
  canvas.style.left = `${Math.round(offset)}px`;
33069
33681
  canvasContainer.appendChild(canvas);
33070
33682
  const ctx = canvas.getContext('2d');
33071
- this.renderWaveform(data, options, ctx);
33683
+ if (options.renderFunction) {
33684
+ ctx.fillStyle = this.convertColorValues(options.waveColor, ctx);
33685
+ options.renderFunction(data, ctx);
33686
+ }
33687
+ else {
33688
+ this.renderWaveform(data, options, ctx);
33689
+ }
33072
33690
  // Draw a progress canvas
33073
33691
  if (canvas.width > 0 && canvas.height > 0) {
33074
33692
  const progressCanvas = canvas.cloneNode();
@@ -33076,7 +33694,7 @@ class Renderer extends EventEmitter {
33076
33694
  progressCtx.drawImage(canvas, 0, 0);
33077
33695
  // Set the composition method to draw only where the waveform is drawn
33078
33696
  progressCtx.globalCompositeOperation = 'source-in';
33079
- progressCtx.fillStyle = this.convertColorValues(options.progressColor);
33697
+ progressCtx.fillStyle = this.convertColorValues(options.progressColor, progressCtx);
33080
33698
  // This rectangle acts as a mask thanks to the composition method
33081
33699
  progressCtx.fillRect(0, 0, canvas.width, canvas.height);
33082
33700
  progressContainer.appendChild(progressCanvas);
@@ -33086,17 +33704,8 @@ class Renderer extends EventEmitter {
33086
33704
  const pixelRatio = this.getPixelRatio();
33087
33705
  const { clientWidth } = this.scrollContainer;
33088
33706
  const totalWidth = width / pixelRatio;
33089
- let singleCanvasWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth, totalWidth);
33707
+ const singleCanvasWidth = calculateSingleCanvasWidth({ clientWidth, totalWidth, options });
33090
33708
  let drawnIndexes = {};
33091
- // Adjust width to avoid gaps between canvases when using bars
33092
- if (options.barWidth || options.barGap) {
33093
- const barWidth = options.barWidth || 0.5;
33094
- const barGap = options.barGap || barWidth / 2;
33095
- const totalBarWidth = barWidth + barGap;
33096
- if (singleCanvasWidth % totalBarWidth !== 0) {
33097
- singleCanvasWidth = Math.floor(singleCanvasWidth / totalBarWidth) * totalBarWidth;
33098
- }
33099
- }
33100
33709
  // Nothing to render
33101
33710
  if (singleCanvasWidth === 0)
33102
33711
  return;
@@ -33110,24 +33719,15 @@ class Renderer extends EventEmitter {
33110
33719
  const offset = index * singleCanvasWidth;
33111
33720
  let clampedWidth = Math.min(totalWidth - offset, singleCanvasWidth);
33112
33721
  // Clamp the width to the bar grid to avoid empty canvases at the end
33113
- if (options.barWidth || options.barGap) {
33114
- const barWidth = options.barWidth || 0.5;
33115
- const barGap = options.barGap || barWidth / 2;
33116
- const totalBarWidth = barWidth + barGap;
33117
- clampedWidth = Math.floor(clampedWidth / totalBarWidth) * totalBarWidth;
33118
- }
33722
+ clampedWidth = clampWidthToBarGrid(clampedWidth, options);
33119
33723
  if (clampedWidth <= 0)
33120
33724
  return;
33121
- const data = channelData.map((channel) => {
33122
- const start = Math.floor((offset / totalWidth) * channel.length);
33123
- const end = Math.floor(((offset + clampedWidth) / totalWidth) * channel.length);
33124
- return channel.slice(start, end);
33125
- });
33725
+ const data = sliceChannelData({ channelData, offset, clampedWidth, totalWidth });
33126
33726
  this.renderSingleCanvas(data, options, clampedWidth, height, offset, canvasContainer, progressContainer);
33127
33727
  };
33128
33728
  // Clear canvases to avoid too many DOM nodes
33129
33729
  const clearCanvases = () => {
33130
- if (Object.keys(drawnIndexes).length > Renderer.MAX_NODES) {
33730
+ if (shouldClearCanvases(Object.keys(drawnIndexes).length)) {
33131
33731
  canvasContainer.innerHTML = '';
33132
33732
  progressContainer.innerHTML = '';
33133
33733
  drawnIndexes = {};
@@ -33143,21 +33743,18 @@ class Renderer extends EventEmitter {
33143
33743
  return;
33144
33744
  }
33145
33745
  // Lazy rendering
33146
- const viewPosition = this.scrollContainer.scrollLeft / totalWidth;
33147
- const startCanvas = Math.floor(viewPosition * numCanvases);
33148
- // Draw the canvases in the viewport first
33149
- draw(startCanvas - 1);
33150
- draw(startCanvas);
33151
- draw(startCanvas + 1);
33746
+ const initialRange = getLazyRenderRange({
33747
+ scrollLeft: this.scrollContainer.scrollLeft,
33748
+ totalWidth,
33749
+ numCanvases,
33750
+ });
33751
+ initialRange.forEach((index) => draw(index));
33152
33752
  // Subscribe to the scroll event to draw additional canvases
33153
33753
  if (numCanvases > 1) {
33154
33754
  const unsubscribe = this.on('scroll', () => {
33155
33755
  const { scrollLeft } = this.scrollContainer;
33156
- const canvasIndex = Math.floor((scrollLeft / totalWidth) * numCanvases);
33157
33756
  clearCanvases();
33158
- draw(canvasIndex - 1);
33159
- draw(canvasIndex);
33160
- draw(canvasIndex + 1);
33757
+ getLazyRenderRange({ scrollLeft, totalWidth, numCanvases }).forEach((index) => draw(index));
33161
33758
  });
33162
33759
  this.unsubscribeOnScroll.push(unsubscribe);
33163
33760
  }
@@ -33196,12 +33793,15 @@ class Renderer extends EventEmitter {
33196
33793
  // Determine the width of the waveform
33197
33794
  const pixelRatio = this.getPixelRatio();
33198
33795
  const parentWidth = this.scrollContainer.clientWidth;
33199
- const scrollWidth = Math.ceil(audioData.duration * (this.options.minPxPerSec || 0));
33796
+ const { scrollWidth, isScrollable, useParentWidth, width } = calculateWaveformLayout({
33797
+ duration: audioData.duration,
33798
+ minPxPerSec: this.options.minPxPerSec || 0,
33799
+ parentWidth,
33800
+ fillParent: this.options.fillParent,
33801
+ pixelRatio,
33802
+ });
33200
33803
  // Whether the container should scroll
33201
- this.isScrollable = scrollWidth > parentWidth;
33202
- const useParentWidth = this.options.fillParent && !this.isScrollable;
33203
- // Width of the waveform in pixels
33204
- const width = (useParentWidth ? parentWidth : scrollWidth) * pixelRatio;
33804
+ this.isScrollable = isScrollable;
33205
33805
  // Set the width of the wrapper
33206
33806
  this.wrapper.style.width = useParentWidth ? '100%' : `${scrollWidth}px`;
33207
33807
  // Set additional styles
@@ -33244,12 +33844,7 @@ class Renderer extends EventEmitter {
33244
33844
  // Adjust the scroll position so that the cursor stays in the same place
33245
33845
  if (this.isScrollable && scrollWidth !== this.scrollContainer.scrollWidth) {
33246
33846
  const { right: after } = this.progressWrapper.getBoundingClientRect();
33247
- let delta = after - before;
33248
- // to limit compounding floating-point drift
33249
- // we need to round to the half px furthest from 0
33250
- delta *= 2;
33251
- delta = delta < 0 ? Math.floor(delta) : Math.ceil(delta);
33252
- delta /= 2;
33847
+ const delta = roundToHalfAwayFromZero(after - before);
33253
33848
  this.scrollContainer.scrollLeft += delta;
33254
33849
  }
33255
33850
  }
@@ -33280,16 +33875,9 @@ class Renderer extends EventEmitter {
33280
33875
  // Keep the cursor centered when playing
33281
33876
  const center = progressWidth - scrollLeft - middle;
33282
33877
  if (isPlaying && this.options.autoCenter && center > 0) {
33283
- this.scrollContainer.scrollLeft += Math.min(center, 10);
33878
+ this.scrollContainer.scrollLeft += center;
33284
33879
  }
33285
33880
  }
33286
- // Emit the scroll event
33287
- {
33288
- const newScroll = this.scrollContainer.scrollLeft;
33289
- const startX = newScroll / scrollWidth;
33290
- const endX = (newScroll + clientWidth) / scrollWidth;
33291
- this.emit('scroll', startX, endX, newScroll, newScroll + clientWidth);
33292
- }
33293
33881
  }
33294
33882
  renderProgress(progress, isPlaying) {
33295
33883
  if (isNaN(progress))
@@ -33301,7 +33889,8 @@ class Renderer extends EventEmitter {
33301
33889
  this.cursor.style.transform = this.options.cursorWidth
33302
33890
  ? `translateX(-${progress * this.options.cursorWidth}px)`
33303
33891
  : '';
33304
- if (this.isScrollable && this.options.autoScroll) {
33892
+ // Only scroll if we have valid audio data to prevent race conditions during loading
33893
+ if (this.isScrollable && this.options.autoScroll && this.audioData && this.audioData.duration > 0) {
33305
33894
  this.scrollIntoView(progress, isPlaying);
33306
33895
  }
33307
33896
  }
@@ -33332,27 +33921,39 @@ class Renderer extends EventEmitter {
33332
33921
  });
33333
33922
  }
33334
33923
  }
33335
- Renderer.MAX_CANVAS_WIDTH = 8000;
33336
- Renderer.MAX_NODES = 10;
33337
33924
 
33338
33925
  class Timer extends EventEmitter {
33339
33926
  constructor() {
33340
33927
  super(...arguments);
33341
- this.unsubscribe = () => undefined;
33928
+ this.animationFrameId = null;
33929
+ this.isRunning = false;
33342
33930
  }
33343
33931
  start() {
33344
- this.unsubscribe = this.on('tick', () => {
33345
- requestAnimationFrame(() => {
33346
- this.emit('tick');
33347
- });
33348
- });
33349
- this.emit('tick');
33932
+ // Prevent multiple simultaneous loops
33933
+ if (this.isRunning)
33934
+ return;
33935
+ this.isRunning = true;
33936
+ const tick = () => {
33937
+ // Only continue if timer is still running
33938
+ if (!this.isRunning)
33939
+ return;
33940
+ this.emit('tick');
33941
+ // Schedule next frame
33942
+ this.animationFrameId = requestAnimationFrame(tick);
33943
+ };
33944
+ // Start the loop
33945
+ tick();
33350
33946
  }
33351
33947
  stop() {
33352
- this.unsubscribe();
33948
+ this.isRunning = false;
33949
+ // Cancel any pending animation frame
33950
+ if (this.animationFrameId !== null) {
33951
+ cancelAnimationFrame(this.animationFrameId);
33952
+ this.animationFrameId = null;
33953
+ }
33353
33954
  }
33354
33955
  destroy() {
33355
- this.unsubscribe();
33956
+ this.stop();
33356
33957
  }
33357
33958
  }
33358
33959
 
@@ -33367,6 +33968,10 @@ var __awaiter$4 = (undefined && undefined.__awaiter) || function (thisArg, _argu
33367
33968
  };
33368
33969
  /**
33369
33970
  * A Web Audio buffer player emulating the behavior of an HTML5 Audio element.
33971
+ *
33972
+ * Note: This class does not manage blob: URLs. If you pass a blob: URL to setSrc(),
33973
+ * you are responsible for revoking it when done. The Player class (player.ts) handles
33974
+ * blob URL lifecycle management automatically.
33370
33975
  */
33371
33976
  class WebAudioPlayer extends EventEmitter {
33372
33977
  constructor(audioContext = new AudioContext()) {
@@ -33427,14 +34032,21 @@ class WebAudioPlayer extends EventEmitter {
33427
34032
  this.emit('canplay');
33428
34033
  if (this.autoplay)
33429
34034
  this.play();
34035
+ })
34036
+ .catch((err) => {
34037
+ // Emit error for proper error handling
34038
+ console.error('WebAudioPlayer load error:', err);
33430
34039
  });
33431
34040
  }
33432
34041
  _play() {
33433
- var _a;
33434
34042
  if (!this.paused)
33435
34043
  return;
33436
34044
  this.paused = false;
33437
- (_a = this.bufferNode) === null || _a === void 0 ? void 0 : _a.disconnect();
34045
+ // Clean up old buffer node completely before creating new one
34046
+ if (this.bufferNode) {
34047
+ this.bufferNode.onended = null;
34048
+ this.bufferNode.disconnect();
34049
+ }
33438
34050
  this.bufferNode = this.audioContext.createBufferSource();
33439
34051
  if (this.buffer) {
33440
34052
  this.bufferNode.buffer = this.buffer;
@@ -33590,6 +34202,239 @@ class WebAudioPlayer extends EventEmitter {
33590
34202
  }
33591
34203
  }
33592
34204
 
34205
+ /**
34206
+ * Centralized reactive state for WaveSurfer
34207
+ *
34208
+ * This module provides a single source of truth for all WaveSurfer state.
34209
+ * State is managed using reactive signals that automatically notify subscribers.
34210
+ */
34211
+ /**
34212
+ * Create a new WaveSurfer state instance
34213
+ *
34214
+ * @param playerSignals - Optional signals from Player to compose with WaveSurfer state
34215
+ *
34216
+ * @example
34217
+ * ```typescript
34218
+ * // Without Player signals (standalone)
34219
+ * const { state, actions } = createWaveSurferState()
34220
+ *
34221
+ * // With Player signals (composed)
34222
+ * const { state, actions } = createWaveSurferState({
34223
+ * isPlaying: player.isPlayingSignal,
34224
+ * currentTime: player.currentTimeSignal,
34225
+ * // ...
34226
+ * })
34227
+ *
34228
+ * // Read state
34229
+ * console.log(state.isPlaying.value)
34230
+ *
34231
+ * // Update state
34232
+ * actions.setPlaying(true)
34233
+ *
34234
+ * // Subscribe to changes
34235
+ * state.isPlaying.subscribe(playing => {
34236
+ * console.log('Playing:', playing)
34237
+ * })
34238
+ * ```
34239
+ */
34240
+ function createWaveSurferState(playerSignals) {
34241
+ var _a, _b, _c, _d, _e, _f;
34242
+ // Use Player signals if provided, otherwise create new ones
34243
+ const currentTime = (_a = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.currentTime) !== null && _a !== void 0 ? _a : signal(0);
34244
+ const duration = (_b = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.duration) !== null && _b !== void 0 ? _b : signal(0);
34245
+ const isPlaying = (_c = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.isPlaying) !== null && _c !== void 0 ? _c : signal(false);
34246
+ const isSeeking = (_d = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.isSeeking) !== null && _d !== void 0 ? _d : signal(false);
34247
+ const volume = (_e = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.volume) !== null && _e !== void 0 ? _e : signal(1);
34248
+ const playbackRate = (_f = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.playbackRate) !== null && _f !== void 0 ? _f : signal(1);
34249
+ // WaveSurfer-specific signals (not in Player)
34250
+ const audioBuffer = signal(null);
34251
+ const peaks = signal(null);
34252
+ const url = signal('');
34253
+ const zoom = signal(0);
34254
+ const scrollPosition = signal(0);
34255
+ // Computed values (derived state)
34256
+ const isPaused = computed(() => !isPlaying.value, [isPlaying]);
34257
+ const canPlay = computed(() => audioBuffer.value !== null, [audioBuffer]);
34258
+ const isReady = computed(() => {
34259
+ return canPlay.value && duration.value > 0;
34260
+ }, [canPlay, duration]);
34261
+ const progress = computed(() => currentTime.value, [currentTime]);
34262
+ const progressPercent = computed(() => {
34263
+ return duration.value > 0 ? currentTime.value / duration.value : 0;
34264
+ }, [currentTime, duration]);
34265
+ // Public read-only state
34266
+ const state = {
34267
+ currentTime,
34268
+ duration,
34269
+ isPlaying,
34270
+ isPaused,
34271
+ isSeeking,
34272
+ volume,
34273
+ playbackRate,
34274
+ audioBuffer,
34275
+ peaks,
34276
+ url,
34277
+ zoom,
34278
+ scrollPosition,
34279
+ canPlay,
34280
+ isReady,
34281
+ progress,
34282
+ progressPercent,
34283
+ };
34284
+ // Actions that modify state
34285
+ const actions = {
34286
+ setCurrentTime: (time) => {
34287
+ const clampedTime = Math.max(0, Math.min(duration.value || Infinity, time));
34288
+ currentTime.set(clampedTime);
34289
+ },
34290
+ setDuration: (d) => {
34291
+ duration.set(Math.max(0, d));
34292
+ },
34293
+ setPlaying: (playing) => {
34294
+ isPlaying.set(playing);
34295
+ },
34296
+ setSeeking: (seeking) => {
34297
+ isSeeking.set(seeking);
34298
+ },
34299
+ setVolume: (v) => {
34300
+ const clampedVolume = Math.max(0, Math.min(1, v));
34301
+ volume.set(clampedVolume);
34302
+ },
34303
+ setPlaybackRate: (rate) => {
34304
+ const clampedRate = Math.max(0.1, Math.min(16, rate));
34305
+ playbackRate.set(clampedRate);
34306
+ },
34307
+ setAudioBuffer: (buffer) => {
34308
+ audioBuffer.set(buffer);
34309
+ if (buffer) {
34310
+ duration.set(buffer.duration);
34311
+ }
34312
+ },
34313
+ setPeaks: (p) => {
34314
+ peaks.set(p);
34315
+ },
34316
+ setUrl: (u) => {
34317
+ url.set(u);
34318
+ },
34319
+ setZoom: (z) => {
34320
+ zoom.set(Math.max(0, z));
34321
+ },
34322
+ setScrollPosition: (pos) => {
34323
+ scrollPosition.set(Math.max(0, pos));
34324
+ },
34325
+ };
34326
+ return { state, actions };
34327
+ }
34328
+
34329
+ /**
34330
+ * State-driven event emission utilities
34331
+ *
34332
+ * Automatically emit events when reactive state changes.
34333
+ * Ensures events are always in sync with state and removes manual emit() calls.
34334
+ */
34335
+ /**
34336
+ * Setup automatic event emission from state changes
34337
+ *
34338
+ * This function subscribes to all relevant state signals and automatically
34339
+ * emits corresponding events when state changes. This ensures:
34340
+ * - Events are always in sync with state
34341
+ * - No manual emit() calls needed
34342
+ * - Can't forget to emit an event
34343
+ * - Clear event sources (state changes)
34344
+ *
34345
+ * @example
34346
+ * ```typescript
34347
+ * const { state } = createWaveSurferState()
34348
+ * const wavesurfer = new WaveSurfer()
34349
+ *
34350
+ * const cleanup = setupStateEventEmission(state, wavesurfer)
34351
+ *
34352
+ * // Now state changes automatically emit events
34353
+ * state.isPlaying.set(true) // → wavesurfer.emit('play')
34354
+ * ```
34355
+ *
34356
+ * @param state - Reactive state to observe
34357
+ * @param emitter - Event emitter to emit events on
34358
+ * @returns Cleanup function that removes all subscriptions
34359
+ */
34360
+ function setupStateEventEmission(state, emitter) {
34361
+ const cleanups = [];
34362
+ // ============================================================================
34363
+ // Play/Pause Events
34364
+ // ============================================================================
34365
+ // Emit play/pause events when playing state changes
34366
+ cleanups.push(effect(() => {
34367
+ const isPlaying = state.isPlaying.value;
34368
+ emitter.emit(isPlaying ? 'play' : 'pause');
34369
+ }, [state.isPlaying]));
34370
+ // ============================================================================
34371
+ // Time Update Events
34372
+ // ============================================================================
34373
+ // Emit timeupdate when current time changes
34374
+ cleanups.push(effect(() => {
34375
+ const currentTime = state.currentTime.value;
34376
+ emitter.emit('timeupdate', currentTime);
34377
+ // Also emit audioprocess when playing
34378
+ if (state.isPlaying.value) {
34379
+ emitter.emit('audioprocess', currentTime);
34380
+ }
34381
+ }, [state.currentTime, state.isPlaying]));
34382
+ // ============================================================================
34383
+ // Seeking Events
34384
+ // ============================================================================
34385
+ // Emit seeking event when seeking state changes to true
34386
+ cleanups.push(effect(() => {
34387
+ const isSeeking = state.isSeeking.value;
34388
+ if (isSeeking) {
34389
+ emitter.emit('seeking', state.currentTime.value);
34390
+ }
34391
+ }, [state.isSeeking, state.currentTime]));
34392
+ // ============================================================================
34393
+ // Ready Event
34394
+ // ============================================================================
34395
+ // Emit ready when state becomes ready
34396
+ let wasReady = false;
34397
+ cleanups.push(effect(() => {
34398
+ const isReady = state.isReady.value;
34399
+ if (isReady && !wasReady) {
34400
+ wasReady = true;
34401
+ emitter.emit('ready', state.duration.value);
34402
+ }
34403
+ }, [state.isReady, state.duration]));
34404
+ // ============================================================================
34405
+ // Finish Event
34406
+ // ============================================================================
34407
+ // Emit finish when playback ends (reached duration and stopped)
34408
+ let wasPlayingAtEnd = false;
34409
+ cleanups.push(effect(() => {
34410
+ const isPlaying = state.isPlaying.value;
34411
+ const currentTime = state.currentTime.value;
34412
+ const duration = state.duration.value;
34413
+ // Check if we're at the end
34414
+ const isAtEnd = duration > 0 && currentTime >= duration;
34415
+ // Emit finish when we were playing at end and now stopped
34416
+ if (wasPlayingAtEnd && !isPlaying && isAtEnd) {
34417
+ emitter.emit('finish');
34418
+ }
34419
+ // Track if we're playing at the end
34420
+ wasPlayingAtEnd = isPlaying && isAtEnd;
34421
+ }, [state.isPlaying, state.currentTime, state.duration]));
34422
+ // ============================================================================
34423
+ // Zoom Events
34424
+ // ============================================================================
34425
+ // Emit zoom when zoom level changes
34426
+ cleanups.push(effect(() => {
34427
+ const zoom = state.zoom.value;
34428
+ if (zoom > 0) {
34429
+ emitter.emit('zoom', zoom);
34430
+ }
34431
+ }, [state.zoom]));
34432
+ // Return cleanup function
34433
+ return () => {
34434
+ cleanups.forEach((cleanup) => cleanup());
34435
+ };
34436
+ }
34437
+
33593
34438
  var __awaiter$5 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
33594
34439
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
33595
34440
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -33616,6 +34461,14 @@ class WaveSurfer extends Player {
33616
34461
  static create(options) {
33617
34462
  return new WaveSurfer(options);
33618
34463
  }
34464
+ /** Get the reactive state for advanced use cases */
34465
+ getState() {
34466
+ return this.wavesurferState;
34467
+ }
34468
+ /** Get the renderer instance for plugin access to reactive streams */
34469
+ getRenderer() {
34470
+ return this.renderer;
34471
+ }
33619
34472
  /** Create a new WaveSurfer instance */
33620
34473
  constructor(options) {
33621
34474
  const media = options.media ||
@@ -33632,13 +34485,27 @@ class WaveSurfer extends Player {
33632
34485
  this.subscriptions = [];
33633
34486
  this.mediaSubscriptions = [];
33634
34487
  this.abortController = null;
34488
+ this.reactiveCleanups = [];
33635
34489
  this.options = Object.assign({}, defaultOptions, options);
34490
+ // Initialize reactive state
34491
+ // Pass Player signals to compose them into WaveSurferState
34492
+ const { state, actions } = createWaveSurferState({
34493
+ isPlaying: this.isPlayingSignal,
34494
+ currentTime: this.currentTimeSignal,
34495
+ duration: this.durationSignal,
34496
+ volume: this.volumeSignal,
34497
+ playbackRate: this.playbackRateSignal,
34498
+ isSeeking: this.seekingSignal,
34499
+ });
34500
+ this.wavesurferState = state;
34501
+ this.wavesurferActions = actions;
33636
34502
  this.timer = new Timer();
33637
34503
  const audioElement = media ? undefined : this.getMediaElement();
33638
34504
  this.renderer = new Renderer(this.options, audioElement);
33639
34505
  this.initPlayerEvents();
33640
34506
  this.initRendererEvents();
33641
34507
  this.initTimerEvents();
34508
+ this.initReactiveState();
33642
34509
  this.initPlugins();
33643
34510
  // Read the initial URL before load has been called
33644
34511
  const initialUrl = this.options.url || this.getSrc() || '';
@@ -33651,7 +34518,10 @@ class WaveSurfer extends Player {
33651
34518
  if (initialUrl || (peaks && duration)) {
33652
34519
  // Swallow async errors because they cannot be caught from a constructor call.
33653
34520
  // Subscribe to the wavesurfer's error event to handle them.
33654
- this.load(initialUrl, peaks, duration).catch(() => null);
34521
+ this.load(initialUrl, peaks, duration).catch((err) => {
34522
+ // Emit error event for proper error handling
34523
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
34524
+ });
33655
34525
  }
33656
34526
  });
33657
34527
  }
@@ -33673,6 +34543,12 @@ class WaveSurfer extends Player {
33673
34543
  }
33674
34544
  }));
33675
34545
  }
34546
+ initReactiveState() {
34547
+ // Bridge reactive state to EventEmitter for backwards compatibility
34548
+ this.reactiveCleanups.push(setupStateEventEmission(this.wavesurferState, {
34549
+ emit: this.emit.bind(this),
34550
+ }));
34551
+ }
33676
34552
  initPlayerEvents() {
33677
34553
  if (this.isPlaying()) {
33678
34554
  this.emit('play');
@@ -33737,33 +34613,44 @@ class WaveSurfer extends Player {
33737
34613
  // DragEnd
33738
34614
  this.renderer.on('dragend', (relativeX) => {
33739
34615
  this.emit('dragend', relativeX);
34616
+ }),
34617
+ // Resize
34618
+ this.renderer.on('resize', () => {
34619
+ this.emit('resize');
33740
34620
  }));
33741
34621
  // Drag
33742
34622
  {
33743
34623
  let debounce;
33744
- this.subscriptions.push(this.renderer.on('drag', (relativeX) => {
34624
+ const unsubscribeDrag = this.renderer.on('drag', (relativeX) => {
34625
+ var _a;
33745
34626
  if (!this.options.interact)
33746
34627
  return;
33747
34628
  // Update the visual position
33748
34629
  this.renderer.renderProgress(relativeX);
33749
34630
  // Set the audio position with a debounce
33750
34631
  clearTimeout(debounce);
33751
- let debounceTime;
34632
+ let debounceTime = 0;
34633
+ const dragToSeek = this.options.dragToSeek;
33752
34634
  if (this.isPlaying()) {
33753
34635
  debounceTime = 0;
33754
34636
  }
33755
- else if (this.options.dragToSeek === true) {
34637
+ else if (dragToSeek === true) {
33756
34638
  debounceTime = 200;
33757
34639
  }
33758
- else if (typeof this.options.dragToSeek === 'object' && this.options.dragToSeek !== undefined) {
33759
- debounceTime = this.options.dragToSeek['debounceTime'];
34640
+ else if (dragToSeek && typeof dragToSeek === 'object') {
34641
+ debounceTime = (_a = dragToSeek.debounceTime) !== null && _a !== void 0 ? _a : 200;
33760
34642
  }
33761
34643
  debounce = setTimeout(() => {
33762
34644
  this.seekTo(relativeX);
33763
34645
  }, debounceTime);
33764
34646
  this.emit('interaction', relativeX * this.getDuration());
33765
34647
  this.emit('drag', relativeX);
33766
- }));
34648
+ });
34649
+ // Clear debounce timeout on destroy
34650
+ this.subscriptions.push(() => {
34651
+ clearTimeout(debounce);
34652
+ unsubscribeDrag();
34653
+ });
33767
34654
  }
33768
34655
  }
33769
34656
  initPlugins() {
@@ -33850,12 +34737,15 @@ class WaveSurfer extends Player {
33850
34737
  this.pause();
33851
34738
  this.decodedData = null;
33852
34739
  this.stopAtPosition = null;
34740
+ // Abort any ongoing fetch before starting a new one
34741
+ (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.abort();
34742
+ this.abortController = null;
33853
34743
  // Fetch the entire audio as a blob if pre-decoded data is not provided
33854
34744
  if (!blob && !channelData) {
33855
34745
  const fetchParams = this.options.fetchParams || {};
33856
34746
  if (window.AbortController && !fetchParams.signal) {
33857
34747
  this.abortController = new AbortController();
33858
- fetchParams.signal = (_a = this.abortController) === null || _a === void 0 ? void 0 : _a.signal;
34748
+ fetchParams.signal = this.abortController.signal;
33859
34749
  }
33860
34750
  const onProgress = (percentage) => this.emit('loading', percentage);
33861
34751
  blob = yield Fetcher.fetchBlob(url, onProgress, fetchParams);
@@ -33945,8 +34835,8 @@ class WaveSurfer extends Player {
33945
34835
  const channel = this.decodedData.getChannelData(i);
33946
34836
  const data = [];
33947
34837
  const sampleSize = channel.length / maxLength;
33948
- for (let i = 0; i < maxLength; i++) {
33949
- const sample = channel.slice(Math.floor(i * sampleSize), Math.ceil((i + 1) * sampleSize));
34838
+ for (let j = 0; j < maxLength; j++) {
34839
+ const sample = channel.slice(Math.floor(j * sampleSize), Math.ceil((j + 1) * sampleSize));
33950
34840
  let max = 0;
33951
34841
  for (let x = 0; x < sample.length; x++) {
33952
34842
  const n = sample[x];
@@ -33979,7 +34869,7 @@ class WaveSurfer extends Player {
33979
34869
  this.updateProgress(time);
33980
34870
  this.emit('timeupdate', time);
33981
34871
  }
33982
- /** Seek to a percentage of audio as [0..1] (0 = beginning, 1 = end) */
34872
+ /** Seek to a ratio of audio as [0..1] (0 = beginning, 1 = end) */
33983
34873
  seekTo(progress) {
33984
34874
  const time = this.getDuration() * progress;
33985
34875
  this.setTime(time);
@@ -34043,6 +34933,8 @@ class WaveSurfer extends Player {
34043
34933
  this.plugins.forEach((plugin) => plugin.destroy());
34044
34934
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
34045
34935
  this.unsubscribePlayerEvents();
34936
+ this.reactiveCleanups.forEach((cleanup) => cleanup());
34937
+ this.reactiveCleanups = [];
34046
34938
  this.timer.destroy();
34047
34939
  this.renderer.destroy();
34048
34940
  super.destroy();
@@ -35260,7 +36152,11 @@ var Attachment = function Attachment(_ref) {
35260
36152
  channelId: channelId,
35261
36153
  incoming: incoming,
35262
36154
  viewOnce: viewOnce,
35263
- setViewOnceVoiceModalOpen: viewOnce ? setViewOnceVoiceModalOpen : undefined
36155
+ setViewOnceVoiceModalOpen: viewOnce ? function () {
36156
+ if (connectionStatus === CONNECTION_STATUS.CONNECTED) {
36157
+ setViewOnceVoiceModalOpen(true);
36158
+ }
36159
+ } : undefined
35264
36160
  })) : attachment.type === attachmentTypes.link ? null : (
35265
36161
  /*#__PURE__*/
35266
36162
  React__default.createElement(AttachmentFile$1, {