diff --git a/__tests__/html2/speechToSpeech/mute.unmute.html b/__tests__/html2/speechToSpeech/mute.unmute.html index 396060f495..4759fa6caa 100644 --- a/__tests__/html2/speechToSpeech/mute.unmute.html +++ b/__tests__/html2/speechToSpeech/mute.unmute.html @@ -16,10 +16,11 @@ Test: Mute/Unmute functionality for Speech-to-Speech This test validates: - 1. Listening state can transition to muted and back to listening - 2. Other states (idle) cannot transition to muted - 3. Muted chunks contain all zeros (silent audio) - 4. Uses useVoiceRecordingMuted hook via Composer pattern for mute/unmute control + 1. Mute is allowed from all state except idle + 2. When muted during listening, chunks contain all zeros (silent audio) + 3. When unmuted, chunks contain real audio + 4. Mute resets to false when recording stops + 5. Uses useVoiceRecordingMuted hook for mute/unmute control --> diff --git a/packages/api/src/hooks/useVoiceRecordingMuted.ts b/packages/api/src/hooks/useVoiceRecordingMuted.ts index a562a84204..e291b5d772 100644 --- a/packages/api/src/hooks/useVoiceRecordingMuted.ts +++ b/packages/api/src/hooks/useVoiceRecordingMuted.ts @@ -4,10 +4,14 @@ import { useDispatch, useSelector } from './internal/WebChatReduxContext'; /** * Hook to get and set voice recording mute state in speech-to-speech mode. + * + * Mute is independent of voice state - it can be toggled at any time. + * When muted, silent audio chunks are sent instead of real audio. + * Mute resets to false when recording stops. */ export default function useVoiceRecordingMuted(): readonly [boolean, (muted: boolean) => void] { const dispatch = useDispatch(); - const value = useSelector(({ voice }) => voice.voiceState === 'muted'); + const value = useSelector(({ voice }) => voice.microphoneMuted); const setter = useCallback( (muted: boolean) => { diff --git a/packages/api/src/providers/SpeechToSpeech/private/VoiceRecorderBridge.tsx b/packages/api/src/providers/SpeechToSpeech/private/VoiceRecorderBridge.tsx index bd2e351ab7..1c63de121a 100644 --- a/packages/api/src/providers/SpeechToSpeech/private/VoiceRecorderBridge.tsx +++ b/packages/api/src/providers/SpeechToSpeech/private/VoiceRecorderBridge.tsx @@ -1,6 +1,7 @@ import { useEffect, useCallback } from 'react'; import { useRecorder } from './useRecorder'; import usePostVoiceActivity from '../../../hooks/internal/usePostVoiceActivity'; +import useVoiceRecordingMuted from '../../../hooks/useVoiceRecordingMuted'; import useVoiceState from '../../../hooks/useVoiceState'; /** @@ -8,10 +9,10 @@ import useVoiceState from '../../../hooks/useVoiceState'; * with the actual microphone recording functionality. */ export function VoiceRecorderBridge(): null { + const [muted] = useVoiceRecordingMuted(); const [voiceState] = useVoiceState(); const postVoiceActivity = usePostVoiceActivity(); - const muted = voiceState === 'muted'; // Derive recording state from voiceState - recording is active when not idle const recording = voiceState !== 'idle'; @@ -32,17 +33,17 @@ export function VoiceRecorderBridge(): null { const { mute, record } = useRecorder(handleAudioChunk); - useEffect(() => { - if (muted) { - return mute(); - } - }, [muted, mute]); - useEffect(() => { if (recording) { return record(); } }, [record, recording]); + useEffect(() => { + if (muted) { + return mute(); + } + }, [muted, mute]); + return null; } diff --git a/packages/core/src/actions/setVoiceState.ts b/packages/core/src/actions/setVoiceState.ts index 70feef25c3..53fc12b7c2 100644 --- a/packages/core/src/actions/setVoiceState.ts +++ b/packages/core/src/actions/setVoiceState.ts @@ -1,6 +1,6 @@ const VOICE_SET_STATE = 'WEB_CHAT/VOICE_SET_STATE' as const; -type VoiceState = 'idle' | 'listening' | 'muted' | 'user_speaking' | 'processing' | 'bot_speaking'; +type VoiceState = 'idle' | 'listening' | 'user_speaking' | 'processing' | 'bot_speaking'; type VoiceSetStateAction = { type: typeof VOICE_SET_STATE; diff --git a/packages/core/src/reducers/voiceActivity.ts b/packages/core/src/reducers/voiceActivity.ts index 727394ea77..2f948cc43b 100644 --- a/packages/core/src/reducers/voiceActivity.ts +++ b/packages/core/src/reducers/voiceActivity.ts @@ -24,11 +24,13 @@ type VoiceActivityActions = | VoiceUnregisterHandlerAction; interface VoiceActivityState { + microphoneMuted: boolean; voiceState: VoiceState; voiceHandlers: Map; } const DEFAULT_STATE: VoiceActivityState = { + microphoneMuted: false, voiceState: 'idle', voiceHandlers: new Map() }; @@ -39,15 +41,15 @@ export default function voiceActivity( ): VoiceActivityState { switch (action.type) { case VOICE_MUTE_RECORDING: - // Only allow muting when in listening state - if (state.voiceState !== 'listening') { - console.warn(`botframework-webchat: Cannot mute from "${state.voiceState}" state, must be "listening"`); + // Only allow muting when in recording state + if (state.voiceState === 'idle') { + console.warn(`botframework-webchat: Cannot mute from "${state.voiceState}" state, must be in recording state.`); return state; } return { ...state, - voiceState: 'muted' + microphoneMuted: true }; case VOICE_REGISTER_HANDLER: { @@ -87,17 +89,14 @@ export default function voiceActivity( case VOICE_STOP_RECORDING: return { ...state, + microphoneMuted: false, voiceState: 'idle' }; case VOICE_UNMUTE_RECORDING: - if (state.voiceState !== 'muted') { - console.warn(`botframework-webchat: Should not transit from "${state.voiceState}" to "listening"`); - } - return { ...state, - voiceState: 'listening' + microphoneMuted: false }; default: