@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.
- package/package.json +4 -4
- package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
- package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
- package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
- package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
- package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
- package/src/tools/AudioPlayer/README.md +177 -205
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
- package/src/tools/AudioPlayer/components/index.ts +6 -1
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -3
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
- package/src/tools/AudioPlayer/context/index.ts +14 -2
- package/src/tools/AudioPlayer/hooks/index.ts +11 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
- package/src/tools/AudioPlayer/index.ts +31 -0
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
- package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
- 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.
|