wavesurfer.js 7.11.0 → 7.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/memory-leaks.test.d.ts +7 -0
  3. package/dist/__tests__/memory-leaks.test.js +184 -0
  4. package/dist/__tests__/player.test.js +107 -0
  5. package/dist/__tests__/renderer-utils.test.d.ts +1 -0
  6. package/dist/__tests__/renderer-utils.test.js +373 -0
  7. package/dist/decoder.js +12 -4
  8. package/dist/draggable.d.ts +4 -0
  9. package/dist/draggable.js +20 -5
  10. package/dist/fetcher.js +14 -23
  11. package/dist/player.d.ts +21 -0
  12. package/dist/player.js +86 -0
  13. package/dist/plugins/envelope.cjs +1 -1
  14. package/dist/plugins/envelope.esm.js +1 -1
  15. package/dist/plugins/envelope.js +1 -1
  16. package/dist/plugins/envelope.min.js +1 -1
  17. package/dist/plugins/hover.cjs +1 -1
  18. package/dist/plugins/hover.d.ts +0 -3
  19. package/dist/plugins/hover.esm.js +1 -1
  20. package/dist/plugins/hover.js +1 -1
  21. package/dist/plugins/hover.min.js +1 -1
  22. package/dist/plugins/minimap.cjs +1 -1
  23. package/dist/plugins/minimap.esm.js +1 -1
  24. package/dist/plugins/minimap.js +1 -1
  25. package/dist/plugins/minimap.min.js +1 -1
  26. package/dist/plugins/record.cjs +1 -1
  27. package/dist/plugins/record.esm.js +1 -1
  28. package/dist/plugins/record.js +1 -1
  29. package/dist/plugins/record.min.js +1 -1
  30. package/dist/plugins/regions.cjs +1 -1
  31. package/dist/plugins/regions.esm.js +1 -1
  32. package/dist/plugins/regions.js +1 -1
  33. package/dist/plugins/regions.min.js +1 -1
  34. package/dist/plugins/timeline.cjs +1 -1
  35. package/dist/plugins/timeline.d.ts +1 -1
  36. package/dist/plugins/timeline.esm.js +1 -1
  37. package/dist/plugins/timeline.js +1 -1
  38. package/dist/plugins/timeline.min.js +1 -1
  39. package/dist/plugins/zoom.cjs +1 -1
  40. package/dist/plugins/zoom.d.ts +10 -1
  41. package/dist/plugins/zoom.esm.js +1 -1
  42. package/dist/plugins/zoom.js +1 -1
  43. package/dist/plugins/zoom.min.js +1 -1
  44. package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
  45. package/dist/reactive/__tests__/drag-stream.test.js +199 -0
  46. package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
  47. package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
  48. package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
  49. package/dist/reactive/__tests__/event-streams.test.js +278 -0
  50. package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
  51. package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
  52. package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
  53. package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
  54. package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
  55. package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
  56. package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
  57. package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
  58. package/dist/reactive/__tests__/store.test.d.ts +1 -0
  59. package/dist/reactive/__tests__/store.test.js +271 -0
  60. package/dist/reactive/drag-stream.d.ts +48 -0
  61. package/dist/reactive/drag-stream.js +146 -0
  62. package/dist/reactive/event-stream-emitter.d.ts +104 -0
  63. package/dist/reactive/event-stream-emitter.js +151 -0
  64. package/dist/reactive/event-streams.d.ts +65 -0
  65. package/dist/reactive/event-streams.js +146 -0
  66. package/dist/reactive/media-event-bridge.d.ts +51 -0
  67. package/dist/reactive/media-event-bridge.js +152 -0
  68. package/dist/reactive/render-scheduler.d.ts +43 -0
  69. package/dist/reactive/render-scheduler.js +75 -0
  70. package/dist/reactive/scroll-stream.d.ts +93 -0
  71. package/dist/reactive/scroll-stream.js +131 -0
  72. package/dist/reactive/state-event-emitter.d.ts +105 -0
  73. package/dist/reactive/state-event-emitter.js +213 -0
  74. package/dist/reactive/store.d.ts +66 -0
  75. package/dist/reactive/store.js +115 -0
  76. package/dist/renderer-utils.d.ts +126 -0
  77. package/dist/renderer-utils.js +252 -0
  78. package/dist/renderer.d.ts +4 -4
  79. package/dist/renderer.js +133 -197
  80. package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
  81. package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
  82. package/dist/state/wavesurfer-state.d.ts +90 -0
  83. package/dist/state/wavesurfer-state.js +124 -0
  84. package/dist/types.d.ts +167 -1
  85. package/dist/wavesurfer.cjs +1 -1
  86. package/dist/wavesurfer.d.ts +18 -0
  87. package/dist/wavesurfer.esm.js +1 -1
  88. package/dist/wavesurfer.js +46 -8
  89. package/dist/wavesurfer.min.js +1 -1
  90. package/dist/webaudio.js +4 -0
  91. package/package.json +6 -6
@@ -0,0 +1,268 @@
1
+ import { createWaveSurferState } from '../wavesurfer-state';
2
+ describe('WaveSurferState', () => {
3
+ it('should create state with default values', () => {
4
+ const { state } = createWaveSurferState();
5
+ expect(state.currentTime.value).toBe(0);
6
+ expect(state.duration.value).toBe(0);
7
+ expect(state.isPlaying.value).toBe(false);
8
+ expect(state.isPaused.value).toBe(true);
9
+ expect(state.isSeeking.value).toBe(false);
10
+ expect(state.volume.value).toBe(1);
11
+ expect(state.playbackRate.value).toBe(1);
12
+ expect(state.audioBuffer.value).toBeNull();
13
+ expect(state.peaks.value).toBeNull();
14
+ expect(state.url.value).toBe('');
15
+ expect(state.zoom.value).toBe(0);
16
+ expect(state.scrollPosition.value).toBe(0);
17
+ });
18
+ describe('actions', () => {
19
+ it('should update currentTime', () => {
20
+ const { state, actions } = createWaveSurferState();
21
+ actions.setCurrentTime(10);
22
+ expect(state.currentTime.value).toBe(10);
23
+ });
24
+ it('should clamp currentTime to duration', () => {
25
+ const { state, actions } = createWaveSurferState();
26
+ actions.setDuration(100);
27
+ actions.setCurrentTime(150);
28
+ expect(state.currentTime.value).toBe(100);
29
+ });
30
+ it('should not allow negative currentTime', () => {
31
+ const { state, actions } = createWaveSurferState();
32
+ actions.setCurrentTime(-10);
33
+ expect(state.currentTime.value).toBe(0);
34
+ });
35
+ it('should update duration', () => {
36
+ const { state, actions } = createWaveSurferState();
37
+ actions.setDuration(120);
38
+ expect(state.duration.value).toBe(120);
39
+ });
40
+ it('should not allow negative duration', () => {
41
+ const { state, actions } = createWaveSurferState();
42
+ actions.setDuration(-10);
43
+ expect(state.duration.value).toBe(0);
44
+ });
45
+ it('should update isPlaying', () => {
46
+ const { state, actions } = createWaveSurferState();
47
+ actions.setPlaying(true);
48
+ expect(state.isPlaying.value).toBe(true);
49
+ actions.setPlaying(false);
50
+ expect(state.isPlaying.value).toBe(false);
51
+ });
52
+ it('should update isSeeking', () => {
53
+ const { state, actions } = createWaveSurferState();
54
+ actions.setSeeking(true);
55
+ expect(state.isSeeking.value).toBe(true);
56
+ });
57
+ it('should update volume', () => {
58
+ const { state, actions } = createWaveSurferState();
59
+ actions.setVolume(0.5);
60
+ expect(state.volume.value).toBe(0.5);
61
+ });
62
+ it('should clamp volume between 0 and 1', () => {
63
+ const { state, actions } = createWaveSurferState();
64
+ actions.setVolume(-0.5);
65
+ expect(state.volume.value).toBe(0);
66
+ actions.setVolume(1.5);
67
+ expect(state.volume.value).toBe(1);
68
+ });
69
+ it('should update playbackRate', () => {
70
+ const { state, actions } = createWaveSurferState();
71
+ actions.setPlaybackRate(2);
72
+ expect(state.playbackRate.value).toBe(2);
73
+ });
74
+ it('should clamp playbackRate between 0.1 and 16', () => {
75
+ const { state, actions } = createWaveSurferState();
76
+ actions.setPlaybackRate(0.05);
77
+ expect(state.playbackRate.value).toBe(0.1);
78
+ actions.setPlaybackRate(20);
79
+ expect(state.playbackRate.value).toBe(16);
80
+ });
81
+ it('should update audioBuffer', () => {
82
+ const { state, actions } = createWaveSurferState();
83
+ const buffer = { duration: 120 };
84
+ actions.setAudioBuffer(buffer);
85
+ expect(state.audioBuffer.value).toBe(buffer);
86
+ });
87
+ it('should update duration when audioBuffer is set', () => {
88
+ const { state, actions } = createWaveSurferState();
89
+ const buffer = { duration: 120 };
90
+ actions.setAudioBuffer(buffer);
91
+ expect(state.duration.value).toBe(120);
92
+ });
93
+ it('should update peaks', () => {
94
+ const { state, actions } = createWaveSurferState();
95
+ const peaks = [new Float32Array([1, 2, 3])];
96
+ actions.setPeaks(peaks);
97
+ expect(state.peaks.value).toBe(peaks);
98
+ });
99
+ it('should update url', () => {
100
+ const { state, actions } = createWaveSurferState();
101
+ actions.setUrl('/audio.mp3');
102
+ expect(state.url.value).toBe('/audio.mp3');
103
+ });
104
+ it('should update zoom', () => {
105
+ const { state, actions } = createWaveSurferState();
106
+ actions.setZoom(100);
107
+ expect(state.zoom.value).toBe(100);
108
+ });
109
+ it('should not allow negative zoom', () => {
110
+ const { state, actions } = createWaveSurferState();
111
+ actions.setZoom(-10);
112
+ expect(state.zoom.value).toBe(0);
113
+ });
114
+ it('should update scrollPosition', () => {
115
+ const { state, actions } = createWaveSurferState();
116
+ actions.setScrollPosition(50);
117
+ expect(state.scrollPosition.value).toBe(50);
118
+ });
119
+ it('should not allow negative scrollPosition', () => {
120
+ const { state, actions } = createWaveSurferState();
121
+ actions.setScrollPosition(-10);
122
+ expect(state.scrollPosition.value).toBe(0);
123
+ });
124
+ });
125
+ describe('computed values', () => {
126
+ it('should compute isPaused from isPlaying', () => {
127
+ const { state, actions } = createWaveSurferState();
128
+ expect(state.isPaused.value).toBe(true);
129
+ actions.setPlaying(true);
130
+ expect(state.isPaused.value).toBe(false);
131
+ actions.setPlaying(false);
132
+ expect(state.isPaused.value).toBe(true);
133
+ });
134
+ it('should compute canPlay from audioBuffer', () => {
135
+ const { state, actions } = createWaveSurferState();
136
+ expect(state.canPlay.value).toBe(false);
137
+ const buffer = { duration: 120 };
138
+ actions.setAudioBuffer(buffer);
139
+ expect(state.canPlay.value).toBe(true);
140
+ actions.setAudioBuffer(null);
141
+ expect(state.canPlay.value).toBe(false);
142
+ });
143
+ it('should compute isReady from canPlay and duration', () => {
144
+ const { state, actions } = createWaveSurferState();
145
+ expect(state.isReady.value).toBe(false);
146
+ // Only buffer, no duration
147
+ const buffer = { duration: 0 };
148
+ actions.setAudioBuffer(buffer);
149
+ expect(state.isReady.value).toBe(false);
150
+ // Both buffer and duration
151
+ actions.setDuration(120);
152
+ expect(state.isReady.value).toBe(true);
153
+ // Remove buffer
154
+ actions.setAudioBuffer(null);
155
+ expect(state.isReady.value).toBe(false);
156
+ });
157
+ it('should compute progress from currentTime', () => {
158
+ const { state, actions } = createWaveSurferState();
159
+ expect(state.progress.value).toBe(0);
160
+ actions.setCurrentTime(50);
161
+ expect(state.progress.value).toBe(50);
162
+ });
163
+ it('should compute progressPercent from currentTime and duration', () => {
164
+ const { state, actions } = createWaveSurferState();
165
+ expect(state.progressPercent.value).toBe(0);
166
+ actions.setDuration(100);
167
+ actions.setCurrentTime(25);
168
+ expect(state.progressPercent.value).toBe(0.25);
169
+ actions.setCurrentTime(50);
170
+ expect(state.progressPercent.value).toBe(0.5);
171
+ actions.setCurrentTime(100);
172
+ expect(state.progressPercent.value).toBe(1);
173
+ });
174
+ it('should handle progressPercent with zero duration', () => {
175
+ const { state, actions } = createWaveSurferState();
176
+ actions.setDuration(0);
177
+ actions.setCurrentTime(10);
178
+ expect(state.progressPercent.value).toBe(0);
179
+ });
180
+ });
181
+ describe('subscriptions', () => {
182
+ it('should notify subscribers on state changes', () => {
183
+ const { state, actions } = createWaveSurferState();
184
+ const callback = jest.fn();
185
+ state.isPlaying.subscribe(callback);
186
+ actions.setPlaying(true);
187
+ expect(callback).toHaveBeenCalledWith(true);
188
+ });
189
+ it('should notify computed value subscribers', () => {
190
+ const { state, actions } = createWaveSurferState();
191
+ const callback = jest.fn();
192
+ state.progressPercent.subscribe(callback);
193
+ actions.setDuration(100);
194
+ actions.setCurrentTime(50);
195
+ expect(callback).toHaveBeenCalledWith(0.5);
196
+ });
197
+ it('should work with multiple subscribers', () => {
198
+ const { state, actions } = createWaveSurferState();
199
+ const callback1 = jest.fn();
200
+ const callback2 = jest.fn();
201
+ state.isPlaying.subscribe(callback1);
202
+ state.isPlaying.subscribe(callback2);
203
+ actions.setPlaying(true);
204
+ expect(callback1).toHaveBeenCalledWith(true);
205
+ expect(callback2).toHaveBeenCalledWith(true);
206
+ });
207
+ });
208
+ describe('state isolation', () => {
209
+ it('should create independent state instances', () => {
210
+ const instance1 = createWaveSurferState();
211
+ const instance2 = createWaveSurferState();
212
+ instance1.actions.setCurrentTime(10);
213
+ instance2.actions.setCurrentTime(20);
214
+ expect(instance1.state.currentTime.value).toBe(10);
215
+ expect(instance2.state.currentTime.value).toBe(20);
216
+ });
217
+ it('should not share state between instances', () => {
218
+ const instance1 = createWaveSurferState();
219
+ const instance2 = createWaveSurferState();
220
+ instance1.actions.setPlaying(true);
221
+ expect(instance1.state.isPlaying.value).toBe(true);
222
+ expect(instance2.state.isPlaying.value).toBe(false);
223
+ });
224
+ });
225
+ describe('complex state updates', () => {
226
+ it('should handle multiple rapid updates', () => {
227
+ const { state, actions } = createWaveSurferState();
228
+ const values = [];
229
+ state.currentTime.subscribe((time) => values.push(time));
230
+ // Start from 1 since 0 is the initial value (no change from initial)
231
+ for (let i = 1; i < 100; i++) {
232
+ actions.setCurrentTime(i);
233
+ }
234
+ expect(values).toHaveLength(99);
235
+ expect(state.currentTime.value).toBe(99);
236
+ });
237
+ it('should maintain consistency across dependent computed values', () => {
238
+ const { state, actions } = createWaveSurferState();
239
+ actions.setDuration(100);
240
+ actions.setCurrentTime(50);
241
+ const buffer = { duration: 100 };
242
+ actions.setAudioBuffer(buffer);
243
+ expect(state.progressPercent.value).toBe(0.5);
244
+ expect(state.canPlay.value).toBe(true);
245
+ expect(state.isReady.value).toBe(true);
246
+ actions.setCurrentTime(75);
247
+ expect(state.progressPercent.value).toBe(0.75);
248
+ expect(state.progress.value).toBe(75);
249
+ });
250
+ it('should handle state reset correctly', () => {
251
+ const { state, actions } = createWaveSurferState();
252
+ // Set some state
253
+ actions.setDuration(100);
254
+ actions.setCurrentTime(50);
255
+ actions.setPlaying(true);
256
+ actions.setVolume(0.5);
257
+ // Reset
258
+ actions.setCurrentTime(0);
259
+ actions.setDuration(0);
260
+ actions.setPlaying(false);
261
+ actions.setVolume(1);
262
+ expect(state.currentTime.value).toBe(0);
263
+ expect(state.duration.value).toBe(0);
264
+ expect(state.isPlaying.value).toBe(false);
265
+ expect(state.volume.value).toBe(1);
266
+ });
267
+ });
268
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Centralized reactive state for WaveSurfer
3
+ *
4
+ * This module provides a single source of truth for all WaveSurfer state.
5
+ * State is managed using reactive signals that automatically notify subscribers.
6
+ */
7
+ import { type Signal, type WritableSignal } from '../reactive/store.js';
8
+ /**
9
+ * Read-only reactive state for WaveSurfer
10
+ */
11
+ export interface WaveSurferState {
12
+ readonly currentTime: Signal<number>;
13
+ readonly duration: Signal<number>;
14
+ readonly isPlaying: Signal<boolean>;
15
+ readonly isPaused: Signal<boolean>;
16
+ readonly isSeeking: Signal<boolean>;
17
+ readonly volume: Signal<number>;
18
+ readonly playbackRate: Signal<number>;
19
+ readonly audioBuffer: Signal<AudioBuffer | null>;
20
+ readonly peaks: Signal<Array<Float32Array | number[]> | null>;
21
+ readonly url: Signal<string>;
22
+ readonly zoom: Signal<number>;
23
+ readonly scrollPosition: Signal<number>;
24
+ readonly canPlay: Signal<boolean>;
25
+ readonly isReady: Signal<boolean>;
26
+ readonly progress: Signal<number>;
27
+ readonly progressPercent: Signal<number>;
28
+ }
29
+ /**
30
+ * Actions for updating WaveSurfer state
31
+ */
32
+ export interface WaveSurferActions {
33
+ setCurrentTime: (time: number) => void;
34
+ setDuration: (duration: number) => void;
35
+ setPlaying: (playing: boolean) => void;
36
+ setSeeking: (seeking: boolean) => void;
37
+ setVolume: (volume: number) => void;
38
+ setPlaybackRate: (rate: number) => void;
39
+ setAudioBuffer: (buffer: AudioBuffer | null) => void;
40
+ setPeaks: (peaks: Array<Float32Array | number[]> | null) => void;
41
+ setUrl: (url: string) => void;
42
+ setZoom: (zoom: number) => void;
43
+ setScrollPosition: (position: number) => void;
44
+ }
45
+ /**
46
+ * Optional Player signals to compose into WaveSurferState
47
+ * When provided, these signals from Player are used directly instead of creating new ones
48
+ * Note: Signals must be WritableSignal to allow state actions to update them
49
+ */
50
+ export interface PlayerSignals {
51
+ isPlaying?: WritableSignal<boolean>;
52
+ currentTime?: WritableSignal<number>;
53
+ duration?: WritableSignal<number>;
54
+ volume?: WritableSignal<number>;
55
+ playbackRate?: WritableSignal<number>;
56
+ isSeeking?: WritableSignal<boolean>;
57
+ }
58
+ /**
59
+ * Create a new WaveSurfer state instance
60
+ *
61
+ * @param playerSignals - Optional signals from Player to compose with WaveSurfer state
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Without Player signals (standalone)
66
+ * const { state, actions } = createWaveSurferState()
67
+ *
68
+ * // With Player signals (composed)
69
+ * const { state, actions } = createWaveSurferState({
70
+ * isPlaying: player.isPlayingSignal,
71
+ * currentTime: player.currentTimeSignal,
72
+ * // ...
73
+ * })
74
+ *
75
+ * // Read state
76
+ * console.log(state.isPlaying.value)
77
+ *
78
+ * // Update state
79
+ * actions.setPlaying(true)
80
+ *
81
+ * // Subscribe to changes
82
+ * state.isPlaying.subscribe(playing => {
83
+ * console.log('Playing:', playing)
84
+ * })
85
+ * ```
86
+ */
87
+ export declare function createWaveSurferState(playerSignals?: PlayerSignals): {
88
+ state: WaveSurferState;
89
+ actions: WaveSurferActions;
90
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Centralized reactive state for WaveSurfer
3
+ *
4
+ * This module provides a single source of truth for all WaveSurfer state.
5
+ * State is managed using reactive signals that automatically notify subscribers.
6
+ */
7
+ import { signal, computed } from '../reactive/store.js';
8
+ /**
9
+ * Create a new WaveSurfer state instance
10
+ *
11
+ * @param playerSignals - Optional signals from Player to compose with WaveSurfer state
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Without Player signals (standalone)
16
+ * const { state, actions } = createWaveSurferState()
17
+ *
18
+ * // With Player signals (composed)
19
+ * const { state, actions } = createWaveSurferState({
20
+ * isPlaying: player.isPlayingSignal,
21
+ * currentTime: player.currentTimeSignal,
22
+ * // ...
23
+ * })
24
+ *
25
+ * // Read state
26
+ * console.log(state.isPlaying.value)
27
+ *
28
+ * // Update state
29
+ * actions.setPlaying(true)
30
+ *
31
+ * // Subscribe to changes
32
+ * state.isPlaying.subscribe(playing => {
33
+ * console.log('Playing:', playing)
34
+ * })
35
+ * ```
36
+ */
37
+ export function createWaveSurferState(playerSignals) {
38
+ var _a, _b, _c, _d, _e, _f;
39
+ // Use Player signals if provided, otherwise create new ones
40
+ const currentTime = (_a = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.currentTime) !== null && _a !== void 0 ? _a : signal(0);
41
+ const duration = (_b = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.duration) !== null && _b !== void 0 ? _b : signal(0);
42
+ const isPlaying = (_c = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.isPlaying) !== null && _c !== void 0 ? _c : signal(false);
43
+ const isSeeking = (_d = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.isSeeking) !== null && _d !== void 0 ? _d : signal(false);
44
+ const volume = (_e = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.volume) !== null && _e !== void 0 ? _e : signal(1);
45
+ const playbackRate = (_f = playerSignals === null || playerSignals === void 0 ? void 0 : playerSignals.playbackRate) !== null && _f !== void 0 ? _f : signal(1);
46
+ // WaveSurfer-specific signals (not in Player)
47
+ const audioBuffer = signal(null);
48
+ const peaks = signal(null);
49
+ const url = signal('');
50
+ const zoom = signal(0);
51
+ const scrollPosition = signal(0);
52
+ // Computed values (derived state)
53
+ const isPaused = computed(() => !isPlaying.value, [isPlaying]);
54
+ const canPlay = computed(() => audioBuffer.value !== null, [audioBuffer]);
55
+ const isReady = computed(() => {
56
+ return canPlay.value && duration.value > 0;
57
+ }, [canPlay, duration]);
58
+ const progress = computed(() => currentTime.value, [currentTime]);
59
+ const progressPercent = computed(() => {
60
+ return duration.value > 0 ? currentTime.value / duration.value : 0;
61
+ }, [currentTime, duration]);
62
+ // Public read-only state
63
+ const state = {
64
+ currentTime,
65
+ duration,
66
+ isPlaying,
67
+ isPaused,
68
+ isSeeking,
69
+ volume,
70
+ playbackRate,
71
+ audioBuffer,
72
+ peaks,
73
+ url,
74
+ zoom,
75
+ scrollPosition,
76
+ canPlay,
77
+ isReady,
78
+ progress,
79
+ progressPercent,
80
+ };
81
+ // Actions that modify state
82
+ const actions = {
83
+ setCurrentTime: (time) => {
84
+ const clampedTime = Math.max(0, Math.min(duration.value || Infinity, time));
85
+ currentTime.set(clampedTime);
86
+ },
87
+ setDuration: (d) => {
88
+ duration.set(Math.max(0, d));
89
+ },
90
+ setPlaying: (playing) => {
91
+ isPlaying.set(playing);
92
+ },
93
+ setSeeking: (seeking) => {
94
+ isSeeking.set(seeking);
95
+ },
96
+ setVolume: (v) => {
97
+ const clampedVolume = Math.max(0, Math.min(1, v));
98
+ volume.set(clampedVolume);
99
+ },
100
+ setPlaybackRate: (rate) => {
101
+ const clampedRate = Math.max(0.1, Math.min(16, rate));
102
+ playbackRate.set(clampedRate);
103
+ },
104
+ setAudioBuffer: (buffer) => {
105
+ audioBuffer.set(buffer);
106
+ if (buffer) {
107
+ duration.set(buffer.duration);
108
+ }
109
+ },
110
+ setPeaks: (p) => {
111
+ peaks.set(p);
112
+ },
113
+ setUrl: (u) => {
114
+ url.set(u);
115
+ },
116
+ setZoom: (z) => {
117
+ zoom.set(Math.max(0, z));
118
+ },
119
+ setScrollPosition: (pos) => {
120
+ scrollPosition.set(Math.max(0, pos));
121
+ },
122
+ };
123
+ return { state, actions };
124
+ }