@djangocfg/ui-nextjs 2.1.82 → 2.1.83

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 (33) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
  3. package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
  4. package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
  5. package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
  6. package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
  7. package/src/tools/AudioPlayer/README.md +177 -205
  8. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
  9. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
  10. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
  11. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  12. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
  13. package/src/tools/AudioPlayer/components/index.ts +6 -1
  14. package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -3
  15. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
  16. package/src/tools/AudioPlayer/context/index.ts +14 -2
  17. package/src/tools/AudioPlayer/hooks/index.ts +11 -0
  18. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  19. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  20. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
  21. package/src/tools/AudioPlayer/index.ts +31 -0
  22. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
  23. package/src/tools/index.ts +22 -0
  24. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
  25. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
  26. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
  27. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
  28. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
  29. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
  30. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
  31. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
  32. package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
  33. package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
@@ -0,0 +1,373 @@
1
+ # Audio Crackling/Distortion Issue Diagnosis
2
+
3
+ ## Executive Summary
4
+
5
+ The audio crackling and distortion issues in the WaveSurfer-based AudioPlayer stem from **multiple Web Audio API routing conflicts** and **buffer management problems**. The SimpleAudioPlayer (using native HTMLAudioElement) works smoothly because it bypasses all these complexities.
6
+
7
+ ---
8
+
9
+ ## 1. Root Cause Analysis
10
+
11
+ ### 1.1 Double Audio Routing - CRITICAL ISSUE
12
+
13
+ **Location:** `useSharedWebAudio.ts` lines 44-46 + lines 80-82
14
+
15
+ ```typescript
16
+ // First connection (line 44-46):
17
+ sourceRef.current = audioContext.createMediaElementSource(audioElement);
18
+ sourceRef.current.connect(audioContext.destination); // Direct connection
19
+
20
+ // Second connection when creating analyser (line 80-82):
21
+ sourceRef.current.connect(analyser);
22
+ analyser.connect(audioContext.destination); // DUPLICATE path to destination!
23
+ ```
24
+
25
+ **Problem:** Audio signal is routed to `destination` twice:
26
+ 1. Source -> Destination (direct)
27
+ 2. Source -> Analyser -> Destination
28
+
29
+ This causes the audio to be **played twice with slight timing differences**, resulting in:
30
+ - Phase cancellation (certain frequencies cancel out)
31
+ - Crackling from doubled samples
32
+ - Distortion from summed amplitudes exceeding 0dB
33
+
34
+ ### 1.2 WaveSurfer's Dual Decoding Architecture
35
+
36
+ **Location:** `wavesurfer.ts` lines 502-566, `decoder.ts` lines 2-10
37
+
38
+ WaveSurfer performs audio decoding **twice**:
39
+
40
+ 1. **For waveform rendering** (lines 556-557):
41
+ ```typescript
42
+ const arrayBuffer = await blob.arrayBuffer();
43
+ this.decodedData = await Decoder.decode(arrayBuffer, this.options.sampleRate);
44
+ ```
45
+
46
+ 2. **For playback** via MediaElement:
47
+ ```typescript
48
+ this.setSrc(url, blob); // Browser decodes again for playback
49
+ ```
50
+
51
+ The waveform decoder uses a **low sample rate by default** (8000 Hz - see `defaultOptions.sampleRate`), while the browser uses the file's native sample rate for playback. This mismatch can cause visual/audio sync issues but is not the primary crackling cause.
52
+
53
+ ### 1.3 WebAudioPlayer Buffer Recreation on Seek
54
+
55
+ **Location:** `webaudio.ts` lines 96-128
56
+
57
+ ```typescript
58
+ private _play() {
59
+ // Clean up old buffer node
60
+ if (this.bufferNode) {
61
+ this.bufferNode.onended = null;
62
+ this.bufferNode.disconnect();
63
+ }
64
+
65
+ // Create NEW buffer source node every time
66
+ this.bufferNode = this.audioContext.createBufferSource();
67
+ this.bufferNode.buffer = this.buffer;
68
+ // ...
69
+ this.bufferNode.start(this.audioContext.currentTime, currentPos);
70
+ }
71
+ ```
72
+
73
+ **Problem:** `AudioBufferSourceNode` is a **one-shot node** - it can only be started once. WaveSurfer must recreate it on every play/seek, but:
74
+ - Rapid seeks cause rapid node creation/destruction
75
+ - No crossfade between old and new nodes = clicks at transition points
76
+ - `disconnect()` is immediate, not fade-out
77
+
78
+ ### 1.4 Prefetch Streaming Chunking
79
+
80
+ **Location:** `useAudioSource.ts` lines 97-114
81
+
82
+ ```typescript
83
+ // Stream the response for progress tracking
84
+ const reader = response.body.getReader();
85
+ const chunks: ArrayBuffer[] = [];
86
+
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done) break;
90
+ chunks.push(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
91
+ }
92
+
93
+ // Combine chunks into blob
94
+ const blob = new Blob(chunks, { type: 'audio/mpeg' });
95
+ ```
96
+
97
+ **Potential Issue:** If the stream is interrupted or network latency causes gaps, the resulting blob may have discontinuities. However, since the blob is fully assembled before loading, this is a minor concern compared to the routing issue.
98
+
99
+ ---
100
+
101
+ ## 2. Comparison: SimpleAudioPlayer vs WaveSurfer AudioPlayer
102
+
103
+ | Aspect | SimpleAudioPlayer | WaveSurfer AudioPlayer |
104
+ |--------|-------------------|------------------------|
105
+ | **Audio Element** | Native `<audio>` | Native `<audio>` (MediaElement backend) |
106
+ | **Web Audio API** | Not used | Used for analyser + SharedWebAudio |
107
+ | **Audio Routing** | Browser-native | Source -> multiple destinations |
108
+ | **Decoding** | Browser-native once | Browser + custom decoder |
109
+ | **Seeks** | Native seeking | Native + buffer recreation |
110
+ | **Analyser** | None | Connected via SharedWebAudio |
111
+
112
+ ### Why SimpleAudioPlayer Works
113
+
114
+ SimpleAudioPlayer in `SimpleAudioPlayer.tsx` **does use WaveSurfer** via the `AudioProvider`:
115
+
116
+ ```typescript
117
+ // SimpleAudioPlayer.tsx line 200-205
118
+ <AudioProvider
119
+ source={{ uri: src, prefetch }}
120
+ containerRef={containerRef}
121
+ autoPlay={autoPlay}
122
+ waveformOptions={waveformOptions}
123
+ >
124
+ ```
125
+
126
+ So the crackling issue **also affects SimpleAudioPlayer** when:
127
+ 1. `reactiveCover` is enabled (triggers `useAudioAnalysis`)
128
+ 2. Audio levels are being tracked
129
+
130
+ If SimpleAudioPlayer works smoothly for you, check if:
131
+ - `reactiveCover={false}` or `variant="none"`
132
+ - This disables `useAudioAnalysis` which creates the analyser node
133
+
134
+ ---
135
+
136
+ ## 3. Web Audio API Routing Diagram
137
+
138
+ ### Current (Problematic) Architecture
139
+
140
+ ```
141
+ +------------------+
142
+ | |
143
+ +-------------+ connect | destination | <-- DOUBLE SIGNAL!
144
+ | source | ------------> | (speakers) |
145
+ | (MediaElem) | | |
146
+ +-------------+ +------------------+
147
+ | ^
148
+ | |
149
+ | connect | connect
150
+ v |
151
+ +-------------+ |
152
+ | analyser | ---------------------+
153
+ +-------------+
154
+ ```
155
+
156
+ ### Correct Architecture
157
+
158
+ ```
159
+ +-------------+ +-------------+ +------------------+
160
+ | source | connect | analyser | connect | destination |
161
+ | (MediaElem) | ------------> | | ------------> | (speakers) |
162
+ +-------------+ +-------------+ +------------------+
163
+ ```
164
+
165
+ ---
166
+
167
+ ## 4. Code Fixes
168
+
169
+ ### Fix 1: Remove Direct Connection in useSharedWebAudio
170
+
171
+ **File:** `hooks/useSharedWebAudio.ts`
172
+
173
+ ```diff
174
+ const initAudio = () => {
175
+ try {
176
+ if (!audioContextRef.current) {
177
+ const AudioContextClass = window.AudioContext ||
178
+ (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
179
+ audioContextRef.current = new AudioContextClass();
180
+ }
181
+
182
+ const audioContext = audioContextRef.current;
183
+
184
+ if (connectedElementRef.current !== audioElement) {
185
+ if (sourceRef.current) {
186
+ try { sourceRef.current.disconnect(); } catch { /* ignore */ }
187
+ }
188
+
189
+ sourceRef.current = audioContext.createMediaElementSource(audioElement);
190
+ - // Connect directly to destination (analysers will be inserted in between)
191
+ - sourceRef.current.connect(audioContext.destination);
192
+ + // DON'T connect to destination here - let the analyser chain handle it
193
+ + // If no analysers are created, we need a passthrough connection
194
+ + if (analyserNodesRef.current.size === 0) {
195
+ + sourceRef.current.connect(audioContext.destination);
196
+ + }
197
+ connectedElementRef.current = audioElement;
198
+ }
199
+ } catch (error) {
200
+ console.warn('[SharedWebAudio] Could not initialize:', error);
201
+ }
202
+ };
203
+ ```
204
+
205
+ ### Fix 2: Proper Analyser Connection (No Duplicate Path)
206
+
207
+ **File:** `hooks/useSharedWebAudio.ts`
208
+
209
+ ```diff
210
+ const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
211
+ if (!audioContextRef.current || !sourceRef.current) return null;
212
+
213
+ try {
214
+ const analyser = audioContextRef.current.createAnalyser();
215
+ analyser.fftSize = options?.fftSize ?? 256;
216
+ analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
217
+
218
+ - // Connect: source -> analyser -> destination
219
+ - sourceRef.current.connect(analyser);
220
+ - analyser.connect(audioContextRef.current.destination);
221
+ + // First analyser: disconnect source from destination and insert analyser
222
+ + if (analyserNodesRef.current.size === 0) {
223
+ + try { sourceRef.current.disconnect(audioContextRef.current.destination); } catch { /* not connected */ }
224
+ + }
225
+ +
226
+ + // Connect analyser in series (last analyser -> new analyser -> destination)
227
+ + // For simplicity, connect in parallel but only ONE path to destination
228
+ + sourceRef.current.connect(analyser);
229
+ +
230
+ + // Only the first analyser connects to destination
231
+ + if (analyserNodesRef.current.size === 0) {
232
+ + analyser.connect(audioContextRef.current.destination);
233
+ + }
234
+
235
+ analyserNodesRef.current.add(analyser);
236
+ return analyser;
237
+ } catch (error) {
238
+ console.warn('[SharedWebAudio] Could not create analyser:', error);
239
+ return null;
240
+ }
241
+ }, []);
242
+ ```
243
+
244
+ ### Fix 3: Alternative - Use AnalyserNode Without Routing to Destination
245
+
246
+ For analysis-only (no audio modification), analysers don't need to be in the audio path:
247
+
248
+ ```typescript
249
+ const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
250
+ if (!audioContextRef.current || !sourceRef.current) return null;
251
+
252
+ try {
253
+ const analyser = audioContextRef.current.createAnalyser();
254
+ analyser.fftSize = options?.fftSize ?? 256;
255
+ analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
256
+
257
+ // Connect analyser as a "listener" - doesn't need to output to destination
258
+ sourceRef.current.connect(analyser);
259
+ // analyser.connect(destination) - NOT NEEDED for getByteFrequencyData()
260
+
261
+ analyserNodesRef.current.add(analyser);
262
+ return analyser;
263
+ } catch (error) {
264
+ console.warn('[SharedWebAudio] Could not create analyser:', error);
265
+ return null;
266
+ }
267
+ }, []);
268
+ ```
269
+
270
+ **This is the cleanest fix** - the analyser only needs to be connected to the source to read frequency data. It doesn't need to output anywhere.
271
+
272
+ ---
273
+
274
+ ## 5. Additional Recommendations
275
+
276
+ ### 5.1 Add Crossfade for WebAudioPlayer Seeks
277
+
278
+ For smoother seeking when using WebAudio backend:
279
+
280
+ ```typescript
281
+ // In webaudio.ts _play() method
282
+ private _play() {
283
+ if (!this.paused) return;
284
+ this.paused = false;
285
+
286
+ const audioContext = this.audioContext;
287
+
288
+ // Crossfade out old node
289
+ if (this.bufferNode && this.gainNode) {
290
+ const oldGain = this.gainNode.gain.value;
291
+ this.gainNode.gain.setValueAtTime(oldGain, audioContext.currentTime);
292
+ this.gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.01);
293
+ setTimeout(() => {
294
+ this.bufferNode?.disconnect();
295
+ }, 15);
296
+ }
297
+
298
+ // Create new node with fade-in
299
+ this.bufferNode = audioContext.createBufferSource();
300
+ // ... rest of setup
301
+
302
+ this.gainNode.gain.setValueAtTime(0, audioContext.currentTime);
303
+ this.gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 0.01);
304
+ }
305
+ ```
306
+
307
+ ### 5.2 Consider MediaElement Backend Only
308
+
309
+ If waveform visualization isn't critical, avoid WebAudio backend entirely:
310
+
311
+ ```typescript
312
+ // In AudioProvider options, ensure backend is MediaElement (default)
313
+ const options = useMemo(() => ({
314
+ // ...
315
+ backend: 'MediaElement', // Not 'WebAudio'
316
+ }), []);
317
+ ```
318
+
319
+ ### 5.3 Debounce Rapid Seeks
320
+
321
+ Add debouncing for seek operations to prevent rapid buffer recreation:
322
+
323
+ ```typescript
324
+ const seek = useCallback(
325
+ debounce((time: number) => {
326
+ if (wavesurfer) {
327
+ wavesurfer.setTime(Math.max(0, Math.min(time, duration)));
328
+ }
329
+ }, 50),
330
+ [wavesurfer, duration]
331
+ );
332
+ ```
333
+
334
+ ---
335
+
336
+ ## 6. Testing Checklist
337
+
338
+ After applying fixes, test for:
339
+
340
+ - [ ] No crackling during normal playback
341
+ - [ ] No crackling when seeking
342
+ - [ ] No crackling when pausing/resuming
343
+ - [ ] Audio analysis (reactive effects) still work
344
+ - [ ] Waveform visualization still works
345
+ - [ ] Volume control doesn't cause distortion
346
+ - [ ] Multiple AudioPlayer instances don't interfere
347
+
348
+ ---
349
+
350
+ ## 7. Quick Diagnostic
351
+
352
+ To quickly verify the double-routing issue, add this debug code:
353
+
354
+ ```typescript
355
+ // In useSharedWebAudio.ts after creating source
356
+ console.log('[DEBUG] Source node connections:', sourceRef.current?.numberOfOutputs);
357
+ console.log('[DEBUG] Destination inputs:', audioContextRef.current?.destination.numberOfInputs);
358
+ ```
359
+
360
+ If `numberOfOutputs > 1` after creating an analyser, the double-routing is confirmed.
361
+
362
+ ---
363
+
364
+ ## Summary
365
+
366
+ | Issue | Severity | Fix Complexity |
367
+ |-------|----------|----------------|
368
+ | Double audio routing | **CRITICAL** | Low - Remove duplicate connect() |
369
+ | WebAudioPlayer buffer recreation | Medium | Medium - Add crossfade |
370
+ | Prefetch chunking | Low | N/A - Not primary cause |
371
+ | Dual decoding | Low | N/A - Different purposes |
372
+
373
+ **Recommended action:** Apply Fix 3 (analyser without destination routing) first - it's the safest change with the biggest impact.