@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.
@@ -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",
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);