@myned-ai/avatar-chat-widget 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -170
- package/dist/asset/nyx.zip +0 -0
- package/dist/avatar-chat-widget.d.ts +18 -90
- package/dist/avatar-chat-widget.js +715 -333
- package/dist/avatar-chat-widget.js.map +1 -1
- package/dist/avatar-chat-widget.umd.js +1 -1
- package/dist/avatar-chat-widget.umd.js.map +1 -1
- package/dist/pcm16-processor.worklet.js +126 -0
- package/package.json +2 -1
- package/public/asset/nyx.zip +0 -0
- package/public/pcm16-processor.worklet.js +126 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PCM16 Audio Worklet Processor
|
|
3
|
+
* Converts browser audio (Float32, 48kHz) to PCM16 (Int16, 24kHz)
|
|
4
|
+
* Runs on dedicated audio thread for better performance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class PCM16Processor extends AudioWorkletProcessor {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
// Resampling state
|
|
12
|
+
this.resampleBuffer = [];
|
|
13
|
+
this.sampleIndex = 0;
|
|
14
|
+
this.TARGET_SAMPLE_RATE = 24000;
|
|
15
|
+
this.TARGET_BUFFER_SIZE = 2400; // 100ms at 24kHz
|
|
16
|
+
|
|
17
|
+
// Will be set from main thread
|
|
18
|
+
this.inputSampleRate = 48000; // Default, will be updated
|
|
19
|
+
this.resampleRatio = this.inputSampleRate / this.TARGET_SAMPLE_RATE;
|
|
20
|
+
|
|
21
|
+
// Debug counter
|
|
22
|
+
this.debugCounter = 0;
|
|
23
|
+
|
|
24
|
+
// Listen for messages from main thread
|
|
25
|
+
this.port.onmessage = (event) => {
|
|
26
|
+
if (event.data.type === 'config') {
|
|
27
|
+
this.inputSampleRate = event.data.sampleRate;
|
|
28
|
+
this.resampleRatio = this.inputSampleRate / this.TARGET_SAMPLE_RATE;
|
|
29
|
+
console.log(`[Worklet] Configured: inputRate=${this.inputSampleRate}Hz, resampleRatio=${this.resampleRatio.toFixed(2)}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Process audio data - called for each audio block (128 samples)
|
|
36
|
+
* @param {Float32Array[][]} inputs - Input audio data
|
|
37
|
+
* @param {Float32Array[][]} outputs - Output audio data (unused)
|
|
38
|
+
* @param {Object} parameters - Parameters (unused)
|
|
39
|
+
* @returns {boolean} - true to keep processor alive
|
|
40
|
+
*/
|
|
41
|
+
process(inputs, outputs, parameters) {
|
|
42
|
+
const input = inputs[0];
|
|
43
|
+
|
|
44
|
+
// No input or no channels
|
|
45
|
+
if (!input || !input[0]) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const inputData = input[0]; // Mono channel
|
|
50
|
+
this.debugCounter++;
|
|
51
|
+
|
|
52
|
+
// Debug: Log first callback and then every 100th
|
|
53
|
+
if (this.debugCounter === 1 || this.debugCounter % 100 === 0) {
|
|
54
|
+
let maxVal = -Infinity;
|
|
55
|
+
let minVal = Infinity;
|
|
56
|
+
let sum = 0;
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
59
|
+
const val = inputData[i];
|
|
60
|
+
if (val > maxVal) maxVal = val;
|
|
61
|
+
if (val < minVal) minVal = val;
|
|
62
|
+
sum += Math.abs(val);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const avg = sum / inputData.length;
|
|
66
|
+
|
|
67
|
+
// Only log if there's actual audio (not silence)
|
|
68
|
+
if (avg > 0.0001) {
|
|
69
|
+
this.port.postMessage({
|
|
70
|
+
type: 'debug',
|
|
71
|
+
data: {
|
|
72
|
+
counter: this.debugCounter,
|
|
73
|
+
min: minVal.toFixed(4),
|
|
74
|
+
max: maxVal.toFixed(4),
|
|
75
|
+
avg: avg.toFixed(6),
|
|
76
|
+
samples: inputData.length
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Downsample from native rate to 24kHz using simple decimation
|
|
83
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
84
|
+
this.sampleIndex++;
|
|
85
|
+
|
|
86
|
+
if (this.sampleIndex >= this.resampleRatio) {
|
|
87
|
+
this.sampleIndex -= this.resampleRatio;
|
|
88
|
+
this.resampleBuffer.push(inputData[i]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// When we have enough samples, send a chunk
|
|
93
|
+
while (this.resampleBuffer.length >= this.TARGET_BUFFER_SIZE) {
|
|
94
|
+
const chunk = this.resampleBuffer.splice(0, this.TARGET_BUFFER_SIZE);
|
|
95
|
+
|
|
96
|
+
// Convert Float32 [-1, 1] to Int16 [-32768, 32767]
|
|
97
|
+
const pcm16 = new Int16Array(this.TARGET_BUFFER_SIZE);
|
|
98
|
+
|
|
99
|
+
for (let j = 0; j < this.TARGET_BUFFER_SIZE; j++) {
|
|
100
|
+
// Clamp to [-1, 1] range
|
|
101
|
+
const clamped = Math.max(-1, Math.min(1, chunk[j]));
|
|
102
|
+
|
|
103
|
+
// Convert to 16-bit integer
|
|
104
|
+
pcm16[j] = clamped < 0
|
|
105
|
+
? clamped * 0x8000 // -32768
|
|
106
|
+
: clamped * 0x7FFF; // 32767
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Send PCM16 data to main thread
|
|
110
|
+
// Transfer ownership for zero-copy (performance optimization)
|
|
111
|
+
this.port.postMessage(
|
|
112
|
+
{
|
|
113
|
+
type: 'audio',
|
|
114
|
+
data: pcm16.buffer
|
|
115
|
+
},
|
|
116
|
+
[pcm16.buffer] // Transfer ownership
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Keep processor alive
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Register the processor
|
|
126
|
+
registerProcessor('pcm16-processor', PCM16Processor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myned-ai/avatar-chat-widget",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time voice/text chat widget with 3D avatar animation",
|
|
6
6
|
"author": "Myned AI",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
+
"public",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
Binary file
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PCM16 Audio Worklet Processor
|
|
3
|
+
* Converts browser audio (Float32, 48kHz) to PCM16 (Int16, 24kHz)
|
|
4
|
+
* Runs on dedicated audio thread for better performance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class PCM16Processor extends AudioWorkletProcessor {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
// Resampling state
|
|
12
|
+
this.resampleBuffer = [];
|
|
13
|
+
this.sampleIndex = 0;
|
|
14
|
+
this.TARGET_SAMPLE_RATE = 24000;
|
|
15
|
+
this.TARGET_BUFFER_SIZE = 2400; // 100ms at 24kHz
|
|
16
|
+
|
|
17
|
+
// Will be set from main thread
|
|
18
|
+
this.inputSampleRate = 48000; // Default, will be updated
|
|
19
|
+
this.resampleRatio = this.inputSampleRate / this.TARGET_SAMPLE_RATE;
|
|
20
|
+
|
|
21
|
+
// Debug counter
|
|
22
|
+
this.debugCounter = 0;
|
|
23
|
+
|
|
24
|
+
// Listen for messages from main thread
|
|
25
|
+
this.port.onmessage = (event) => {
|
|
26
|
+
if (event.data.type === 'config') {
|
|
27
|
+
this.inputSampleRate = event.data.sampleRate;
|
|
28
|
+
this.resampleRatio = this.inputSampleRate / this.TARGET_SAMPLE_RATE;
|
|
29
|
+
console.log(`[Worklet] Configured: inputRate=${this.inputSampleRate}Hz, resampleRatio=${this.resampleRatio.toFixed(2)}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Process audio data - called for each audio block (128 samples)
|
|
36
|
+
* @param {Float32Array[][]} inputs - Input audio data
|
|
37
|
+
* @param {Float32Array[][]} outputs - Output audio data (unused)
|
|
38
|
+
* @param {Object} parameters - Parameters (unused)
|
|
39
|
+
* @returns {boolean} - true to keep processor alive
|
|
40
|
+
*/
|
|
41
|
+
process(inputs, outputs, parameters) {
|
|
42
|
+
const input = inputs[0];
|
|
43
|
+
|
|
44
|
+
// No input or no channels
|
|
45
|
+
if (!input || !input[0]) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const inputData = input[0]; // Mono channel
|
|
50
|
+
this.debugCounter++;
|
|
51
|
+
|
|
52
|
+
// Debug: Log first callback and then every 100th
|
|
53
|
+
if (this.debugCounter === 1 || this.debugCounter % 100 === 0) {
|
|
54
|
+
let maxVal = -Infinity;
|
|
55
|
+
let minVal = Infinity;
|
|
56
|
+
let sum = 0;
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
59
|
+
const val = inputData[i];
|
|
60
|
+
if (val > maxVal) maxVal = val;
|
|
61
|
+
if (val < minVal) minVal = val;
|
|
62
|
+
sum += Math.abs(val);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const avg = sum / inputData.length;
|
|
66
|
+
|
|
67
|
+
// Only log if there's actual audio (not silence)
|
|
68
|
+
if (avg > 0.0001) {
|
|
69
|
+
this.port.postMessage({
|
|
70
|
+
type: 'debug',
|
|
71
|
+
data: {
|
|
72
|
+
counter: this.debugCounter,
|
|
73
|
+
min: minVal.toFixed(4),
|
|
74
|
+
max: maxVal.toFixed(4),
|
|
75
|
+
avg: avg.toFixed(6),
|
|
76
|
+
samples: inputData.length
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Downsample from native rate to 24kHz using simple decimation
|
|
83
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
84
|
+
this.sampleIndex++;
|
|
85
|
+
|
|
86
|
+
if (this.sampleIndex >= this.resampleRatio) {
|
|
87
|
+
this.sampleIndex -= this.resampleRatio;
|
|
88
|
+
this.resampleBuffer.push(inputData[i]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// When we have enough samples, send a chunk
|
|
93
|
+
while (this.resampleBuffer.length >= this.TARGET_BUFFER_SIZE) {
|
|
94
|
+
const chunk = this.resampleBuffer.splice(0, this.TARGET_BUFFER_SIZE);
|
|
95
|
+
|
|
96
|
+
// Convert Float32 [-1, 1] to Int16 [-32768, 32767]
|
|
97
|
+
const pcm16 = new Int16Array(this.TARGET_BUFFER_SIZE);
|
|
98
|
+
|
|
99
|
+
for (let j = 0; j < this.TARGET_BUFFER_SIZE; j++) {
|
|
100
|
+
// Clamp to [-1, 1] range
|
|
101
|
+
const clamped = Math.max(-1, Math.min(1, chunk[j]));
|
|
102
|
+
|
|
103
|
+
// Convert to 16-bit integer
|
|
104
|
+
pcm16[j] = clamped < 0
|
|
105
|
+
? clamped * 0x8000 // -32768
|
|
106
|
+
: clamped * 0x7FFF; // 32767
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Send PCM16 data to main thread
|
|
110
|
+
// Transfer ownership for zero-copy (performance optimization)
|
|
111
|
+
this.port.postMessage(
|
|
112
|
+
{
|
|
113
|
+
type: 'audio',
|
|
114
|
+
data: pcm16.buffer
|
|
115
|
+
},
|
|
116
|
+
[pcm16.buffer] // Transfer ownership
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Keep processor alive
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Register the processor
|
|
126
|
+
registerProcessor('pcm16-processor', PCM16Processor);
|