@waveform-playlist/browser 15.1.0 → 15.3.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.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @waveform-playlist/browser
2
+
3
+ React components, hooks, and context providers for building a multitrack Web Audio editor and player — canvas waveform rendering, clip-based editing, and playback controls on top of Tone.js or a native `HTMLAudioElement`.
4
+
5
+ ## Features
6
+
7
+ - **Multitrack editor** — `<Waveform />` renders canvas waveforms, per-track controls (mute/solo/volume/pan), a time ruler, and a playhead for any number of tracks
8
+ - **Clip-based editing** — drag-to-move, boundary trimming with collision detection, splitting at the playhead, snap-to-grid (beats/bars or timescale), undo/redo
9
+ - **Split context providers** — `WaveformPlaylistProvider` (multitrack, Tone.js-backed) and `MediaElementPlaylistProvider` (single-track, `<audio>`-backed with pitch-preserving playback rate)
10
+ - **Four focused context hooks** — `usePlaybackAnimation`, `usePlaylistState`, `usePlaylistControls`, `usePlaylistData` isolate 60fps playhead updates from low-frequency state so unrelated components don't re-render on every animation frame
11
+ - **Audio effects** — 20 Tone.js effects (reverb, delay, modulation, filter, distortion, dynamics, spatial) with runtime parameter control, master and per-track chains, plus WAM 2.0 plugin hosting
12
+ - **Recording** — pairs with `@waveform-playlist/recording` for mic input, overdubbing, and live waveform preview; punch-in takes replace overlapped content, and `setRecordingActive(active, armedTrackId)` suppresses the end-of-timeline auto-stop and transiently mutes the recorded-over track for the session
13
+ - **Annotations & spectrogram** — optional integration contexts for `@waveform-playlist/annotations` and `@waveform-playlist/spectrogram`
14
+ - **WAV export** — render the full mix (including effects) offline to a downloadable WAV file
15
+ - **MediaElement player mode** — a lightweight single-track player with pitch-preserving speed control, for podcast/audiobook-style use cases that don't need multitrack mixing
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @waveform-playlist/browser tone @dnd-kit/react
21
+ ```
22
+
23
+ `@waveform-playlist/browser` requires these peer dependencies:
24
+
25
+ | Package | Purpose |
26
+ |---------|---------|
27
+ | `react` / `react-dom` | ^18.0.0 or ^19.0.0 |
28
+ | `styled-components` | ^6.0.0 — CSS-in-JS styling |
29
+ | `tone` | ^15.0.0 — Web Audio engine (optional if you supply your own `PlayoutAdapter`) |
30
+ | `@dnd-kit/react` | ^0.3.0 — drag-and-drop (includes `@dnd-kit/dom` and `@dnd-kit/abstract`) |
31
+ | `@waveform-playlist/playout` | Tone.js playout engine (optional peer, dynamically imported by default) |
32
+ | `@waveform-playlist/media-element-playout` | engine for `MediaElementPlaylistProvider` (optional peer) |
33
+ | `@waveform-playlist/annotations` / `@waveform-playlist/recording` / `@dawcore/wam` | optional, install only if you use those features |
34
+
35
+ **Engine-free core:** the package's main entry has no static dependency on `tone` or `@waveform-playlist/playout` — a `MediaElementPlaylistProvider`-only consumer, or one supplying a custom `createAdapter`, never resolves either. Tone-coupled exports (`useAudioTracks`, `useDynamicTracks`, effects hooks, `useExportWav`, `useOutputMeter`, `useMasterAnalyser`) live at the **`@waveform-playlist/browser/tone`** subpath:
36
+
37
+ ```typescript
38
+ import { useAudioTracks, useDynamicEffects, useExportWav } from '@waveform-playlist/browser/tone';
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```tsx
44
+ import {
45
+ WaveformPlaylistProvider,
46
+ Waveform,
47
+ PlayButton,
48
+ PauseButton,
49
+ StopButton,
50
+ } from '@waveform-playlist/browser';
51
+ import { useAudioTracks } from '@waveform-playlist/browser/tone';
52
+
53
+ function MyPlaylist() {
54
+ const { tracks, loading, error } = useAudioTracks([
55
+ { src: '/audio/vocals.mp3', name: 'Vocals' },
56
+ { src: '/audio/guitar.mp3', name: 'Guitar' },
57
+ ]);
58
+
59
+ if (loading) return <div>Loading audio...</div>;
60
+ if (error) return <div>Error: {error}</div>;
61
+
62
+ return (
63
+ <WaveformPlaylistProvider
64
+ tracks={tracks}
65
+ samplesPerPixel={1024}
66
+ waveHeight={128}
67
+ timescale
68
+ controls={{ show: true, width: 200 }}
69
+ >
70
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
71
+ <PlayButton />
72
+ <PauseButton />
73
+ <StopButton />
74
+ </div>
75
+ <Waveform />
76
+ </WaveformPlaylistProvider>
77
+ );
78
+ }
79
+ ```
80
+
81
+ Prefer a lightweight single-track player instead? Use `MediaElementPlaylistProvider` with `@waveform-playlist/media-element-playout` — no Tone.js, no AudioBuffer decoding, and pitch-preserving playback rate out of the box.
82
+
83
+ ## Context Hooks
84
+
85
+ `WaveformPlaylistProvider` splits its context into four hooks so components only re-render for the data they actually read:
86
+
87
+ | Hook | Exposes |
88
+ |------|---------|
89
+ | `usePlaybackAnimation` | `isPlaying`, `currentTime`, animation refs, `getPlaybackTime()`, `registerFrameCallback()` for 60fps-driven UI (playhead, progress bars) |
90
+ | `usePlaylistState` | Low-frequency state — selection, loop region, annotations, `selectedTrackId`, `canUndo`/`canRedo` |
91
+ | `usePlaylistControls` | Actions — `play`, `pause`, `stop`, `seekTo`, track mute/solo/volume/pan, zoom, undo/redo, `setRecordingActive` (recording-session suppression + armed-track mute) |
92
+ | `usePlaylistData` | Derived/config data — `tracks`, `duration`, `sampleRate`, `peaksDataArray`, `trackStates`, `isReady` |
93
+
94
+ `usePlaylistDataOptional` is the same as `usePlaylistData` but returns `null` outside a provider (for components that render both inside and outside one). `MediaElementPlaylistProvider` has the equivalent `useMediaElementAnimation`, `useMediaElementState`, `useMediaElementControls`, `useMediaElementData`.
95
+
96
+ ## Examples & Documentation
97
+
98
+ - [naomiaro.github.io/waveform-playlist](https://naomiaro.github.io/waveform-playlist/) — guides, API reference, and live examples
99
+ - [Basic Usage guide](https://naomiaro.github.io/waveform-playlist/docs/react/getting-started/basic-usage) — walkthrough of the provider pattern
100
+ - [`examples/media-element-player`](https://github.com/naomiaro/waveform-playlist/tree/main/examples/media-element-player) — `MediaElementPlaylistProvider` starter (`pnpm example:media-element`)
101
+
102
+ ## License
103
+
104
+ MIT
package/dist/index.d.mts CHANGED
@@ -94,8 +94,12 @@ interface PlaylistStateContextValue {
94
94
  selectedTrackId: string | null;
95
95
  loopStart: number;
96
96
  loopEnd: number;
97
- /** Whether playback continues past the end of loaded audio */
97
+ /** Whether playback continues past the end of loaded audio instead of
98
+ * auto-stopping (implies the fillViewport layout) */
98
99
  indefinitePlayback: boolean;
100
+ /** Whether the timeline visually fills the scroll container even when the
101
+ * audio is shorter (layout only — no effect on playback) */
102
+ fillViewport: boolean;
99
103
  /** Whether undo is available */
100
104
  canUndo: boolean;
101
105
  /** Whether redo is available */
@@ -132,6 +136,16 @@ interface PlaylistControlsContextValue {
132
136
  clearLoopRegion: () => void;
133
137
  undo: () => void;
134
138
  redo: () => void;
139
+ /** Mark a recording session active/inactive. While active: (1) the
140
+ * end-of-audio auto-stop is suppressed so overdub playback runs past the
141
+ * end of existing material, and (2) with an `armedTrackId`, that track's
142
+ * existing content is transiently muted — punch-in recording replaces
143
+ * whatever the take overlaps (#579), so the doomed material must not play
144
+ * under the overdub; its previous mute state is restored when the session
145
+ * ends (audio-only, the UI mute control is untouched). Wired automatically
146
+ * from the Waveform / PlaylistVisualization `recordingState` prop; overdub
147
+ * flows should also call it eagerly (before `play()`). */
148
+ setRecordingActive: (active: boolean, armedTrackId?: string | null) => void;
135
149
  }
136
150
  interface PlaylistDataContextValue {
137
151
  duration: number;
@@ -155,6 +169,8 @@ interface PlaylistDataContextValue {
155
169
  canZoomOut: boolean;
156
170
  barWidth: number;
157
171
  barGap: number;
172
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). */
173
+ roundedBars: boolean;
158
174
  /** Width in pixels of progress bars. Defaults to barWidth + barGap (fills gaps). */
159
175
  progressBarWidth: number;
160
176
  /** Whether the playlist has finished loading all tracks */
@@ -200,6 +216,8 @@ interface WaveformPlaylistProviderProps {
200
216
  barWidth?: number;
201
217
  /** Spacing in pixels between waveform bars. Default: 0 */
202
218
  barGap?: number;
219
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
220
+ roundedBars?: boolean;
203
221
  /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). */
204
222
  progressBarWidth?: number;
205
223
  /** Callback when engine clip operations (move, trim, split) change tracks.
@@ -218,9 +236,17 @@ interface WaveformPlaylistProviderProps {
218
236
  * Use this during progressive loading to avoid rebuilding the engine for
219
237
  * each track — flip to false when all tracks are ready for a single build. */
220
238
  deferEngineRebuild?: boolean;
221
- /** Disable automatic stop when the cursor reaches the end of the longest
222
- * track. Useful for DAW-style recording beyond existing audio. */
239
+ /** Disable the automatic stop when the cursor reaches the end of the
240
+ * longest track the transport rolls until an explicit stop/pause
241
+ * (DAW-style). Implies the fillViewport layout. Recording sessions
242
+ * suppress the auto-stop automatically, so most recording UIs don't
243
+ * need this. */
223
244
  indefinitePlayback?: boolean;
245
+ /** Extend the timeline (background + timescale) to fill the visible scroll
246
+ * container even when the audio is shorter. Layout only. Recording UIs
247
+ * typically want this so the empty timeline doesn't collapse to the audio
248
+ * width. */
249
+ fillViewport?: boolean;
224
250
  /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at
225
251
  * this rate via standardized-audio-context. Pre-computed peaks (.dat files)
226
252
  * render instantly when they match. On mismatch, falls back to worker. */
@@ -298,6 +324,8 @@ interface MediaElementDataContextValue {
298
324
  };
299
325
  barWidth: number;
300
326
  barGap: number;
327
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). */
328
+ roundedBars: boolean;
301
329
  progressBarWidth: number;
302
330
  fadeIn?: FadeConfig;
303
331
  fadeOut?: FadeConfig;
@@ -334,6 +362,8 @@ interface MediaElementPlaylistProviderProps {
334
362
  barWidth?: number;
335
363
  /** Gap between waveform bars */
336
364
  barGap?: number;
365
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
366
+ roundedBars?: boolean;
337
367
  /** Width of progress bars */
338
368
  progressBarWidth?: number;
339
369
  /** Callback when annotations are changed (drag, edit, etc.) */
package/dist/index.d.ts CHANGED
@@ -94,8 +94,12 @@ interface PlaylistStateContextValue {
94
94
  selectedTrackId: string | null;
95
95
  loopStart: number;
96
96
  loopEnd: number;
97
- /** Whether playback continues past the end of loaded audio */
97
+ /** Whether playback continues past the end of loaded audio instead of
98
+ * auto-stopping (implies the fillViewport layout) */
98
99
  indefinitePlayback: boolean;
100
+ /** Whether the timeline visually fills the scroll container even when the
101
+ * audio is shorter (layout only — no effect on playback) */
102
+ fillViewport: boolean;
99
103
  /** Whether undo is available */
100
104
  canUndo: boolean;
101
105
  /** Whether redo is available */
@@ -132,6 +136,16 @@ interface PlaylistControlsContextValue {
132
136
  clearLoopRegion: () => void;
133
137
  undo: () => void;
134
138
  redo: () => void;
139
+ /** Mark a recording session active/inactive. While active: (1) the
140
+ * end-of-audio auto-stop is suppressed so overdub playback runs past the
141
+ * end of existing material, and (2) with an `armedTrackId`, that track's
142
+ * existing content is transiently muted — punch-in recording replaces
143
+ * whatever the take overlaps (#579), so the doomed material must not play
144
+ * under the overdub; its previous mute state is restored when the session
145
+ * ends (audio-only, the UI mute control is untouched). Wired automatically
146
+ * from the Waveform / PlaylistVisualization `recordingState` prop; overdub
147
+ * flows should also call it eagerly (before `play()`). */
148
+ setRecordingActive: (active: boolean, armedTrackId?: string | null) => void;
135
149
  }
136
150
  interface PlaylistDataContextValue {
137
151
  duration: number;
@@ -155,6 +169,8 @@ interface PlaylistDataContextValue {
155
169
  canZoomOut: boolean;
156
170
  barWidth: number;
157
171
  barGap: number;
172
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). */
173
+ roundedBars: boolean;
158
174
  /** Width in pixels of progress bars. Defaults to barWidth + barGap (fills gaps). */
159
175
  progressBarWidth: number;
160
176
  /** Whether the playlist has finished loading all tracks */
@@ -200,6 +216,8 @@ interface WaveformPlaylistProviderProps {
200
216
  barWidth?: number;
201
217
  /** Spacing in pixels between waveform bars. Default: 0 */
202
218
  barGap?: number;
219
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
220
+ roundedBars?: boolean;
203
221
  /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). */
204
222
  progressBarWidth?: number;
205
223
  /** Callback when engine clip operations (move, trim, split) change tracks.
@@ -218,9 +236,17 @@ interface WaveformPlaylistProviderProps {
218
236
  * Use this during progressive loading to avoid rebuilding the engine for
219
237
  * each track — flip to false when all tracks are ready for a single build. */
220
238
  deferEngineRebuild?: boolean;
221
- /** Disable automatic stop when the cursor reaches the end of the longest
222
- * track. Useful for DAW-style recording beyond existing audio. */
239
+ /** Disable the automatic stop when the cursor reaches the end of the
240
+ * longest track the transport rolls until an explicit stop/pause
241
+ * (DAW-style). Implies the fillViewport layout. Recording sessions
242
+ * suppress the auto-stop automatically, so most recording UIs don't
243
+ * need this. */
223
244
  indefinitePlayback?: boolean;
245
+ /** Extend the timeline (background + timescale) to fill the visible scroll
246
+ * container even when the audio is shorter. Layout only. Recording UIs
247
+ * typically want this so the empty timeline doesn't collapse to the audio
248
+ * width. */
249
+ fillViewport?: boolean;
224
250
  /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at
225
251
  * this rate via standardized-audio-context. Pre-computed peaks (.dat files)
226
252
  * render instantly when they match. On mismatch, falls back to worker. */
@@ -298,6 +324,8 @@ interface MediaElementDataContextValue {
298
324
  };
299
325
  barWidth: number;
300
326
  barGap: number;
327
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). */
328
+ roundedBars: boolean;
301
329
  progressBarWidth: number;
302
330
  fadeIn?: FadeConfig;
303
331
  fadeOut?: FadeConfig;
@@ -334,6 +362,8 @@ interface MediaElementPlaylistProviderProps {
334
362
  barWidth?: number;
335
363
  /** Gap between waveform bars */
336
364
  barGap?: number;
365
+ /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */
366
+ roundedBars?: boolean;
337
367
  /** Width of progress bars */
338
368
  progressBarWidth?: number;
339
369
  /** Callback when annotations are changed (drag, edit, etc.) */
package/dist/index.js CHANGED
@@ -1875,19 +1875,19 @@ var WaveformPlaylistProvider = ({
1875
1875
  onAnnotationsChange,
1876
1876
  barWidth = 1,
1877
1877
  barGap = 0,
1878
+ roundedBars = false,
1878
1879
  progressBarWidth: progressBarWidthProp,
1879
1880
  onTracksChange,
1880
1881
  soundFontCache,
1881
1882
  deferEngineRebuild = false,
1882
1883
  indefinitePlayback = false,
1884
+ fillViewport = false,
1883
1885
  sampleRate: sampleRateProp,
1884
1886
  createAdapter,
1885
1887
  children
1886
1888
  }) => {
1887
1889
  var _a, _b, _c, _d;
1888
1890
  const progressBarWidth = progressBarWidthProp != null ? progressBarWidthProp : barWidth + barGap;
1889
- const indefinitePlaybackRef = (0, import_react17.useRef)(indefinitePlayback);
1890
- indefinitePlaybackRef.current = indefinitePlayback;
1891
1891
  const stableZoomLevels = (0, import_react17.useMemo)(
1892
1892
  () => zoomLevels,
1893
1893
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1925,6 +1925,28 @@ var WaveformPlaylistProvider = ({
1925
1925
  const engineRef = (0, import_react17.useRef)(null);
1926
1926
  const adapterRef = (0, import_react17.useRef)(null);
1927
1927
  const audioInitializedRef = (0, import_react17.useRef)(false);
1928
+ const indefinitePlaybackRef = (0, import_react17.useRef)(indefinitePlayback);
1929
+ indefinitePlaybackRef.current = indefinitePlayback;
1930
+ const recordingActiveRef = (0, import_react17.useRef)(false);
1931
+ const armedTrackMuteRef = (0, import_react17.useRef)(null);
1932
+ const setRecordingActive = (0, import_react17.useCallback)((active, armedTrackId) => {
1933
+ var _a2, _b2, _c2;
1934
+ recordingActiveRef.current = active;
1935
+ const engine = engineRef.current;
1936
+ if (!engine) return;
1937
+ if (active && armedTrackId) {
1938
+ if (((_a2 = armedTrackMuteRef.current) == null ? void 0 : _a2.trackId) === armedTrackId) return;
1939
+ if (armedTrackMuteRef.current) {
1940
+ engine.setTrackMute(armedTrackMuteRef.current.trackId, armedTrackMuteRef.current.prevMuted);
1941
+ }
1942
+ const prevMuted = (_c2 = (_b2 = engine.getState().tracks.find((t) => t.id === armedTrackId)) == null ? void 0 : _b2.muted) != null ? _c2 : false;
1943
+ armedTrackMuteRef.current = { trackId: armedTrackId, prevMuted };
1944
+ engine.setTrackMute(armedTrackId, true);
1945
+ } else if (!active && armedTrackMuteRef.current) {
1946
+ engine.setTrackMute(armedTrackMuteRef.current.trackId, armedTrackMuteRef.current.prevMuted);
1947
+ armedTrackMuteRef.current = null;
1948
+ }
1949
+ }, []);
1928
1950
  const isPlayingRef = (0, import_react17.useRef)(false);
1929
1951
  isPlayingRef.current = isPlaying;
1930
1952
  const playStartPositionRef = (0, import_react17.useRef)(0);
@@ -2516,7 +2538,7 @@ var WaveformPlaylistProvider = ({
2516
2538
  playbackEndTimeRef.current = null;
2517
2539
  return;
2518
2540
  }
2519
- if (time >= duration && !indefinitePlaybackRef.current) {
2541
+ if (time >= duration && !indefinitePlaybackRef.current && !recordingActiveRef.current) {
2520
2542
  if (engineRef.current) {
2521
2543
  engineRef.current.stop();
2522
2544
  }
@@ -2787,6 +2809,7 @@ var WaveformPlaylistProvider = ({
2787
2809
  loopStart,
2788
2810
  loopEnd,
2789
2811
  indefinitePlayback,
2812
+ fillViewport,
2790
2813
  canUndo,
2791
2814
  canRedo
2792
2815
  }),
@@ -2804,6 +2827,7 @@ var WaveformPlaylistProvider = ({
2804
2827
  loopStart,
2805
2828
  loopEnd,
2806
2829
  indefinitePlayback,
2830
+ fillViewport,
2807
2831
  canUndo,
2808
2832
  canRedo
2809
2833
  ]
@@ -2859,7 +2883,9 @@ var WaveformPlaylistProvider = ({
2859
2883
  clearLoopRegion,
2860
2884
  // Undo/redo
2861
2885
  undo,
2862
- redo
2886
+ redo,
2887
+ // Recording
2888
+ setRecordingActive
2863
2889
  }),
2864
2890
  [
2865
2891
  play,
@@ -2891,7 +2917,8 @@ var WaveformPlaylistProvider = ({
2891
2917
  setLoopRegionFromSelection,
2892
2918
  clearLoopRegion,
2893
2919
  undo,
2894
- redo
2920
+ redo,
2921
+ setRecordingActive
2895
2922
  ]
2896
2923
  );
2897
2924
  const dataValue = (0, import_react17.useMemo)(
@@ -2914,6 +2941,7 @@ var WaveformPlaylistProvider = ({
2914
2941
  canZoomOut: zoom.canZoomOut,
2915
2942
  barWidth,
2916
2943
  barGap,
2944
+ roundedBars,
2917
2945
  progressBarWidth,
2918
2946
  isReady,
2919
2947
  mono,
@@ -2939,6 +2967,7 @@ var WaveformPlaylistProvider = ({
2939
2967
  zoom.canZoomOut,
2940
2968
  barWidth,
2941
2969
  barGap,
2970
+ roundedBars,
2942
2971
  progressBarWidth,
2943
2972
  isReady,
2944
2973
  mono,
@@ -3026,6 +3055,7 @@ var MediaElementPlaylistProvider = ({
3026
3055
  annotationList,
3027
3056
  barWidth = 1,
3028
3057
  barGap = 0,
3058
+ roundedBars = false,
3029
3059
  progressBarWidth: progressBarWidthProp,
3030
3060
  audioContext,
3031
3061
  onAnnotationsChange,
@@ -3348,6 +3378,7 @@ var MediaElementPlaylistProvider = ({
3348
3378
  controls,
3349
3379
  barWidth,
3350
3380
  barGap,
3381
+ roundedBars,
3351
3382
  progressBarWidth,
3352
3383
  fadeIn: track.fadeIn,
3353
3384
  fadeOut: track.fadeOut
@@ -3362,6 +3393,7 @@ var MediaElementPlaylistProvider = ({
3362
3393
  controls,
3363
3394
  barWidth,
3364
3395
  barGap,
3396
+ roundedBars,
3365
3397
  progressBarWidth,
3366
3398
  track.fadeIn,
3367
3399
  track.fadeOut
@@ -4015,7 +4047,7 @@ var PlaylistVisualization = ({
4015
4047
  onRemoveTrack,
4016
4048
  recordingState
4017
4049
  }) => {
4018
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
4050
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
4019
4051
  const theme = (0, import_ui_components8.useTheme)();
4020
4052
  const { isPlaying, getLookAhead, getOutputLatency } = usePlaybackAnimation();
4021
4053
  const {
@@ -4030,7 +4062,8 @@ var PlaylistVisualization = ({
4030
4062
  loopStart,
4031
4063
  loopEnd,
4032
4064
  isLoopEnabled,
4033
- indefinitePlayback
4065
+ indefinitePlayback,
4066
+ fillViewport
4034
4067
  } = usePlaylistState();
4035
4068
  const annotationIntegration = (0, import_react26.useContext)(AnnotationIntegrationContext);
4036
4069
  const {
@@ -4045,7 +4078,8 @@ var PlaylistVisualization = ({
4045
4078
  setScrollContainer,
4046
4079
  setSelectedTrackId,
4047
4080
  setCurrentTime,
4048
- setLoopRegion
4081
+ setLoopRegion,
4082
+ setRecordingActive
4049
4083
  } = usePlaylistControls();
4050
4084
  const {
4051
4085
  peaksDataArray,
@@ -4059,10 +4093,18 @@ var PlaylistVisualization = ({
4059
4093
  controls,
4060
4094
  barWidth,
4061
4095
  barGap,
4096
+ roundedBars,
4062
4097
  isReady,
4063
4098
  mono
4064
4099
  } = usePlaylistData();
4065
4100
  const spectrogram = (0, import_react26.useContext)(SpectrogramIntegrationContext);
4101
+ const recordingActive = !!(recordingState == null ? void 0 : recordingState.isRecording);
4102
+ const armedTrackId = (_a = recordingState == null ? void 0 : recordingState.trackId) != null ? _a : null;
4103
+ (0, import_react26.useEffect)(() => {
4104
+ if (!recordingActive) return void 0;
4105
+ setRecordingActive(true, armedTrackId);
4106
+ return () => setRecordingActive(false);
4107
+ }, [recordingActive, armedTrackId, setRecordingActive]);
4066
4108
  const perTrackSpectrogramHelpers = (0, import_react26.useMemo)(() => {
4067
4109
  if (!spectrogram)
4068
4110
  return /* @__PURE__ */ new Map();
@@ -4100,8 +4142,8 @@ var PlaylistVisualization = ({
4100
4142
  }, max);
4101
4143
  }, 0);
4102
4144
  let displayDuration = tracksMaxDuration > 0 ? tracksMaxDuration : duration > 0 ? duration : DEFAULT_EMPTY_TRACK_DURATION;
4103
- if (indefinitePlayback) {
4104
- const containerWidth = (_b = (_a = scrollContainerRef.current) == null ? void 0 : _a.clientWidth) != null ? _b : 0;
4145
+ if (indefinitePlayback || fillViewport) {
4146
+ const containerWidth = (_c = (_b = scrollContainerRef.current) == null ? void 0 : _b.clientWidth) != null ? _c : 0;
4105
4147
  const minContainerDuration = containerWidth * samplesPerPixel / sampleRate;
4106
4148
  displayDuration = Math.max(displayDuration, minContainerDuration);
4107
4149
  }
@@ -4317,7 +4359,8 @@ var PlaylistVisualization = ({
4317
4359
  duration: displayDuration * 1e3,
4318
4360
  controls,
4319
4361
  barWidth,
4320
- barGap
4362
+ barGap,
4363
+ roundedBars
4321
4364
  },
4322
4365
  children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4323
4366
  import_ui_components8.Playlist,
@@ -4558,8 +4601,8 @@ var PlaylistVisualization = ({
4558
4601
  {
4559
4602
  open: settingsModalTrackId !== null,
4560
4603
  onClose: () => setSettingsModalTrackId(null),
4561
- config: settingsModalTrackId !== null ? (_g = (_f = (_e = (_c = spectrogram.trackSpectrogramOverrides.get(settingsModalTrackId)) == null ? void 0 : _c.config) != null ? _e : (_d = tracks.find((t) => t.id === settingsModalTrackId)) == null ? void 0 : _d.spectrogramConfig) != null ? _f : spectrogram.spectrogramConfig) != null ? _g : {} : {},
4562
- colorMap: settingsModalTrackId !== null ? (_l = (_k = (_j = (_h = spectrogram.trackSpectrogramOverrides.get(settingsModalTrackId)) == null ? void 0 : _h.colorMap) != null ? _j : (_i = tracks.find((t) => t.id === settingsModalTrackId)) == null ? void 0 : _i.spectrogramColorMap) != null ? _k : spectrogram.spectrogramColorMap) != null ? _l : "viridis" : "viridis",
4604
+ config: settingsModalTrackId !== null ? (_h = (_g = (_f = (_d = spectrogram.trackSpectrogramOverrides.get(settingsModalTrackId)) == null ? void 0 : _d.config) != null ? _f : (_e = tracks.find((t) => t.id === settingsModalTrackId)) == null ? void 0 : _e.spectrogramConfig) != null ? _g : spectrogram.spectrogramConfig) != null ? _h : {} : {},
4605
+ colorMap: settingsModalTrackId !== null ? (_m = (_l = (_k = (_i = spectrogram.trackSpectrogramOverrides.get(settingsModalTrackId)) == null ? void 0 : _i.colorMap) != null ? _k : (_j = tracks.find((t) => t.id === settingsModalTrackId)) == null ? void 0 : _j.spectrogramColorMap) != null ? _l : spectrogram.spectrogramColorMap) != null ? _m : "viridis" : "viridis",
4563
4606
  onApply: (newConfig, newColorMap) => {
4564
4607
  if (settingsModalTrackId !== null) {
4565
4608
  spectrogram.setTrackSpectrogramConfig(settingsModalTrackId, newConfig, newColorMap);
@@ -4935,6 +4978,7 @@ var MediaElementPlaylist = ({
4935
4978
  playoutRef,
4936
4979
  barWidth,
4937
4980
  barGap,
4981
+ roundedBars,
4938
4982
  fadeIn,
4939
4983
  fadeOut
4940
4984
  } = useMediaElementData();
@@ -5046,7 +5090,8 @@ var MediaElementPlaylist = ({
5046
5090
  duration: duration * 1e3,
5047
5091
  controls,
5048
5092
  barWidth,
5049
- barGap
5093
+ barGap,
5094
+ roundedBars
5050
5095
  },
5051
5096
  children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5052
5097
  import_ui_components10.Playlist,