byt-lingxiao-ai 0.3.28 → 0.3.30

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.
@@ -1,20 +1,94 @@
1
1
  export default {
2
2
  data() {
3
3
  return {
4
+ audioContext: null,
5
+ workletNode: null,
6
+ mediaStreamSource: null,
4
7
  isRecording: false,
5
8
  isMicAvailable: false,
6
- audioContext: null,
7
- microphone: null,
8
- processor: null,
9
- audioBuffer: new Float32Array(0)
10
- }
9
+ audioData: new Float32Array(128)
10
+ };
11
11
  },
12
12
  methods: {
13
+ async initAudioWorklet() {
14
+ if (this.audioContext) return;
15
+
16
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
17
+ this.audioContext = new AudioContext({ sampleRate: this.SAMPLE_RATE });
18
+
19
+ const processorCode = `
20
+ class RecorderProcessor extends AudioWorkletProcessor {
21
+ constructor() {
22
+ super();
23
+ this.buffer = [];
24
+ this.frameSize = ${this.FRAME_SIZE};
25
+ }
26
+
27
+ process(inputs) {
28
+ const input = inputs[0];
29
+ if (!input || !input[0]) return true;
30
+
31
+ const channel = input[0];
32
+
33
+ for (let i = 0; i < channel.length; i++) {
34
+ this.buffer.push(channel[i]);
35
+ }
36
+
37
+ while (this.buffer.length >= this.frameSize) {
38
+ const frame = this.buffer.splice(0, this.frameSize);
39
+
40
+ // PCM
41
+ const pcm = this.floatTo16BitPCM(frame);
42
+ this.port.postMessage({
43
+ type: 'audio',
44
+ payload: pcm
45
+ });
46
+
47
+ // Visual(下采样)
48
+ this.port.postMessage({
49
+ type: 'visual',
50
+ payload: new Float32Array(frame.slice(0, 128))
51
+ });
52
+ }
53
+ return true;
54
+ }
55
+
56
+ floatTo16BitPCM(float32Array) {
57
+ const pcm = new Int16Array(float32Array.length);
58
+ for (let i = 0; i < float32Array.length; i++) {
59
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
60
+ pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
61
+ }
62
+ return pcm.buffer;
63
+ }
64
+ }
65
+
66
+ registerProcessor('recorder-processor', RecorderProcessor);
67
+ `;
68
+
69
+ const blob = new Blob([processorCode], {
70
+ type: 'application/javascript'
71
+ });
72
+ const moduleUrl = URL.createObjectURL(blob);
73
+
74
+ await this.audioContext.audioWorklet.addModule(moduleUrl);
75
+
76
+ this.workletNode = new AudioWorkletNode(
77
+ this.audioContext,
78
+ 'recorder-processor'
79
+ );
80
+
81
+ this.workletNode.port.onmessage = this.handleWorkletMessage;
82
+ },
13
83
  async initAudio() {
14
84
  if (this.isRecording) return;
85
+
15
86
  try {
16
87
  this.isMicAvailable = true;
17
- const stream = await navigator.mediaDevices.getUserMedia({
88
+
89
+ await this.initAudioWorklet();
90
+
91
+ this.stream = await navigator.mediaDevices.getUserMedia({
18
92
  audio: {
19
93
  sampleRate: this.SAMPLE_RATE,
20
94
  channelCount: 1,
@@ -24,102 +98,85 @@ export default {
24
98
  }
25
99
  });
26
100
 
27
- this.audioContext = new AudioContext({ sampleRate: this.SAMPLE_RATE });
28
- this.microphone = this.audioContext.createMediaStreamSource(stream);
29
- this.processor = this.audioContext.createScriptProcessor(this.FRAME_SIZE, 1, 1);
30
- this.processor.onaudioprocess = this.processAudio;
31
-
32
- this.microphone.connect(this.processor);
33
- this.processor.connect(this.audioContext.destination);
101
+ this.mediaStreamSource =
102
+ this.audioContext.createMediaStreamSource(this.stream);
103
+
104
+ this.mediaStreamSource.connect(this.workletNode);
105
+
106
+ if (this.audioContext.state === 'suspended') {
107
+ await this.audioContext.resume();
108
+ }
34
109
 
35
110
  this.isRecording = true;
36
- console.log(`录音中 (采样率: ${this.audioContext.sampleRate}Hz)`);
37
- } catch (error) {
38
- console.error("音频初始化失败:", error);
111
+ console.log(`AudioWorklet 录音中 (${this.SAMPLE_RATE}Hz)`);
112
+
113
+ } catch (e) {
114
+ console.error('音频初始化失败:', e);
39
115
  this.isRecording = false;
40
116
  this.isMicAvailable = false;
41
117
  }
42
118
  },
119
+ handleWorkletMessage(e) {
120
+ const { type, payload } = e.data;
43
121
 
44
- processAudio(event) {
45
- if (!this.isRecording) return;
46
- const inputData = event.inputBuffer.getChannelData(0);
47
-
48
- const tempBuffer = new Float32Array(this.audioBuffer.length + inputData.length);
49
- tempBuffer.set(this.audioBuffer, 0);
50
- tempBuffer.set(inputData, this.audioBuffer.length);
51
- this.audioBuffer = tempBuffer;
52
-
53
- while (this.audioBuffer.length >= this.FRAME_SIZE) {
54
- const frame = this.audioBuffer.slice(0, this.FRAME_SIZE);
55
- this.audioBuffer = this.audioBuffer.slice(this.FRAME_SIZE);
56
- const pcmData = this.floatTo16BitPCM(frame);
57
-
58
- if (this.ws && this.ws.readyState === this.ws.OPEN) {
59
- this.ws.send(pcmData);
122
+ if (type === 'audio') {
123
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
124
+ this.ws.send(payload);
60
125
  }
61
126
  }
62
127
  },
128
+ stopRecording() {
129
+ if (!this.isRecording) return;
63
130
 
64
- floatTo16BitPCM(input) {
65
- const output = new Int16Array(input.length);
66
- for (let i = 0; i < input.length; i++) {
67
- const s = Math.max(-1, Math.min(1, input[i]));
68
- output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
131
+ if (this.mediaStreamSource) this.mediaStreamSource.disconnect();
132
+ if (this.workletNode) this.workletNode.disconnect();
133
+
134
+ if (this.stream) {
135
+ this.stream.getTracks().forEach(t => t.stop());
69
136
  }
70
- return output.buffer;
71
- },
72
137
 
73
- stopRecording() {
74
- if (!this.isRecording) return;
75
- if (this.microphone) this.microphone.disconnect();
76
- if (this.processor) this.processor.disconnect();
77
- if (this.analyser) this.analyser.disconnect();
78
138
  if (this.audioContext) {
79
- this.audioContext.close().catch(e => {
80
- console.error("关闭音频上下文失败:", e);
81
- });
139
+ this.audioContext.close();
140
+ this.audioContext = null;
82
141
  }
142
+
83
143
  this.isRecording = false;
144
+ console.log('录音已停止');
84
145
  },
146
+ handleWebSocketMessage(data) {
147
+ if (data.type === 'detection') {
148
+ if (this.robotStatus === 'speaking') {
149
+ this.robotStatus = 'waiting';
150
+ }
151
+ this.avaterStatus = 'normal';
152
+ this.startTime = Date.now();
153
+ }
154
+
155
+ if (data.type === 'Collecting') {
156
+ this.avaterStatus = 'thinking';
157
+ }
85
158
 
159
+ if (data.type === 'command') {
160
+ if (this.startTime) {
161
+ console.log(
162
+ `[Latency] ${Date.now() - this.startTime}ms`
163
+ );
164
+ this.startTime = null;
165
+ }
166
+ this.analyzeAudioCommand(data.category);
167
+ }
168
+ },
86
169
  play() {
87
- this.robotStatus = 'speaking';
88
- this.$refs.audioPlayer.play();
170
+ this.$refs.audioPlayer?.play();
89
171
  },
90
-
91
172
  pause() {
92
- console.log('暂停播放');
93
- this.robotStatus = 'waiting';
94
- this.$refs.audioPlayer.pause();
173
+ this.$refs.audioPlayer?.pause();
95
174
  },
96
-
97
175
  stop() {
98
- this.robotStatus = 'leaving';
99
- this.$refs.audioPlayer.pause();
100
- this.$refs.audioPlayer.currentTime = 0;
101
- this.jumpedTimePoints.clear();
102
- },
103
-
104
- analyzeAudioCommand(command) {
105
- console.log('分析音频命令:', command);
106
- if (command === 'C5') {
107
- this.robotStatus = 'entering';
108
- setTimeout(() => {
109
- this.robotStatus = 'speaking';
110
- this.play();
111
- }, 3000);
112
- } else if (command === 'C8') {
113
- this.robotStatus = 'speaking';
114
- this.play();
115
- } else if (command === 'C7') {
116
- this.robotStatus = 'waiting';
117
- this.pause();
118
- } else if (command === 'C6') {
119
- this.robotStatus = 'leaving';
120
- this.avaterStatus = 'normal';
121
- this.stop();
122
- }
176
+ const p = this.$refs.audioPlayer;
177
+ if (!p) return;
178
+ p.pause();
179
+ p.currentTime = 0;
123
180
  }
124
181
  }
125
- }
182
+ };
@@ -12,11 +12,7 @@ export default {
12
12
  methods: {
13
13
  initWebSocket() {
14
14
  try {
15
- // this.ws = new WebSocket('ws://10.2.233.41:9999');
16
- // 测试
17
- // console.log('WS_URL:', WS_URL)
18
15
  this.ws = new WebSocket(WS_URL);
19
- // this.ws = new WebSocket('ws://192.168.8.87/audio/ws/');
20
16
  this.ws.binaryType = 'arraybuffer';
21
17
 
22
18
  this.ws.onopen = async () => {
@@ -74,7 +70,6 @@ export default {
74
70
  console.error('WebSocket连接失败:', error);
75
71
  }
76
72
  },
77
-
78
73
  handleWebSocketMessage(data) {
79
74
  if (data.type === 'detection') {
80
75
  console.log('检测到唤醒词');
@@ -95,13 +90,15 @@ export default {
95
90
  console.log('状态: 处理中');
96
91
 
97
92
  // 性能检测终点
98
- if (this.startTime) {
99
- const latency = Date.now() - this.startTime;
100
- console.log(`[Latency] 完整命令处理耗时: ${latency}ms`); // 记录端到端延迟
101
- this.startTime = null;
102
- }
93
+ // if (this.startTime) {
94
+ // const latency = Date.now() - this.startTime;
95
+ // console.log(`[Latency] 完整命令处理耗时: ${latency}ms`); // 记录端到端延迟
96
+ // this.startTime = null;
97
+ // }
103
98
 
104
99
  this.analyzeAudioCommand(data.category);
100
+ } else if (data.type === 'voice') {
101
+ this.analyzeVoiceCommand(data.category);
105
102
  } else {
106
103
  console.log('状态: 其他');
107
104
  this.avaterStatus = 'normal';