Skip to content

Commit af50d49

Browse files
remaining review comment fixed
1 parent 41de921 commit af50d49

8 files changed

Lines changed: 64 additions & 73 deletions

File tree

packages/api/src/hooks/internal/useSetVoiceState.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { setVoiceState, type VoiceState } from 'botframework-webchat-core';
2+
import { useCallback } from 'react';
3+
import { useDispatch, useSelector } from './WebChatReduxContext';
4+
5+
/**
6+
* Internal hook to set the voice state.
7+
*/
8+
export default function useVoiceStateWritable(): readonly [VoiceState, (state: VoiceState) => void] {
9+
const dispatch = useDispatch();
10+
const setter = useCallback(
11+
(state: VoiceState) => {
12+
dispatch(setVoiceState(state));
13+
},
14+
[dispatch]
15+
);
16+
const value = useSelector(({ voice }) => voice.voiceState);
17+
return Object.freeze([value, setter]);
18+
}
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { VoiceState } from 'botframework-webchat-core';
2-
import { useSelector } from './internal/WebChatReduxContext';
2+
import useVoiceStateWritable from './internal/useVoiceStateWritable';
33

44
/**
55
* Hook to get the voice state.
@@ -10,8 +10,6 @@ import { useSelector } from './internal/WebChatReduxContext';
1010
* - 'processing': User finished speaking, server is processing
1111
* - 'bot_speaking': Bot is speaking (audio playback)
1212
*/
13-
export default function useVoiceState(): [VoiceState] {
14-
return [useSelector(({ voice }) => voice.voiceState)];
13+
export default function useVoiceState(): readonly [VoiceState] {
14+
return Object.freeze([useVoiceStateWritable()[0]]);
1515
}
16-
17-
export type { VoiceState };

packages/api/src/providers/SpeechToSpeech/private/VoiceHandlerBridge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const VoiceHandlerBridge = () => {
1818
return;
1919
}
2020
return registerVoiceHandler({ queueAudio, stopAllAudio });
21-
}, [registerVoiceHandler, queueAudio, stopAllAudio, showMicrophoneButton]);
21+
}, [registerVoiceHandler, queueAudio, showMicrophoneButton, stopAllAudio]);
2222

2323
return null;
2424
};

packages/api/src/providers/SpeechToSpeech/private/useAudioPlayer.spec.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ import { useAudioPlayer } from './useAudioPlayer';
99
// Mock setVoiceState function
1010
const mockSetVoiceState = jest.fn();
1111

12-
// Mock useSetVoiceState hook
13-
jest.mock('../../../hooks/internal/useSetVoiceState', () => ({
12+
// Mock useVoiceStateWritable hook - returns [state, setVoiceState] array
13+
jest.mock('../../../hooks/internal/useVoiceStateWritable', () => ({
1414
__esModule: true,
15-
default: jest.fn(() => mockSetVoiceState)
15+
default: jest.fn(() => [undefined, mockSetVoiceState])
1616
}));
1717

1818
// Mock AudioContext and related APIs
1919
const mockAudioContext = {
20-
sampleRate: 24000,
20+
close: jest.fn().mockResolvedValue(undefined),
21+
createBuffer: jest.fn(),
22+
createBufferSource: jest.fn(),
2123
currentTime: 0,
2224
destination: {},
23-
state: 'running',
2425
resume: jest.fn().mockResolvedValue(undefined),
25-
close: jest.fn().mockResolvedValue(undefined),
26-
createBuffer: jest.fn(),
27-
createBufferSource: jest.fn()
26+
sampleRate: 24000,
27+
state: 'running'
2828
};
2929

3030
const mockAudioBuffer = {
@@ -36,10 +36,10 @@ const mockAudioBuffer = {
3636
const createMockBufferSource = () => ({
3737
buffer: null as typeof mockAudioBuffer | null,
3838
connect: jest.fn(),
39-
start: jest.fn(),
40-
stop: jest.fn(),
4139
disconnect: jest.fn(),
42-
onended: null as (() => void) | null
40+
onended: null as (() => void) | null,
41+
start: jest.fn(),
42+
stop: jest.fn()
4343
});
4444

4545
// Track all created buffer sources for assertions

packages/api/src/providers/SpeechToSpeech/private/useAudioPlayer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { useRef, useCallback, useMemo } from 'react';
2-
import useSetVoiceState from '../../../hooks/internal/useSetVoiceState';
2+
import useVoiceStateWritable from '../../../hooks/internal/useVoiceStateWritable';
33

44
const DEFAULT_SAMPLE_RATE = 24000;
55
const INT16_SCALE = 32768;
66

77
export function useAudioPlayer() {
88
const audioCtxRef = useRef<AudioContext | undefined>(undefined);
9-
const nextPlayTimeRef = useRef(0);
109
const lastSourceRef = useRef<AudioBufferSourceNode | undefined>(undefined);
11-
const setVoiceState = useSetVoiceState();
10+
const nextPlayTimeRef = useRef(0);
11+
const [, setVoiceState] = useVoiceStateWritable();
1212

1313
const queueAudio = useCallback(
1414
async (base64: string) => {

packages/api/src/providers/SpeechToSpeech/private/useRecorder.spec.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ const mockMediaDevices = {
2020
};
2121

2222
const mockWorkletPort = {
23-
postMessage: jest.fn(),
24-
onmessage: null as ((event: { data: unknown }) => void) | null
23+
onmessage: null as ((event: { data: unknown }) => void) | null,
24+
postMessage: jest.fn()
2525
};
2626

2727
const mockWorkletNode = {
@@ -31,15 +31,15 @@ const mockWorkletNode = {
3131
};
3232

3333
const mockAudioContext = {
34-
state: 'running',
35-
resume: jest.fn().mockResolvedValue(undefined),
34+
audioWorklet: {
35+
addModule: jest.fn().mockResolvedValue(undefined)
36+
},
3637
createMediaStreamSource: jest.fn(() => ({
3738
connect: jest.fn()
3839
})),
3940
destination: {},
40-
audioWorklet: {
41-
addModule: jest.fn().mockResolvedValue(undefined)
42-
}
41+
resume: jest.fn().mockResolvedValue(undefined),
42+
state: 'running'
4343
};
4444

4545
// --- Global Mocks Setup ---
@@ -206,8 +206,8 @@ describe('useRecorder', () => {
206206
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({
207207
audio: {
208208
channelCount: 1,
209-
sampleRate: 24000,
210-
echoCancellation: true
209+
echoCancellation: true,
210+
sampleRate: 24000
211211
}
212212
});
213213
});

packages/api/src/providers/SpeechToSpeech/private/useRecorder.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,43 @@ import usePonyfill from '../../Ponyfill/usePonyfill';
55
// adding reference of worker does not work
66
declare class AudioWorkletProcessor {
77
readonly port: MessagePort;
8+
recording: boolean;
9+
buffer: number[];
10+
bufferSize: number;
811
constructor(options?: AudioWorkletNodeOptions);
912
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean;
1013
}
1114
declare function registerProcessor(name: string, processorCtor: typeof AudioWorkletProcessor): void;
1215

1316
/**
1417
* CSP Compliant: check __tests__/html2/speechToSpeech/csp.recording.html for CSP compliance tests.
18+
* NOTE: This code is stringified and run in an AudioWorklet context, so it must be plain JavaScript
19+
* without any TypeScript annotations that could be transformed by the compiler.
1520
*/
1621
const audioProcessorCode = `(${function () {
17-
type RecorderState = { recording: boolean; buffer: number[]; bufferSize: number };
1822
class AudioRecorderProcessor extends AudioWorkletProcessor {
1923
constructor(options: AudioWorkletNodeOptions) {
2024
super();
21-
const state: RecorderState = {
22-
recording: false,
23-
buffer: [],
24-
bufferSize: options.processorOptions.bufferSize
25-
};
26-
Object.assign(this, state);
25+
this.buffer = [];
26+
this.bufferSize = options.processorOptions.bufferSize;
27+
this.recording = false;
2728
28-
this.port.onmessage = (e: MessageEvent) => {
29-
const state = this as unknown as RecorderState;
29+
this.port.onmessage = e => {
3030
if (e.data.command === 'START') {
31-
state.recording = true;
31+
this.recording = true;
3232
} else if (e.data.command === 'STOP') {
33-
state.recording = false;
34-
state.buffer = [];
33+
this.recording = false;
34+
this.buffer = [];
3535
}
3636
};
3737
}
3838
39-
sendBuffer() {
40-
const { buffer, bufferSize } = this as unknown as RecorderState;
41-
while (buffer.length >= bufferSize) {
42-
const chunk = buffer.splice(0, bufferSize);
43-
this.port.postMessage({ eventType: 'audio', audioData: new Float32Array(chunk) });
44-
}
45-
}
46-
4739
process(inputs: Float32Array[][]) {
48-
const state = this as unknown as RecorderState;
49-
if (inputs[0]?.length && state.recording) {
50-
state.buffer.push(...inputs[0][0]);
51-
if (state.buffer.length >= state.bufferSize) {
52-
this.sendBuffer();
40+
if (inputs[0] && inputs[0].length && this.recording) {
41+
this.buffer.push(...inputs[0][0]);
42+
while (this.buffer.length >= this.bufferSize) {
43+
const chunk = this.buffer.splice(0, this.bufferSize);
44+
this.port.postMessage({ eventType: 'audio', audioData: new Float32Array(chunk) });
5345
}
5446
}
5547
return true;
@@ -109,8 +101,8 @@ export function useRecorder(onAudioChunk: (base64: string, timestamp: string) =>
109101
const stream = await navigator.mediaDevices.getUserMedia({
110102
audio: {
111103
channelCount: 1,
112-
sampleRate: DEFAULT_SAMPLE_RATE,
113-
echoCancellation: true
104+
echoCancellation: true,
105+
sampleRate: DEFAULT_SAMPLE_RATE
114106
}
115107
});
116108
streamRef.current = stream;

0 commit comments

Comments
 (0)