@spatialwalk/avatarkit 1.0.0-beta.22 → 1.0.0-beta.23
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/CHANGELOG.md +5 -0
- package/README.md +33 -173
- package/dist/{StreamingAudioPlayer-DjPD7f6u.js → StreamingAudioPlayer-PkzxBP93.js} +2 -2
- package/dist/{StreamingAudioPlayer-DjPD7f6u.js.map → StreamingAudioPlayer-PkzxBP93.js.map} +1 -1
- package/dist/core/AvatarController.d.ts +0 -11
- package/dist/core/AvatarController.d.ts.map +1 -1
- package/dist/{index-BB9yoGY2.js → index-DYf1u8L7.js} +4 -3
- package/dist/index-DYf1u8L7.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/index-BB9yoGY2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.0-beta.23] - 2025-01-26
|
|
9
|
+
|
|
10
|
+
### 🔧 API Changes
|
|
11
|
+
- **Breaking Change** - `playback()` method is no longer supported and has been removed from public API
|
|
12
|
+
|
|
8
13
|
## [1.0.0-beta.22] - 2025-01-26
|
|
9
14
|
|
|
10
15
|
### 🔧 API Changes
|
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
AvatarManager,
|
|
29
29
|
AvatarView,
|
|
30
30
|
Configuration,
|
|
31
|
-
Environment
|
|
31
|
+
Environment,
|
|
32
|
+
DrivingServiceMode
|
|
32
33
|
} from '@spatialwalk/avatarkit'
|
|
33
34
|
|
|
34
35
|
// 1. Initialize SDK
|
|
@@ -61,12 +62,9 @@ const avatarView = new AvatarView(avatar, container)
|
|
|
61
62
|
// 4. Start real-time communication (SDK mode only)
|
|
62
63
|
await avatarView.avatarController.start()
|
|
63
64
|
|
|
64
|
-
// 5. Send audio data (SDK mode)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const audioUint8 = new Uint8Array(1024) // Example: 16kHz PCM16 audio data (512 samples = 1024 bytes)
|
|
68
|
-
const audioData = audioUint8.slice().buffer // Simplified conversion, works for ArrayBuffer and SharedArrayBuffer
|
|
69
|
-
avatarView.avatarController.send(audioData, false) // Send audio data, will automatically start playing after accumulating enough data
|
|
65
|
+
// 5. Send audio data (SDK mode, must be 16kHz mono PCM16 format)
|
|
66
|
+
const audioData = new ArrayBuffer(1024) // Example: 16kHz PCM16 audio data
|
|
67
|
+
avatarView.avatarController.send(audioData, false) // Send audio data
|
|
70
68
|
avatarView.avatarController.send(audioData, true) // end=true marks the end of current conversation round
|
|
71
69
|
```
|
|
72
70
|
|
|
@@ -81,22 +79,9 @@ const container = document.getElementById('avatar-container')
|
|
|
81
79
|
const avatarView = new AvatarView(avatar, container)
|
|
82
80
|
|
|
83
81
|
// 4. Host Mode Workflow:
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Option A: Playback existing audio and animation data (replay mode)
|
|
89
|
-
const initialAudioChunks = [{ data: audioData1, isLast: false }, { data: audioData2, isLast: false }]
|
|
90
|
-
const initialKeyframes = animationData1 // Animation keyframes from your service
|
|
91
|
-
// Step 1: Send audio first to get conversationId
|
|
92
|
-
const conversationId = await avatarView.avatarController.playback(initialAudioChunks, initialKeyframes)
|
|
93
|
-
|
|
94
|
-
// Option B: Stream new audio and animation data (start a new session directly)
|
|
95
|
-
// Step 1: Send audio data first to get conversationId (automatically generates conversationId if starting new session)
|
|
96
|
-
const currentConversationId = avatarView.avatarController.yieldAudioData(audioData3, false)
|
|
97
|
-
// Step 2: Use the conversationId to send animation data (mismatched conversationId will be discarded)
|
|
98
|
-
avatarView.avatarController.yieldFramesData(animationData2, currentConversationId || conversationId)
|
|
99
|
-
// Note: To start playback, you need to call playback() with the accumulated data, or ensure enough audio data is sent
|
|
82
|
+
// Send audio data first to get conversationId, then use it to send animation data
|
|
83
|
+
const conversationId = avatarView.avatarController.yieldAudioData(audioData, false)
|
|
84
|
+
avatarView.avatarController.yieldFramesData(animationData, conversationId)
|
|
100
85
|
```
|
|
101
86
|
|
|
102
87
|
### Complete Examples
|
|
@@ -184,15 +169,9 @@ RenderSystem → WebGPU/WebGL → Canvas rendering
|
|
|
184
169
|
```
|
|
185
170
|
External data source (audio + animation)
|
|
186
171
|
↓
|
|
187
|
-
Step 1: Send audio data FIRST to get conversationId
|
|
188
|
-
↓
|
|
189
|
-
AvatarController.playback(initialAudio, initialKeyframes) // Returns conversationId
|
|
190
|
-
OR
|
|
191
172
|
AvatarController.yieldAudioData(audioChunk) // Returns conversationId
|
|
192
173
|
↓
|
|
193
|
-
|
|
194
|
-
↓
|
|
195
|
-
AvatarController.yieldFramesData(keyframes, conversationId) // Requires conversationId
|
|
174
|
+
AvatarController.yieldFramesData(keyframes, conversationId)
|
|
196
175
|
↓
|
|
197
176
|
AvatarController → AnimationPlayer (synchronized playback)
|
|
198
177
|
↓
|
|
@@ -203,10 +182,6 @@ AvatarController (playback loop) → AvatarView.renderRealtimeFrame()
|
|
|
203
182
|
RenderSystem → WebGPU/WebGL → Canvas rendering
|
|
204
183
|
```
|
|
205
184
|
|
|
206
|
-
**Note:**
|
|
207
|
-
- In SDK mode, users provide audio data, SDK handles network communication and animation data reception
|
|
208
|
-
- In Host mode, users provide both audio and animation data, SDK handles synchronized playback only
|
|
209
|
-
|
|
210
185
|
### Audio Format Requirements
|
|
211
186
|
|
|
212
187
|
**⚠️ Important:** The SDK requires audio data to be in **16kHz mono PCM16** format:
|
|
@@ -286,14 +261,25 @@ manager.clearCache()
|
|
|
286
261
|
|
|
287
262
|
3D rendering view (rendering layer), responsible for 3D rendering only. Internally automatically creates and manages `AvatarController`.
|
|
288
263
|
|
|
289
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
constructor(avatar: Avatar, container: HTMLElement)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Parameters:**
|
|
269
|
+
- `avatar`: Avatar 实例
|
|
270
|
+
- `container`: Canvas 容器元素(必选)
|
|
271
|
+
- Canvas 自动使用容器的完整尺寸(宽度和高度)
|
|
272
|
+
- Canvas 宽高比适应容器尺寸 - 设置容器尺寸以控制宽高比
|
|
273
|
+
- Canvas 会自动添加到容器中
|
|
274
|
+
- SDK automatically handles resize events via ResizeObserver
|
|
275
|
+
|
|
276
|
+
**Playback Mode:**
|
|
277
|
+
- The playback mode is determined by `drivingServiceMode` in `AvatarKit.initialize()` configuration
|
|
290
278
|
- The playback mode is fixed when creating `AvatarView` and persists throughout its lifecycle
|
|
291
279
|
- Cannot be changed after creation
|
|
292
280
|
|
|
293
281
|
```typescript
|
|
294
|
-
|
|
295
282
|
// Create view (Canvas is automatically added to container)
|
|
296
|
-
// Create view (playback mode is determined by drivingServiceMode in AvatarKit configuration)
|
|
297
283
|
const container = document.getElementById('avatar-container')
|
|
298
284
|
const avatarView = new AvatarView(avatar, container)
|
|
299
285
|
|
|
@@ -319,7 +305,7 @@ const newAvatar = await avatarManager.load('new-character-id')
|
|
|
319
305
|
currentAvatarView = new AvatarView(newAvatar, container)
|
|
320
306
|
|
|
321
307
|
// SDK mode: start connection (will throw error if not in SDK mode)
|
|
322
|
-
await currentAvatarView.controller.start()
|
|
308
|
+
await currentAvatarView.controller.start()
|
|
323
309
|
```
|
|
324
310
|
|
|
325
311
|
### AvatarController
|
|
@@ -334,14 +320,9 @@ Audio/animation playback controller (playback layer), manages synchronized playb
|
|
|
334
320
|
// Start WebSocket service
|
|
335
321
|
await avatarView.avatarController.start()
|
|
336
322
|
|
|
337
|
-
// Send audio data
|
|
323
|
+
// Send audio data (must be 16kHz mono PCM16 format)
|
|
338
324
|
const conversationId = avatarView.avatarController.send(audioData: ArrayBuffer, end: boolean)
|
|
339
|
-
// Returns: conversationId - Conversation ID for this conversation session
|
|
340
|
-
// audioData: Audio data (ArrayBuffer format, must be 16kHz mono PCM16)
|
|
341
|
-
// - Sample rate: 16kHz (16000 Hz) - backend requirement
|
|
342
|
-
// - Format: PCM16 (16-bit signed integer, little-endian)
|
|
343
|
-
// - Channels: Mono (single channel)
|
|
344
|
-
// - Example: 1 second = 16000 samples × 2 bytes = 32000 bytes
|
|
325
|
+
// Returns: conversationId - Conversation ID for this conversation session
|
|
345
326
|
// end: false (default) - Continue sending audio data for current conversation
|
|
346
327
|
// end: true - Mark the end of current conversation round. After end=true, sending new audio data will interrupt any ongoing playback from the previous conversation round
|
|
347
328
|
|
|
@@ -352,25 +333,17 @@ avatarView.avatarController.close()
|
|
|
352
333
|
#### Host Mode Methods
|
|
353
334
|
|
|
354
335
|
```typescript
|
|
355
|
-
//
|
|
356
|
-
const conversationId = await avatarView.avatarController.playback(
|
|
357
|
-
initialAudioChunks?: Array<{ data: Uint8Array, isLast: boolean }>, // Existing audio chunks (16kHz mono PCM16)
|
|
358
|
-
initialKeyframes?: any[] // Existing animation keyframes (obtained from your service)
|
|
359
|
-
)
|
|
360
|
-
// Returns: conversationId - New conversation ID for this conversation session
|
|
361
|
-
|
|
362
|
-
// Stream audio chunks (can be called directly to start a new session, or after playback() to add more data)
|
|
336
|
+
// Stream audio chunks (must be 16kHz mono PCM16 format)
|
|
363
337
|
const conversationId = avatarView.avatarController.yieldAudioData(
|
|
364
338
|
data: Uint8Array, // Audio chunk data
|
|
365
339
|
isLast: boolean = false // Whether this is the last chunk
|
|
366
340
|
)
|
|
367
341
|
// Returns: conversationId - Conversation ID for this audio session
|
|
368
|
-
// Note: If no conversationId exists, a new one will be automatically generated
|
|
369
342
|
|
|
370
343
|
// Stream animation keyframes (requires conversationId from audio data)
|
|
371
344
|
avatarView.avatarController.yieldFramesData(
|
|
372
345
|
keyframes: any[], // Animation keyframes (obtained from your service)
|
|
373
|
-
conversationId: string
|
|
346
|
+
conversationId: string // Conversation ID (required)
|
|
374
347
|
)
|
|
375
348
|
```
|
|
376
349
|
|
|
@@ -378,36 +351,14 @@ avatarView.avatarController.yieldFramesData(
|
|
|
378
351
|
|
|
379
352
|
**SDK Mode:**
|
|
380
353
|
- `send()` returns a conversationId to distinguish each conversation round
|
|
381
|
-
- `end=true` marks the end of a conversation round
|
|
354
|
+
- `end=true` marks the end of a conversation round
|
|
382
355
|
|
|
383
356
|
**Host Mode:**
|
|
384
|
-
|
|
385
|
-
1. **First send audio data** to get a conversationId (used to distinguish each conversation round):
|
|
386
|
-
- `playback()` returns a conversationId when playback existing audio and animation data (replay mode)
|
|
387
|
-
- `yieldAudioData()` returns a conversationId for streaming new audio data
|
|
388
|
-
2. **Then use that conversationId** to send animation data:
|
|
357
|
+
- `yieldAudioData()` returns a conversationId (automatically generates if starting new session)
|
|
389
358
|
- `yieldFramesData()` requires a valid conversationId parameter
|
|
390
359
|
- Animation data with mismatched conversationId will be **discarded**
|
|
391
360
|
- Use `getCurrentConversationId()` to retrieve the current active conversationId
|
|
392
361
|
|
|
393
|
-
**Example Flow (Host Mode):**
|
|
394
|
-
```typescript
|
|
395
|
-
// Option A: Playback existing complete data (replay mode)
|
|
396
|
-
const conversationId = await avatarView.avatarController.playback(initialAudioChunks, initialKeyframes)
|
|
397
|
-
|
|
398
|
-
// Option B: Start streaming new data directly
|
|
399
|
-
// Step 1: Send audio data first to get conversationId (automatically generates if starting new session)
|
|
400
|
-
const conversationId = avatarView.avatarController.yieldAudioData(audioChunk, false)
|
|
401
|
-
// Step 2: Use the conversationId to send animation data
|
|
402
|
-
avatarView.avatarController.yieldFramesData(keyframes, conversationId)
|
|
403
|
-
// Note: To start playback with Option B, call playback() with accumulated data or ensure enough audio is sent
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**Why conversationId is required:**
|
|
407
|
-
- Ensures audio and animation data belong to the same conversation session
|
|
408
|
-
- Prevents data from different sessions from being mixed
|
|
409
|
-
- Automatically discards mismatched animation data for data integrity
|
|
410
|
-
|
|
411
362
|
#### Common Methods (Both Modes)
|
|
412
363
|
|
|
413
364
|
```typescript
|
|
@@ -439,11 +390,9 @@ avatarView.avatarController.onError = (error: Error) => {}
|
|
|
439
390
|
|
|
440
391
|
**Important Notes:**
|
|
441
392
|
- `start()` and `close()` are only available in SDK mode
|
|
442
|
-
- `
|
|
393
|
+
- `yieldAudioData()` and `yieldFramesData()` are only available in Host mode
|
|
443
394
|
- `pause()`, `resume()`, `interrupt()`, `clear()`, `getCurrentConversationId()`, `setVolume()`, and `getVolume()` are available in both modes
|
|
444
395
|
- The playback mode is determined when creating `AvatarView` and cannot be changed
|
|
445
|
-
- **Conversation ID**: In Host mode, always send audio data first to obtain a conversationId, then use that conversationId when sending animation data. Animation data with mismatched conversationId will be discarded. Use `getCurrentConversationId()` to retrieve the current active conversationId.
|
|
446
|
-
- **Volume Control**: Use `setVolume(volume)` to control audio playback volume (0.0 to 1.0). This only affects the avatar's audio player, not system volume. Volume changes take effect immediately, including for currently playing audio.
|
|
447
396
|
|
|
448
397
|
## 🔧 Configuration
|
|
449
398
|
|
|
@@ -471,29 +420,6 @@ enum Environment {
|
|
|
471
420
|
}
|
|
472
421
|
```
|
|
473
422
|
|
|
474
|
-
### AvatarView Constructor
|
|
475
|
-
|
|
476
|
-
```typescript
|
|
477
|
-
constructor(avatar: Avatar, container: HTMLElement)
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Parameters:**
|
|
481
|
-
- `avatar`: Avatar 实例
|
|
482
|
-
- `container`: Canvas 容器元素(必选)
|
|
483
|
-
- Canvas 自动使用容器的完整尺寸(宽度和高度)
|
|
484
|
-
- Canvas 宽高比适应容器尺寸 - 设置容器尺寸以控制宽高比
|
|
485
|
-
- Canvas 会自动添加到容器中
|
|
486
|
-
|
|
487
|
-
**Note:** 播放模式由 `AvatarKit.initialize()` 配置中的 `drivingServiceMode` 决定,而不是在构造函数参数中
|
|
488
|
-
- SDK automatically handles resize events via ResizeObserver
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
enum DrivingServiceMode {
|
|
492
|
-
sdk = 'sdk', // SDK mode: SDK handles WebSocket communication
|
|
493
|
-
host = 'host' // Host mode: Host provides data, SDK handles playback
|
|
494
|
-
}
|
|
495
|
-
```
|
|
496
|
-
|
|
497
423
|
### CameraConfig
|
|
498
424
|
|
|
499
425
|
```typescript
|
|
@@ -604,13 +530,8 @@ const container = document.getElementById('avatar-container')
|
|
|
604
530
|
const avatarView = new AvatarView(avatar, container)
|
|
605
531
|
|
|
606
532
|
// Use
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
const conversationId = await avatarView.avatarController.playback(initialAudioChunks, initialKeyframes)
|
|
610
|
-
// Step 2: Stream additional audio (returns conversationId)
|
|
611
|
-
const currentConversationId = avatarView.avatarController.yieldAudioData(audioChunk, false)
|
|
612
|
-
// Step 3: Use conversationId to send animation data (mismatched conversationId will be discarded)
|
|
613
|
-
avatarView.avatarController.yieldFramesData(keyframes, currentConversationId || conversationId)
|
|
533
|
+
const conversationId = avatarView.avatarController.yieldAudioData(audioChunk, false)
|
|
534
|
+
avatarView.avatarController.yieldFramesData(keyframes, conversationId)
|
|
614
535
|
|
|
615
536
|
// Cleanup
|
|
616
537
|
avatarView.avatarController.clear() // Clear all data and resources
|
|
@@ -629,67 +550,6 @@ avatarView.dispose() // Automatically cleans up all resources
|
|
|
629
550
|
- Supports dynamic loading/unloading of character and animation resources
|
|
630
551
|
- Provides memory usage monitoring interface
|
|
631
552
|
|
|
632
|
-
### Audio Data Sending
|
|
633
|
-
|
|
634
|
-
#### SDK Mode
|
|
635
|
-
|
|
636
|
-
The `send()` method receives audio data in `ArrayBuffer` format:
|
|
637
|
-
|
|
638
|
-
**Audio Format Requirements:**
|
|
639
|
-
- **Sample Rate**: 16kHz (16000 Hz) - **Backend requirement, must be exactly 16kHz**
|
|
640
|
-
- **Format**: PCM16 (16-bit signed integer, little-endian)
|
|
641
|
-
- **Channels**: Mono (single channel)
|
|
642
|
-
- **Data Size**: Each sample is 2 bytes, so 1 second of audio = 16000 samples × 2 bytes = 32000 bytes
|
|
643
|
-
|
|
644
|
-
**Usage:**
|
|
645
|
-
- `audioData`: Audio data (ArrayBuffer format, must be 16kHz mono PCM16)
|
|
646
|
-
- `end=false` (default) - Continue sending audio data for current conversation
|
|
647
|
-
- `end=true` - Mark the end of current conversation round. After `end=true`, sending new audio data will interrupt any ongoing playback from the previous conversation round
|
|
648
|
-
- **Important**: No need to wait for `end=true` to start playing, it will automatically start playing after accumulating enough audio data
|
|
649
|
-
|
|
650
|
-
#### Host Mode
|
|
651
|
-
|
|
652
|
-
The `playback()` method is used to playback existing audio and animation data (replay mode), generating a new conversationId and interrupting any existing conversation.
|
|
653
|
-
|
|
654
|
-
**Two ways to start a session in Host mode:**
|
|
655
|
-
1. **Use `playback()`** - For replaying existing complete audio and animation data
|
|
656
|
-
2. **Use `yieldAudioData()` directly** - For streaming new audio data (automatically generates conversationId if needed)
|
|
657
|
-
|
|
658
|
-
Then use `yieldAudioData()` to stream additional audio:
|
|
659
|
-
|
|
660
|
-
**Audio Format Requirements:**
|
|
661
|
-
- Same as SDK mode: 16kHz mono PCM16 format
|
|
662
|
-
- Audio data should be provided as `Uint8Array` in chunks with `isLast` flag
|
|
663
|
-
|
|
664
|
-
**Usage:**
|
|
665
|
-
```typescript
|
|
666
|
-
// Playback existing audio and animation data (starts a new conversation)
|
|
667
|
-
// Note: Audio and animation data should be obtained from your backend service
|
|
668
|
-
const initialAudioChunks = [
|
|
669
|
-
{ data: audioData1, isLast: false },
|
|
670
|
-
{ data: audioData2, isLast: false }
|
|
671
|
-
]
|
|
672
|
-
const conversationId = await avatarController.playback(initialAudioChunks, initialKeyframes)
|
|
673
|
-
// Returns: conversationId - New conversation ID for this conversation session
|
|
674
|
-
|
|
675
|
-
// Stream additional audio chunks
|
|
676
|
-
const conversationId = avatarController.yieldAudioData(audioChunk, isLast)
|
|
677
|
-
// Returns: conversationId - Conversation ID for this audio session
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
**⚠️ Conversation ID Workflow:**
|
|
681
|
-
1. **Start a session** → Choose one of two ways:
|
|
682
|
-
- **Option A**: Use `playback(initialAudioChunks, initialKeyframes)` to replay existing complete data
|
|
683
|
-
- **Option B**: Use `yieldAudioData(audioChunk)` directly to start streaming (automatically generates conversationId)
|
|
684
|
-
2. **Get conversationId** → Both methods return a conversationId
|
|
685
|
-
3. **Send animation with conversationId** → Use the conversationId from step 1 in `yieldFramesData()`
|
|
686
|
-
4. **Data matching** → Only animation data with matching conversationId will be accepted
|
|
687
|
-
|
|
688
|
-
**Resampling (Both Modes):**
|
|
689
|
-
- If your audio source is at a different sample rate (e.g., 24kHz, 48kHz), you **must** resample it to 16kHz before sending
|
|
690
|
-
- For high-quality resampling, use Web Audio API's `OfflineAudioContext` with anti-aliasing filtering
|
|
691
|
-
- See example projects (`vanilla`, `react`, `vue`) for complete resampling implementation
|
|
692
|
-
|
|
693
553
|
## 🌐 Browser Compatibility
|
|
694
554
|
|
|
695
555
|
- **Chrome/Edge** 90+ (WebGPU recommended)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var C = Object.defineProperty;
|
|
2
2
|
var g = (h, t, e) => t in h ? C(h, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : h[t] = e;
|
|
3
3
|
var s = (h, t, e) => g(h, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
-
import { A as m, e as f, a as c, l as n } from "./index-
|
|
4
|
+
import { A as m, e as f, a as c, l as n } from "./index-DYf1u8L7.js";
|
|
5
5
|
class y {
|
|
6
6
|
constructor(t) {
|
|
7
7
|
// AudioContext is managed internally
|
|
@@ -350,4 +350,4 @@ class y {
|
|
|
350
350
|
export {
|
|
351
351
|
y as StreamingAudioPlayer
|
|
352
352
|
};
|
|
353
|
-
//# sourceMappingURL=StreamingAudioPlayer-
|
|
353
|
+
//# sourceMappingURL=StreamingAudioPlayer-PkzxBP93.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamingAudioPlayer-DjPD7f6u.js","sources":["../audio/StreamingAudioPlayer.ts"],"sourcesContent":["/**\n * Streaming Audio Player\n * Implements real-time audio playback using Web Audio API\n * Supports dynamic PCM chunk addition without Workers\n */\n\nimport { APP_CONFIG } from '../config/app-config'\nimport { AvatarKit } from '../core/AvatarKit'\nimport { errorToMessage } from '../utils/error-utils'\nimport { logger } from '../utils/logger'\n\nexport interface StreamingAudioPlayerOptions {\n sampleRate?: number // PCM sample rate (default: APP_CONFIG.audio.sampleRate, backend requires 16kHz)\n channelCount?: number // Number of channels (default: 1)\n debug?: boolean\n}\n\nexport class StreamingAudioPlayer {\n // AudioContext is managed internally\n private audioContext: AudioContext | null = null\n private sampleRate: number\n private channelCount: number\n private debug: boolean\n\n // Session-level state\n private sessionId: string\n private sessionStartTime = 0 // AudioContext time when session started\n private pausedTimeOffset = 0 // Accumulated paused time\n private pausedAt = 0 // Time when paused\n private pausedAudioContextTime = 0 // audioContext.currentTime when paused (for resume calculation)\n private scheduledTime = 0 // Next chunk schedule time in AudioContext time\n\n // Playback state\n private isPlaying = false\n private isPaused = false\n private autoStartEnabled = true // Control whether to auto-start when buffer is ready\n\n // Audio buffer queue\n private audioChunks: Array<{ data: Uint8Array, isLast: boolean }> = []\n private scheduledChunks = 0 // Number of chunks already scheduled\n private activeSources = new Set<AudioBufferSourceNode>()\n\n // Volume control\n private gainNode: GainNode | null = null\n private volume: number = 1.0 // Default volume 1.0 (0.0 - 1.0)\n\n // Event callbacks\n private onEndedCallback?: () => void\n\n constructor(options?: StreamingAudioPlayerOptions) {\n this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n this.sampleRate = options?.sampleRate ?? APP_CONFIG.audio.sampleRate\n this.channelCount = options?.channelCount ?? 1\n this.debug = options?.debug ?? false\n }\n\n /**\n * Initialize audio context (create and ensure it's ready)\n */\n async initialize(): Promise<void> {\n if (this.audioContext) {\n return\n }\n\n try {\n // Create AudioContext\n this.audioContext = new AudioContext({\n sampleRate: this.sampleRate,\n })\n\n // Create GainNode for volume control\n this.gainNode = this.audioContext.createGain()\n this.gainNode.gain.value = this.volume\n this.gainNode.connect(this.audioContext.destination)\n\n // Resume context (required for some browsers)\n if (this.audioContext.state === 'suspended') {\n await this.audioContext.resume()\n }\n\n this.log('AudioContext initialized', {\n sessionId: this.sessionId,\n sampleRate: this.audioContext.sampleRate,\n state: this.audioContext.state,\n })\n }\n catch (error) {\n const message = errorToMessage(error)\n AvatarKit.logEvent('activeAudioSessionFailed', 'warning', {\n sessionId: this.sessionId,\n reason: message,\n })\n logger.error('Failed to initialize AudioContext:', message)\n throw error instanceof Error ? error : new Error(message)\n }\n }\n\n /**\n * Add audio chunk (16-bit PCM)\n */\n addChunk(pcmData: Uint8Array, isLast: boolean = false): void {\n if (!this.audioContext) {\n logger.error('AudioContext not initialized')\n return\n }\n\n // Store chunk with metadata\n this.audioChunks.push({ data: pcmData, isLast })\n\n // Track buffer underrun warning\n if (this.isPlaying && this.audioChunks.length === this.scheduledChunks) {\n // Buffer underrun detected - chunks consumed faster than added\n }\n\n this.log(`Added chunk ${this.audioChunks.length}`, {\n size: pcmData.length,\n totalChunks: this.audioChunks.length,\n isLast,\n isPlaying: this.isPlaying,\n scheduledChunks: this.scheduledChunks,\n })\n\n // Auto-start if we have any audio chunks and auto-start is enabled\n if (!this.isPlaying && this.autoStartEnabled && this.audioChunks.length > 0) {\n this.log('[StreamingAudioPlayer] Auto-starting playback from addChunk')\n this.startPlayback()\n }\n // Schedule next chunk if already playing and not paused\n else if (this.isPlaying && !this.isPaused) {\n this.log('[StreamingAudioPlayer] Already playing, scheduling next chunk')\n this.scheduleNextChunk()\n } else {\n this.log('[StreamingAudioPlayer] Not playing and no chunks, waiting for more chunks')\n }\n }\n\n /**\n * Start new session (stop current and start fresh)\n */\n async startNewSession(audioChunks: Array<{ data: Uint8Array, isLast: boolean }>): Promise<void> {\n // Stop current session if playing\n this.stop()\n\n // Generate new session ID to prevent data mixing\n this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n\n // Reset session state\n this.audioChunks = []\n this.scheduledChunks = 0\n this.pausedTimeOffset = 0\n this.pausedAt = 0\n this.pausedAudioContextTime = 0\n // Don't set sessionStartTime or scheduledTime here - let startPlayback() set them\n this.log('Starting new session', {\n chunks: audioChunks.length,\n })\n\n // Add audio chunks with their metadata\n for (const chunk of audioChunks) {\n this.addChunk(chunk.data, chunk.isLast)\n }\n }\n\n /**\n * Start playback\n */\n private startPlayback(): void {\n if (!this.audioContext) {\n this.log('[StreamingAudioPlayer] Cannot start playback: AudioContext not initialized')\n return\n }\n if (this.isPlaying) {\n this.log('[StreamingAudioPlayer] Cannot start playback: Already playing')\n return\n }\n\n this.isPlaying = true\n this.sessionStartTime = this.audioContext.currentTime\n this.scheduledTime = this.sessionStartTime\n\n this.log('[StreamingAudioPlayer] Starting playback', {\n sessionStartTime: this.sessionStartTime,\n bufferedChunks: this.audioChunks.length,\n scheduledChunks: this.scheduledChunks,\n activeSources: this.activeSources.size,\n })\n\n // Schedule all available chunks\n this.scheduleAllChunks()\n }\n\n /**\n * Schedule all pending chunks\n */\n private scheduleAllChunks(): void {\n while (this.scheduledChunks < this.audioChunks.length) {\n this.scheduleNextChunk()\n }\n }\n\n /**\n * Schedule next audio chunk\n */\n private scheduleNextChunk(): void {\n if (!this.audioContext) {\n this.log('[StreamingAudioPlayer] Cannot schedule chunk: AudioContext not initialized')\n return\n }\n if (!this.isPlaying || this.isPaused) {\n this.log('[StreamingAudioPlayer] Cannot schedule chunk: Not playing or paused')\n return\n }\n\n const chunkIndex = this.scheduledChunks\n if (chunkIndex >= this.audioChunks.length) {\n this.log(`[StreamingAudioPlayer] No more chunks to schedule (chunkIndex: ${chunkIndex}, totalChunks: ${this.audioChunks.length})`)\n return\n }\n\n const chunk = this.audioChunks[chunkIndex]\n\n // 当音频块为空且不是最后一个块时,跳过调度\n if (chunk.data.length === 0 && !chunk.isLast) {\n this.scheduledChunks++\n return\n }\n\n const pcmData = chunk.data\n const isLast = chunk.isLast\n const audioBuffer = this.pcmToAudioBuffer(pcmData)\n\n if (!audioBuffer) {\n const errorMessage = 'Failed to create AudioBuffer from PCM data'\n logger.error(errorMessage)\n AvatarKit.logEvent('character_player', 'error', {\n sessionId: this.sessionId,\n event: 'audio_buffer_creation_failed',\n })\n return\n }\n\n try {\n // Create and configure source node\n const source = this.audioContext.createBufferSource()\n source.buffer = audioBuffer\n // Connect through gainNode for volume control\n source.connect(this.gainNode!)\n\n // Schedule playback\n source.start(this.scheduledTime)\n\n // Track active source for hard-cancel\n this.activeSources.add(source)\n source.onended = () => {\n // Remove from active list when it ends\n this.activeSources.delete(source)\n\n // Check if this was the last chunk and all sources have ended\n if (isLast && this.activeSources.size === 0) {\n this.log('Last audio chunk ended, marking playback as ended')\n this.markEnded()\n }\n }\n\n // Update scheduled time for next chunk\n this.scheduledTime += audioBuffer.duration\n\n this.scheduledChunks++\n\n this.log(`[StreamingAudioPlayer] Scheduled chunk ${chunkIndex + 1}/${this.audioChunks.length}`, {\n startTime: this.scheduledTime - audioBuffer.duration,\n duration: audioBuffer.duration,\n nextScheduleTime: this.scheduledTime,\n isLast,\n activeSources: this.activeSources.size,\n })\n }\n catch (err) {\n logger.errorWithError('Failed to schedule audio chunk:', err)\n AvatarKit.logEvent('character_player', 'error', {\n sessionId: this.sessionId,\n event: 'schedule_chunk_failed',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n /**\n * Convert PCM data to AudioBuffer\n * Input: 16-bit PCM (int16), Output: AudioBuffer (float32 [-1, 1])\n */\n private pcmToAudioBuffer(pcmData: Uint8Array): AudioBuffer | null {\n if (!this.audioContext) {\n return null\n }\n\n // Handle empty PCM data (e.g., when isLast is true)\n if (pcmData.length === 0) {\n // For empty chunks (typically the last chunk), create minimal silence\n // Use a very short duration to avoid playback stuttering\n const silenceDuration = 0.01 // 1ms - minimal silence to maintain timing\n const numSamples = Math.floor(this.sampleRate * silenceDuration)\n\n const audioBuffer = this.audioContext.createBuffer(\n this.channelCount,\n numSamples,\n this.sampleRate,\n )\n\n // Fill with silence (all zeros)\n for (let channel = 0; channel < this.channelCount; channel++) {\n const channelData = audioBuffer.getChannelData(channel)\n channelData.fill(0) // Fill with silence\n }\n\n return audioBuffer\n }\n\n // Create aligned copy to avoid byte offset issues\n // Int16Array requires byteOffset to be a multiple of 2\n const alignedData = new Uint8Array(pcmData)\n const int16Array = new Int16Array(alignedData.buffer, 0, alignedData.length / 2)\n\n // Calculate number of samples\n const numSamples = int16Array.length / this.channelCount\n\n // Create AudioBuffer\n const audioBuffer = this.audioContext.createBuffer(\n this.channelCount,\n numSamples,\n this.sampleRate,\n )\n\n // Convert int16 to float32 [-1, 1]\n for (let channel = 0; channel < this.channelCount; channel++) {\n const channelData = audioBuffer.getChannelData(channel)\n\n for (let i = 0; i < numSamples; i++) {\n const sampleIndex = i * this.channelCount + channel\n // Normalize int16 (-32768 to 32767) to float32 (-1 to 1)\n channelData[i] = int16Array[sampleIndex] / 32768.0\n }\n }\n\n return audioBuffer\n }\n\n /**\n * Get current playback time (seconds)\n */\n getCurrentTime(): number {\n if (!this.audioContext || !this.isPlaying) {\n return 0\n }\n\n if (this.isPaused) {\n return this.pausedAt\n }\n\n // Calculate elapsed time using session start time and paused offset\n const currentAudioTime = this.audioContext.currentTime\n const elapsed = currentAudioTime - this.sessionStartTime - this.pausedTimeOffset\n\n return Math.max(0, elapsed)\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n if (!this.isPlaying || this.isPaused || !this.audioContext) {\n return\n }\n\n // 1. 记录逻辑时间(用于 getCurrentTime 返回固定值)\n this.pausedAt = this.getCurrentTime()\n\n // 2. 记录 AudioContext 时间戳(关键!用于恢复计算)\n this.pausedAudioContextTime = this.audioContext.currentTime\n\n // 3. 设置暂停标志\n this.isPaused = true\n\n // 4. 挂起 AudioContext 以暂停所有活动的音频源\n if (this.audioContext.state === 'running') {\n this.audioContext.suspend().catch((err) => {\n logger.errorWithError('Failed to suspend AudioContext:', err)\n // 如果挂起失败,恢复状态\n this.isPaused = false\n })\n }\n\n this.log('Playback paused', {\n pausedAt: this.pausedAt,\n pausedAudioContextTime: this.pausedAudioContextTime,\n audioContextState: this.audioContext.state,\n })\n }\n\n /**\n * Resume playback\n */\n async resume(): Promise<void> {\n if (!this.isPaused || !this.audioContext || !this.isPlaying) {\n return\n }\n\n // 1. 首先恢复 AudioContext(使 currentTime 继续)\n if (this.audioContext.state === 'suspended') {\n try {\n await this.audioContext.resume()\n }\n catch (err) {\n logger.errorWithError('Failed to resume AudioContext:', err)\n throw err\n }\n }\n\n // 2. 调整 sessionStartTime,使 getCurrentTime() 从 pausedAt 继续\n // 数学推导:\n // 恢复后,我们希望:getCurrentTime() = pausedAt + (currentAudioTime - pausedAudioContextTime)\n // 当前公式:getCurrentTime() = currentAudioTime - sessionStartTime - pausedTimeOffset\n //\n // 令两者相等:\n // pausedAt + (currentAudioTime - pausedAudioContextTime) = currentAudioTime - sessionStartTime - pausedTimeOffset\n // => sessionStartTime = pausedAudioContextTime - pausedAt - pausedTimeOffset\n const currentAudioTime = this.audioContext.currentTime\n this.sessionStartTime = this.pausedAudioContextTime - this.pausedAt - this.pausedTimeOffset\n\n // 3. 清除暂停标志\n this.isPaused = false\n\n // 4. 继续调度未调度的音频块(如果在暂停期间有新数据到达)\n if (this.scheduledChunks < this.audioChunks.length) {\n this.scheduleAllChunks()\n }\n\n this.log('Playback resumed', {\n pausedAt: this.pausedAt,\n pausedAudioContextTime: this.pausedAudioContextTime,\n currentAudioContextTime: currentAudioTime,\n adjustedSessionStartTime: this.sessionStartTime,\n audioContextState: this.audioContext.state,\n })\n }\n\n /**\n * Stop playback\n */\n stop(): void {\n if (!this.audioContext) {\n return\n }\n\n // 如果暂停,先恢复 AudioContext(以便正确停止源)\n if (this.isPaused && this.audioContext.state === 'suspended') {\n this.audioContext.resume().catch(() => {\n // 忽略恢复错误,因为我们要停止播放\n })\n this.isPaused = false\n }\n\n this.isPlaying = false\n this.isPaused = false\n this.sessionStartTime = 0 // Reset session start time\n this.scheduledTime = 0 // Reset scheduled time for next session\n\n // Hard stop all scheduled sources immediately\n for (const source of this.activeSources) {\n source.onended = null\n try {\n source.stop(0)\n }\n catch {}\n try {\n source.disconnect()\n }\n catch {}\n }\n this.activeSources.clear()\n\n // 清理音频块和调度状态,确保下次播放时状态干净\n this.audioChunks = []\n this.scheduledChunks = 0\n\n this.log('[StreamingAudioPlayer] Playback stopped, state reset')\n\n // Note: Individual source nodes will stop automatically\n // We just reset our state\n }\n\n /**\n * Enable or disable auto-start (for delayed start scenarios)\n */\n setAutoStart(enabled: boolean): void {\n this.autoStartEnabled = enabled\n this.log(`Auto-start ${enabled ? 'enabled' : 'disabled'}`)\n }\n\n /**\n * Start playback manually (for delayed start scenarios)\n * This allows starting playback after transition animation completes\n */\n play(): void {\n if (this.isPlaying) {\n return\n }\n // Enable auto-start when manually starting playback\n this.autoStartEnabled = true\n this.startPlayback()\n }\n\n /**\n * Mark playback as ended\n */\n markEnded(): void {\n this.log('Playback ended')\n this.isPlaying = false\n this.onEndedCallback?.()\n }\n\n /**\n * Set ended callback\n */\n onEnded(callback: () => void): void {\n this.onEndedCallback = callback\n }\n\n /**\n * Check if playing\n */\n isPlayingNow(): boolean {\n return this.isPlaying && !this.isPaused\n }\n\n /**\n * Get total duration of buffered audio\n */\n getBufferedDuration(): number {\n if (!this.audioContext) {\n return 0\n }\n\n let totalSamples = 0\n for (const chunk of this.audioChunks) {\n totalSamples += chunk.data.length / 2 / this.channelCount // 16-bit = 2 bytes per sample\n }\n\n return totalSamples / this.sampleRate\n }\n\n /**\n * Get remaining duration (buffered - played) in seconds\n */\n getRemainingDuration(): number {\n const total = this.getBufferedDuration()\n const played = this.getCurrentTime()\n return Math.max(0, total - played)\n }\n\n\n /**\n * Dispose and cleanup\n */\n dispose(): void {\n this.stop()\n\n // Close AudioContext\n if (this.audioContext) {\n this.audioContext.close()\n this.audioContext = null\n this.gainNode = null\n }\n\n // Clear session state\n this.audioChunks = []\n this.scheduledChunks = 0\n this.sessionStartTime = 0\n this.pausedTimeOffset = 0\n this.pausedAt = 0\n this.pausedAudioContextTime = 0\n this.scheduledTime = 0\n this.onEndedCallback = undefined\n\n this.log('StreamingAudioPlayer disposed')\n }\n\n /**\n * Flush buffered audio\n * - hard: stops all playing sources and clears all chunks\n * - soft (default): clears UNSCHEDULED chunks only\n */\n flush(options?: { hard?: boolean }): void {\n const hard = options?.hard === true\n if (hard) {\n this.stop()\n this.audioChunks = []\n this.scheduledChunks = 0\n this.sessionStartTime = 0\n this.pausedAt = 0\n this.scheduledTime = 0\n this.log('Flushed (hard)')\n return\n }\n\n // Soft flush: drop unscheduled region\n if (this.scheduledChunks < this.audioChunks.length) {\n this.audioChunks.splice(this.scheduledChunks)\n }\n this.log('Flushed (soft)', { remainingScheduled: this.scheduledChunks })\n }\n\n /**\n * 设置音量 (0.0 - 1.0)\n * 注意:这仅控制数字人音频播放器的音量,不影响系统音量\n * @param volume 音量值,范围 0.0 到 1.0(0.0 为静音,1.0 为最大音量)\n */\n setVolume(volume: number): void {\n if (volume < 0 || volume > 1) {\n logger.warn(`[StreamingAudioPlayer] Volume out of range: ${volume}, clamping to [0, 1]`)\n volume = Math.max(0, Math.min(1, volume))\n }\n\n this.volume = volume\n if (this.gainNode) {\n this.gainNode.gain.value = volume\n }\n }\n\n /**\n * 获取当前音量\n * @returns 当前音量值 (0.0 - 1.0)\n */\n getVolume(): number {\n return this.volume\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.debug) {\n logger.log(`[StreamingAudioPlayer] ${message}`, data || '')\n }\n }\n}\n"],"names":["StreamingAudioPlayer","options","__publicField","APP_CONFIG","error","message","errorToMessage","AvatarKit","logger","pcmData","isLast","audioChunks","chunk","chunkIndex","audioBuffer","source","err","numSamples","channel","alignedData","int16Array","channelData","i","sampleIndex","elapsed","currentAudioTime","enabled","_a","callback","totalSamples","total","played","volume","data"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StreamingAudioPlayer-PkzxBP93.js","sources":["../audio/StreamingAudioPlayer.ts"],"sourcesContent":["/**\n * Streaming Audio Player\n * Implements real-time audio playback using Web Audio API\n * Supports dynamic PCM chunk addition without Workers\n */\n\nimport { APP_CONFIG } from '../config/app-config'\nimport { AvatarKit } from '../core/AvatarKit'\nimport { errorToMessage } from '../utils/error-utils'\nimport { logger } from '../utils/logger'\n\nexport interface StreamingAudioPlayerOptions {\n sampleRate?: number // PCM sample rate (default: APP_CONFIG.audio.sampleRate, backend requires 16kHz)\n channelCount?: number // Number of channels (default: 1)\n debug?: boolean\n}\n\nexport class StreamingAudioPlayer {\n // AudioContext is managed internally\n private audioContext: AudioContext | null = null\n private sampleRate: number\n private channelCount: number\n private debug: boolean\n\n // Session-level state\n private sessionId: string\n private sessionStartTime = 0 // AudioContext time when session started\n private pausedTimeOffset = 0 // Accumulated paused time\n private pausedAt = 0 // Time when paused\n private pausedAudioContextTime = 0 // audioContext.currentTime when paused (for resume calculation)\n private scheduledTime = 0 // Next chunk schedule time in AudioContext time\n\n // Playback state\n private isPlaying = false\n private isPaused = false\n private autoStartEnabled = true // Control whether to auto-start when buffer is ready\n\n // Audio buffer queue\n private audioChunks: Array<{ data: Uint8Array, isLast: boolean }> = []\n private scheduledChunks = 0 // Number of chunks already scheduled\n private activeSources = new Set<AudioBufferSourceNode>()\n\n // Volume control\n private gainNode: GainNode | null = null\n private volume: number = 1.0 // Default volume 1.0 (0.0 - 1.0)\n\n // Event callbacks\n private onEndedCallback?: () => void\n\n constructor(options?: StreamingAudioPlayerOptions) {\n this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n this.sampleRate = options?.sampleRate ?? APP_CONFIG.audio.sampleRate\n this.channelCount = options?.channelCount ?? 1\n this.debug = options?.debug ?? false\n }\n\n /**\n * Initialize audio context (create and ensure it's ready)\n */\n async initialize(): Promise<void> {\n if (this.audioContext) {\n return\n }\n\n try {\n // Create AudioContext\n this.audioContext = new AudioContext({\n sampleRate: this.sampleRate,\n })\n\n // Create GainNode for volume control\n this.gainNode = this.audioContext.createGain()\n this.gainNode.gain.value = this.volume\n this.gainNode.connect(this.audioContext.destination)\n\n // Resume context (required for some browsers)\n if (this.audioContext.state === 'suspended') {\n await this.audioContext.resume()\n }\n\n this.log('AudioContext initialized', {\n sessionId: this.sessionId,\n sampleRate: this.audioContext.sampleRate,\n state: this.audioContext.state,\n })\n }\n catch (error) {\n const message = errorToMessage(error)\n AvatarKit.logEvent('activeAudioSessionFailed', 'warning', {\n sessionId: this.sessionId,\n reason: message,\n })\n logger.error('Failed to initialize AudioContext:', message)\n throw error instanceof Error ? error : new Error(message)\n }\n }\n\n /**\n * Add audio chunk (16-bit PCM)\n */\n addChunk(pcmData: Uint8Array, isLast: boolean = false): void {\n if (!this.audioContext) {\n logger.error('AudioContext not initialized')\n return\n }\n\n // Store chunk with metadata\n this.audioChunks.push({ data: pcmData, isLast })\n\n // Track buffer underrun warning\n if (this.isPlaying && this.audioChunks.length === this.scheduledChunks) {\n // Buffer underrun detected - chunks consumed faster than added\n }\n\n this.log(`Added chunk ${this.audioChunks.length}`, {\n size: pcmData.length,\n totalChunks: this.audioChunks.length,\n isLast,\n isPlaying: this.isPlaying,\n scheduledChunks: this.scheduledChunks,\n })\n\n // Auto-start if we have any audio chunks and auto-start is enabled\n if (!this.isPlaying && this.autoStartEnabled && this.audioChunks.length > 0) {\n this.log('[StreamingAudioPlayer] Auto-starting playback from addChunk')\n this.startPlayback()\n }\n // Schedule next chunk if already playing and not paused\n else if (this.isPlaying && !this.isPaused) {\n this.log('[StreamingAudioPlayer] Already playing, scheduling next chunk')\n this.scheduleNextChunk()\n } else {\n this.log('[StreamingAudioPlayer] Not playing and no chunks, waiting for more chunks')\n }\n }\n\n /**\n * Start new session (stop current and start fresh)\n */\n async startNewSession(audioChunks: Array<{ data: Uint8Array, isLast: boolean }>): Promise<void> {\n // Stop current session if playing\n this.stop()\n\n // Generate new session ID to prevent data mixing\n this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n\n // Reset session state\n this.audioChunks = []\n this.scheduledChunks = 0\n this.pausedTimeOffset = 0\n this.pausedAt = 0\n this.pausedAudioContextTime = 0\n // Don't set sessionStartTime or scheduledTime here - let startPlayback() set them\n this.log('Starting new session', {\n chunks: audioChunks.length,\n })\n\n // Add audio chunks with their metadata\n for (const chunk of audioChunks) {\n this.addChunk(chunk.data, chunk.isLast)\n }\n }\n\n /**\n * Start playback\n */\n private startPlayback(): void {\n if (!this.audioContext) {\n this.log('[StreamingAudioPlayer] Cannot start playback: AudioContext not initialized')\n return\n }\n if (this.isPlaying) {\n this.log('[StreamingAudioPlayer] Cannot start playback: Already playing')\n return\n }\n\n this.isPlaying = true\n this.sessionStartTime = this.audioContext.currentTime\n this.scheduledTime = this.sessionStartTime\n\n this.log('[StreamingAudioPlayer] Starting playback', {\n sessionStartTime: this.sessionStartTime,\n bufferedChunks: this.audioChunks.length,\n scheduledChunks: this.scheduledChunks,\n activeSources: this.activeSources.size,\n })\n\n // Schedule all available chunks\n this.scheduleAllChunks()\n }\n\n /**\n * Schedule all pending chunks\n */\n private scheduleAllChunks(): void {\n while (this.scheduledChunks < this.audioChunks.length) {\n this.scheduleNextChunk()\n }\n }\n\n /**\n * Schedule next audio chunk\n */\n private scheduleNextChunk(): void {\n if (!this.audioContext) {\n this.log('[StreamingAudioPlayer] Cannot schedule chunk: AudioContext not initialized')\n return\n }\n if (!this.isPlaying || this.isPaused) {\n this.log('[StreamingAudioPlayer] Cannot schedule chunk: Not playing or paused')\n return\n }\n\n const chunkIndex = this.scheduledChunks\n if (chunkIndex >= this.audioChunks.length) {\n this.log(`[StreamingAudioPlayer] No more chunks to schedule (chunkIndex: ${chunkIndex}, totalChunks: ${this.audioChunks.length})`)\n return\n }\n\n const chunk = this.audioChunks[chunkIndex]\n\n // 当音频块为空且不是最后一个块时,跳过调度\n if (chunk.data.length === 0 && !chunk.isLast) {\n this.scheduledChunks++\n return\n }\n\n const pcmData = chunk.data\n const isLast = chunk.isLast\n const audioBuffer = this.pcmToAudioBuffer(pcmData)\n\n if (!audioBuffer) {\n const errorMessage = 'Failed to create AudioBuffer from PCM data'\n logger.error(errorMessage)\n AvatarKit.logEvent('character_player', 'error', {\n sessionId: this.sessionId,\n event: 'audio_buffer_creation_failed',\n })\n return\n }\n\n try {\n // Create and configure source node\n const source = this.audioContext.createBufferSource()\n source.buffer = audioBuffer\n // Connect through gainNode for volume control\n source.connect(this.gainNode!)\n\n // Schedule playback\n source.start(this.scheduledTime)\n\n // Track active source for hard-cancel\n this.activeSources.add(source)\n source.onended = () => {\n // Remove from active list when it ends\n this.activeSources.delete(source)\n\n // Check if this was the last chunk and all sources have ended\n if (isLast && this.activeSources.size === 0) {\n this.log('Last audio chunk ended, marking playback as ended')\n this.markEnded()\n }\n }\n\n // Update scheduled time for next chunk\n this.scheduledTime += audioBuffer.duration\n\n this.scheduledChunks++\n\n this.log(`[StreamingAudioPlayer] Scheduled chunk ${chunkIndex + 1}/${this.audioChunks.length}`, {\n startTime: this.scheduledTime - audioBuffer.duration,\n duration: audioBuffer.duration,\n nextScheduleTime: this.scheduledTime,\n isLast,\n activeSources: this.activeSources.size,\n })\n }\n catch (err) {\n logger.errorWithError('Failed to schedule audio chunk:', err)\n AvatarKit.logEvent('character_player', 'error', {\n sessionId: this.sessionId,\n event: 'schedule_chunk_failed',\n reason: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n /**\n * Convert PCM data to AudioBuffer\n * Input: 16-bit PCM (int16), Output: AudioBuffer (float32 [-1, 1])\n */\n private pcmToAudioBuffer(pcmData: Uint8Array): AudioBuffer | null {\n if (!this.audioContext) {\n return null\n }\n\n // Handle empty PCM data (e.g., when isLast is true)\n if (pcmData.length === 0) {\n // For empty chunks (typically the last chunk), create minimal silence\n // Use a very short duration to avoid playback stuttering\n const silenceDuration = 0.01 // 1ms - minimal silence to maintain timing\n const numSamples = Math.floor(this.sampleRate * silenceDuration)\n\n const audioBuffer = this.audioContext.createBuffer(\n this.channelCount,\n numSamples,\n this.sampleRate,\n )\n\n // Fill with silence (all zeros)\n for (let channel = 0; channel < this.channelCount; channel++) {\n const channelData = audioBuffer.getChannelData(channel)\n channelData.fill(0) // Fill with silence\n }\n\n return audioBuffer\n }\n\n // Create aligned copy to avoid byte offset issues\n // Int16Array requires byteOffset to be a multiple of 2\n const alignedData = new Uint8Array(pcmData)\n const int16Array = new Int16Array(alignedData.buffer, 0, alignedData.length / 2)\n\n // Calculate number of samples\n const numSamples = int16Array.length / this.channelCount\n\n // Create AudioBuffer\n const audioBuffer = this.audioContext.createBuffer(\n this.channelCount,\n numSamples,\n this.sampleRate,\n )\n\n // Convert int16 to float32 [-1, 1]\n for (let channel = 0; channel < this.channelCount; channel++) {\n const channelData = audioBuffer.getChannelData(channel)\n\n for (let i = 0; i < numSamples; i++) {\n const sampleIndex = i * this.channelCount + channel\n // Normalize int16 (-32768 to 32767) to float32 (-1 to 1)\n channelData[i] = int16Array[sampleIndex] / 32768.0\n }\n }\n\n return audioBuffer\n }\n\n /**\n * Get current playback time (seconds)\n */\n getCurrentTime(): number {\n if (!this.audioContext || !this.isPlaying) {\n return 0\n }\n\n if (this.isPaused) {\n return this.pausedAt\n }\n\n // Calculate elapsed time using session start time and paused offset\n const currentAudioTime = this.audioContext.currentTime\n const elapsed = currentAudioTime - this.sessionStartTime - this.pausedTimeOffset\n\n return Math.max(0, elapsed)\n }\n\n /**\n * Pause playback\n */\n pause(): void {\n if (!this.isPlaying || this.isPaused || !this.audioContext) {\n return\n }\n\n // 1. 记录逻辑时间(用于 getCurrentTime 返回固定值)\n this.pausedAt = this.getCurrentTime()\n\n // 2. 记录 AudioContext 时间戳(关键!用于恢复计算)\n this.pausedAudioContextTime = this.audioContext.currentTime\n\n // 3. 设置暂停标志\n this.isPaused = true\n\n // 4. 挂起 AudioContext 以暂停所有活动的音频源\n if (this.audioContext.state === 'running') {\n this.audioContext.suspend().catch((err) => {\n logger.errorWithError('Failed to suspend AudioContext:', err)\n // 如果挂起失败,恢复状态\n this.isPaused = false\n })\n }\n\n this.log('Playback paused', {\n pausedAt: this.pausedAt,\n pausedAudioContextTime: this.pausedAudioContextTime,\n audioContextState: this.audioContext.state,\n })\n }\n\n /**\n * Resume playback\n */\n async resume(): Promise<void> {\n if (!this.isPaused || !this.audioContext || !this.isPlaying) {\n return\n }\n\n // 1. 首先恢复 AudioContext(使 currentTime 继续)\n if (this.audioContext.state === 'suspended') {\n try {\n await this.audioContext.resume()\n }\n catch (err) {\n logger.errorWithError('Failed to resume AudioContext:', err)\n throw err\n }\n }\n\n // 2. 调整 sessionStartTime,使 getCurrentTime() 从 pausedAt 继续\n // 数学推导:\n // 恢复后,我们希望:getCurrentTime() = pausedAt + (currentAudioTime - pausedAudioContextTime)\n // 当前公式:getCurrentTime() = currentAudioTime - sessionStartTime - pausedTimeOffset\n //\n // 令两者相等:\n // pausedAt + (currentAudioTime - pausedAudioContextTime) = currentAudioTime - sessionStartTime - pausedTimeOffset\n // => sessionStartTime = pausedAudioContextTime - pausedAt - pausedTimeOffset\n const currentAudioTime = this.audioContext.currentTime\n this.sessionStartTime = this.pausedAudioContextTime - this.pausedAt - this.pausedTimeOffset\n\n // 3. 清除暂停标志\n this.isPaused = false\n\n // 4. 继续调度未调度的音频块(如果在暂停期间有新数据到达)\n if (this.scheduledChunks < this.audioChunks.length) {\n this.scheduleAllChunks()\n }\n\n this.log('Playback resumed', {\n pausedAt: this.pausedAt,\n pausedAudioContextTime: this.pausedAudioContextTime,\n currentAudioContextTime: currentAudioTime,\n adjustedSessionStartTime: this.sessionStartTime,\n audioContextState: this.audioContext.state,\n })\n }\n\n /**\n * Stop playback\n */\n stop(): void {\n if (!this.audioContext) {\n return\n }\n\n // 如果暂停,先恢复 AudioContext(以便正确停止源)\n if (this.isPaused && this.audioContext.state === 'suspended') {\n this.audioContext.resume().catch(() => {\n // 忽略恢复错误,因为我们要停止播放\n })\n this.isPaused = false\n }\n\n this.isPlaying = false\n this.isPaused = false\n this.sessionStartTime = 0 // Reset session start time\n this.scheduledTime = 0 // Reset scheduled time for next session\n\n // Hard stop all scheduled sources immediately\n for (const source of this.activeSources) {\n source.onended = null\n try {\n source.stop(0)\n }\n catch {}\n try {\n source.disconnect()\n }\n catch {}\n }\n this.activeSources.clear()\n\n // 清理音频块和调度状态,确保下次播放时状态干净\n this.audioChunks = []\n this.scheduledChunks = 0\n\n this.log('[StreamingAudioPlayer] Playback stopped, state reset')\n\n // Note: Individual source nodes will stop automatically\n // We just reset our state\n }\n\n /**\n * Enable or disable auto-start (for delayed start scenarios)\n */\n setAutoStart(enabled: boolean): void {\n this.autoStartEnabled = enabled\n this.log(`Auto-start ${enabled ? 'enabled' : 'disabled'}`)\n }\n\n /**\n * Start playback manually (for delayed start scenarios)\n * This allows starting playback after transition animation completes\n */\n play(): void {\n if (this.isPlaying) {\n return\n }\n // Enable auto-start when manually starting playback\n this.autoStartEnabled = true\n this.startPlayback()\n }\n\n /**\n * Mark playback as ended\n */\n markEnded(): void {\n this.log('Playback ended')\n this.isPlaying = false\n this.onEndedCallback?.()\n }\n\n /**\n * Set ended callback\n */\n onEnded(callback: () => void): void {\n this.onEndedCallback = callback\n }\n\n /**\n * Check if playing\n */\n isPlayingNow(): boolean {\n return this.isPlaying && !this.isPaused\n }\n\n /**\n * Get total duration of buffered audio\n */\n getBufferedDuration(): number {\n if (!this.audioContext) {\n return 0\n }\n\n let totalSamples = 0\n for (const chunk of this.audioChunks) {\n totalSamples += chunk.data.length / 2 / this.channelCount // 16-bit = 2 bytes per sample\n }\n\n return totalSamples / this.sampleRate\n }\n\n /**\n * Get remaining duration (buffered - played) in seconds\n */\n getRemainingDuration(): number {\n const total = this.getBufferedDuration()\n const played = this.getCurrentTime()\n return Math.max(0, total - played)\n }\n\n\n /**\n * Dispose and cleanup\n */\n dispose(): void {\n this.stop()\n\n // Close AudioContext\n if (this.audioContext) {\n this.audioContext.close()\n this.audioContext = null\n this.gainNode = null\n }\n\n // Clear session state\n this.audioChunks = []\n this.scheduledChunks = 0\n this.sessionStartTime = 0\n this.pausedTimeOffset = 0\n this.pausedAt = 0\n this.pausedAudioContextTime = 0\n this.scheduledTime = 0\n this.onEndedCallback = undefined\n\n this.log('StreamingAudioPlayer disposed')\n }\n\n /**\n * Flush buffered audio\n * - hard: stops all playing sources and clears all chunks\n * - soft (default): clears UNSCHEDULED chunks only\n */\n flush(options?: { hard?: boolean }): void {\n const hard = options?.hard === true\n if (hard) {\n this.stop()\n this.audioChunks = []\n this.scheduledChunks = 0\n this.sessionStartTime = 0\n this.pausedAt = 0\n this.scheduledTime = 0\n this.log('Flushed (hard)')\n return\n }\n\n // Soft flush: drop unscheduled region\n if (this.scheduledChunks < this.audioChunks.length) {\n this.audioChunks.splice(this.scheduledChunks)\n }\n this.log('Flushed (soft)', { remainingScheduled: this.scheduledChunks })\n }\n\n /**\n * 设置音量 (0.0 - 1.0)\n * 注意:这仅控制数字人音频播放器的音量,不影响系统音量\n * @param volume 音量值,范围 0.0 到 1.0(0.0 为静音,1.0 为最大音量)\n */\n setVolume(volume: number): void {\n if (volume < 0 || volume > 1) {\n logger.warn(`[StreamingAudioPlayer] Volume out of range: ${volume}, clamping to [0, 1]`)\n volume = Math.max(0, Math.min(1, volume))\n }\n\n this.volume = volume\n if (this.gainNode) {\n this.gainNode.gain.value = volume\n }\n }\n\n /**\n * 获取当前音量\n * @returns 当前音量值 (0.0 - 1.0)\n */\n getVolume(): number {\n return this.volume\n }\n\n /**\n * Debug logging\n */\n private log(message: string, data?: unknown): void {\n if (this.debug) {\n logger.log(`[StreamingAudioPlayer] ${message}`, data || '')\n }\n }\n}\n"],"names":["StreamingAudioPlayer","options","__publicField","APP_CONFIG","error","message","errorToMessage","AvatarKit","logger","pcmData","isLast","audioChunks","chunk","chunkIndex","audioBuffer","source","err","numSamples","channel","alignedData","int16Array","channelData","i","sampleIndex","elapsed","currentAudioTime","enabled","_a","callback","totalSamples","total","played","volume","data"],"mappings":"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"}
|
|
@@ -53,17 +53,6 @@ export declare class AvatarController {
|
|
|
53
53
|
* Close service (SDK mode only)
|
|
54
54
|
*/
|
|
55
55
|
close(): void;
|
|
56
|
-
/**
|
|
57
|
-
* Playback existing audio and animation data (host mode)
|
|
58
|
-
* Starts a new conversation by generating a new conversation ID and interrupting any existing conversation
|
|
59
|
-
* @param initialAudioChunks - Existing audio chunks to playback
|
|
60
|
-
* @param initialKeyframes - Existing animation keyframes to playback
|
|
61
|
-
* @returns conversationId - New conversation ID for this conversation session
|
|
62
|
-
*/
|
|
63
|
-
playback(initialAudioChunks?: Array<{
|
|
64
|
-
data: Uint8Array;
|
|
65
|
-
isLast: boolean;
|
|
66
|
-
}>, initialKeyframes?: Flame[]): Promise<string>;
|
|
67
56
|
/**
|
|
68
57
|
* Send audio data (host mode)
|
|
69
58
|
* Stream additional audio data after playback()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarController.d.ts","sourceRoot":"","sources":["../../core/AvatarController.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,6CAA6C,CAAA;AACxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAItC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,eAAe,EAAiB,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAM7G,qBAAa,gBAAgB;IAE3B,OAAO,CAAC,YAAY,CAAC,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,SAAS,CAAC,MAAM,EAAE,MAAM,CAAA;IAGxB,SAAS,CAAC,eAAe,EAAE,eAAe,GAAG,IAAI,CAAO;IACxD,SAAS,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAK;IACxC,SAAS,CAAC,kBAAkB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAK;IAC/E,SAAS,CAAC,SAAS,UAAQ;IAG3B,SAAS,CAAC,WAAW,UAAQ;IAC7B,SAAS,CAAC,YAAY,EAAE,WAAW,CAAmB;IAGtD,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,MAAM,CAAiB;IAGxB,iBAAiB,EAAE,CAAC,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IACnE,mBAAmB,EAAE,CAAC,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IACvE,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IACtD,OAAO,CAAC,cAAc,CAA+C;IAGrE,OAAO,CAAC,cAAc,CAAC,CAAuD;IAC9E,OAAO,CAAC,eAAe,CAAsB;IAG7C,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,sBAAsB,CAAa;IAC3C,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAO;IACrC,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAO;IACnD,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAM;IAG/C,OAAO,CAAC,eAAe,CAAQ;gBAEnB,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE;IA+E3E;;;;OAIG;IACH,wBAAwB,IAAI,MAAM,GAAG,IAAI;IAMzC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B5B;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,GAAE,OAAe,GAAG,MAAM,GAAG,IAAI;IA2CjE;;OAEG;IACH,KAAK,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"AvatarController.d.ts","sourceRoot":"","sources":["../../core/AvatarController.ts"],"names":[],"mappings":"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"}
|
|
@@ -2061,7 +2061,7 @@ class y {
|
|
|
2061
2061
|
h[i](`[Telemetry] ${e}`, a);
|
|
2062
2062
|
}
|
|
2063
2063
|
}
|
|
2064
|
-
u(y, "_isInitialized", !1), u(y, "_appId", null), u(y, "_configuration", null), u(y, "_sessionToken", null), u(y, "_userId", null), u(y, "_version", "1.0.0-beta.
|
|
2064
|
+
u(y, "_isInitialized", !1), u(y, "_appId", null), u(y, "_configuration", null), u(y, "_sessionToken", null), u(y, "_userId", null), u(y, "_version", "1.0.0-beta.23"), u(y, "_avatarCore", null), u(y, "_dynamicSdkConfig", null), u(y, "_logLevels", {
|
|
2065
2065
|
debug: "log",
|
|
2066
2066
|
info: "log",
|
|
2067
2067
|
warning: "warn",
|
|
@@ -2130,7 +2130,7 @@ const Ae = class Ae {
|
|
|
2130
2130
|
async createAndInitializeStreamingPlayer() {
|
|
2131
2131
|
if (this.streamingPlayer)
|
|
2132
2132
|
return;
|
|
2133
|
-
const { StreamingAudioPlayer: e } = await import("./StreamingAudioPlayer-
|
|
2133
|
+
const { StreamingAudioPlayer: e } = await import("./StreamingAudioPlayer-PkzxBP93.js");
|
|
2134
2134
|
this.streamingPlayer = new e({
|
|
2135
2135
|
sampleRate: k.audio.sampleRate,
|
|
2136
2136
|
channelCount: 1,
|
|
@@ -3888,6 +3888,7 @@ class Ca {
|
|
|
3888
3888
|
* @param initialAudioChunks - Existing audio chunks to playback
|
|
3889
3889
|
* @param initialKeyframes - Existing animation keyframes to playback
|
|
3890
3890
|
* @returns conversationId - New conversation ID for this conversation session
|
|
3891
|
+
* @internal
|
|
3891
3892
|
*/
|
|
3892
3893
|
async playback(e, t) {
|
|
3893
3894
|
if ((this.isPlaying || this.currentConversationId) && this.interrupt(), this.currentConversationId = this.generateAndLogNewConversationId(), this.reqEnd = !1, this.clearPlaybackData(), this.animationPlayer || (this.animationPlayer = new oe()), this.animationPlayer.isStreamingReady() || await this.animationPlayer.createAndInitializeStreamingPlayer(), e && e.length > 0 && this.pendingAudioChunks.push(...e), t && t.length > 0 ? (this.currentKeyframes = t, this.emit("keyframesUpdate", this.currentKeyframes)) : (h.warn("[AvatarController] Empty animation data in playback - enabling audio-only mode"), y.logEvent("character_manager", "info", {
|
|
@@ -6479,4 +6480,4 @@ export {
|
|
|
6479
6480
|
Na as i,
|
|
6480
6481
|
h as l
|
|
6481
6482
|
};
|
|
6482
|
-
//# sourceMappingURL=index-
|
|
6483
|
+
//# sourceMappingURL=index-DYf1u8L7.js.map
|