88 createRealtimeVoiceAgentTalkbackQueue ,
99 createRealtimeVoiceBridgeSession ,
1010 createRealtimeVoiceForcedConsultCoordinator ,
11+ createRealtimeVoiceOutputActivityTracker ,
1112 createRealtimeVoiceTurnContextTracker ,
1213 matchRealtimeVoiceActivationName ,
1314 matchRealtimeVoiceConsultQuestions ,
@@ -30,6 +31,7 @@ import {
3031 type RealtimeVoiceToolCallEvent ,
3132 type RealtimeVoiceForcedConsultCoordinator ,
3233 type RealtimeVoiceForcedConsultHandle ,
34+ type RealtimeVoiceOutputActivityTracker ,
3335 type RealtimeVoiceTurnContextHandle ,
3436 type RealtimeVoiceTurnContextTracker ,
3537 sortRealtimeVoiceActivationNames ,
@@ -376,15 +378,10 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
376378 ignoredContextTtlMs : DISCORD_REALTIME_IGNORED_WAKE_NAME_CONTEXT_TTL_MS ,
377379 } ,
378380 ) ;
379- private outputAudioTimestampMs = 0 ;
380- private outputAudioDiscordBytes = 0 ;
381- private outputAudioRealtimeBytes = 0 ;
382- private outputAudioChunks = 0 ;
383- private outputAudioStartedAt : number | undefined ;
381+ private readonly outputActivity : RealtimeVoiceOutputActivityTracker =
382+ createRealtimeVoiceOutputActivityTracker ( ) ;
384383 private outputPlaybackWatchdog : ReturnType < typeof setTimeout > | undefined ;
385- private outputStreamEnding = false ;
386384 private outputPacedBuffer : Buffer = Buffer . alloc ( 0 ) ;
387- private outputPlaybackStarted = false ;
388385 private realtimeProviderId : string | undefined ;
389386 private queuedExactSpeechMessages : string [ ] = [ ] ;
390387 private exactSpeechResponseActive = false ;
@@ -653,7 +650,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
653650 turn . inputChunks += 1 ;
654651 if ( turn . inputChunks === 1 ) {
655652 logger . info (
656- `discord voice: realtime input audio started guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } user=${ turn . context . userId } speaker=${ turn . context . speakerLabel } discordBytes=${ discordPcm48kStereo . length } realtimeBytes=${ realtimePcm . length } outputAudioMs=${ Math . floor ( this . outputAudioTimestampMs ) } outputActive=${ this . isOutputAudioActive ( ) } ` ,
653+ `discord voice: realtime input audio started guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } user=${ turn . context . userId } speaker=${ turn . context . speakerLabel } discordBytes=${ discordPcm48kStereo . length } realtimeBytes=${ realtimePcm . length } outputAudioMs=${ this . outputAudioMs ( ) } outputActive=${ this . isOutputAudioActive ( ) } ` ,
657654 ) ;
658655 }
659656 const outputActive = this . hasInterruptibleOutputAudio ( ) ;
@@ -663,7 +660,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
663660 `realtime barge-in from active speaker audio: guild ${ this . params . entry . guildId } channel ${ this . params . entry . channelId } user ${ turn . context . userId } ` ,
664661 ) ;
665662 logger . info (
666- `discord voice: realtime barge-in detected source=active-speaker-audio guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } user=${ turn . context . userId } speaker=${ turn . context . speakerLabel } outputAudioMs=${ Math . floor ( this . outputAudioTimestampMs ) } outputActive=${ this . isOutputAudioActive ( ) } discordBytes=${ discordPcm48kStereo . length } realtimeBytes=${ realtimePcm . length } ` ,
663+ `discord voice: realtime barge-in detected source=active-speaker-audio guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } user=${ turn . context . userId } speaker=${ turn . context . speakerLabel } outputAudioMs=${ this . outputAudioMs ( ) } outputActive=${ this . isOutputAudioActive ( ) } discordBytes=${ discordPcm48kStereo . length } realtimeBytes=${ realtimePcm . length } ` ,
667664 ) ;
668665 this . handleBargeIn ( "active-speaker-audio" ) ;
669666 }
@@ -681,12 +678,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
681678 const outputActive = this . hasInterruptibleOutputAudio ( ) ;
682679 if ( ! outputActive ) {
683680 logger . info (
684- `discord voice: realtime barge-in ignored reason=${ reason } outputActive=false guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } playbackChunks=${ this . outputAudioChunks } ` ,
681+ `discord voice: realtime barge-in ignored reason=${ reason } outputActive=false guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } playbackChunks=${ this . outputAudioChunks ( ) } ` ,
685682 ) ;
686683 return ;
687684 }
688685 logger . info (
689- `discord voice: realtime barge-in requested reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } outputAudioMs=${ Math . floor ( this . outputAudioTimestampMs ) } outputActive=${ this . isOutputAudioActive ( ) } playbackChunks=${ this . outputAudioChunks } ` ,
686+ `discord voice: realtime barge-in requested reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } outputAudioMs=${ this . outputAudioMs ( ) } outputActive=${ this . isOutputAudioActive ( ) } playbackChunks=${ this . outputAudioChunks ( ) } ` ,
690687 ) ;
691688 this . bridge ?. handleBargeIn ( { audioPlaybackActive : true } ) ;
692689 }
@@ -704,9 +701,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
704701
705702 private hasInterruptibleOutputAudio ( ) : boolean {
706703 this . syncOutputAudioTimestamp ( ) ;
707- return (
708- this . isOutputAudioActive ( ) || this . outputAudioChunks > 0 || this . outputAudioTimestampMs > 0
709- ) ;
704+ return this . outputActivity . isInterruptible ( this . isOutputStreamActive ( ) ) ;
710705 }
711706
712707 private get realtimeConfig ( ) : DiscordRealtimeVoiceConfig {
@@ -719,7 +714,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
719714 return ;
720715 }
721716 this . syncOutputAudioTimestamp ( ) ;
722- if ( this . outputStreamEnding ) {
717+ if ( this . outputActivity . snapshot ( ) . streamEnding ) {
723718 logVoiceVerbose (
724719 `realtime output audio ignored after stream ending: guild ${ this . params . entry . guildId } channel ${ this . params . entry . channelId } ` ,
725720 ) ;
@@ -729,13 +724,14 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
729724 if ( this . exactSpeechResponseActive ) {
730725 this . exactSpeechAudioStarted = true ;
731726 }
732- this . outputAudioDiscordBytes += discordPcm . length ;
733- this . outputAudioRealtimeBytes += realtimePcm24kMono . length ;
734- this . outputAudioChunks += 1 ;
735- this . outputAudioTimestampMs += pcm16MonoDurationMs (
736- realtimePcm24kMono ,
737- REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ . sampleRateHz ,
738- ) ;
727+ this . outputActivity . markAudio ( {
728+ audioMs : pcm16MonoDurationMs (
729+ realtimePcm24kMono ,
730+ REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ . sampleRateHz ,
731+ ) ,
732+ sourceAudioBytes : realtimePcm24kMono . length ,
733+ sinkAudioBytes : discordPcm . length ,
734+ } ) ;
739735 this . queueOutputAudio ( stream , discordPcm ) ;
740736 }
741737
@@ -746,7 +742,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
746742 const stream = new PassThrough ( { highWaterMark : DISCORD_RAW_PCM_FRAME_BYTES * 128 } ) ;
747743 this . outputStream = stream ;
748744 this . outputPacedBuffer = Buffer . alloc ( 0 ) ;
749- this . outputPlaybackStarted = false ;
745+ this . outputActivity . markStreamOpened ( ) ;
750746 stream . once ( "close" , ( ) => {
751747 if ( this . outputStream === stream ) {
752748 this . logOutputAudioStopped ( "stream-close" ) ;
@@ -759,7 +755,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
759755 }
760756
761757 private queueOutputAudio ( stream : PassThrough , discordPcm : Buffer ) : void {
762- if ( this . outputPlaybackStarted ) {
758+ if ( this . outputActivity . snapshot ( ) . playbackStarted ) {
763759 stream . write ( discordPcm ) ;
764760 return ;
765761 }
@@ -776,7 +772,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
776772 }
777773
778774 private startOutputPlayback ( stream : PassThrough ) : void {
779- if ( this . outputPlaybackStarted || stream . destroyed ) {
775+ if ( this . outputActivity . snapshot ( ) . playbackStarted || stream . destroyed ) {
780776 return ;
781777 }
782778 const voiceSdk = loadDiscordVoiceSdk ( ) ;
@@ -788,8 +784,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
788784 inputType : voiceSdk . StreamType . Raw ,
789785 } ) ;
790786 this . params . entry . player . play ( resource ) ;
791- this . outputPlaybackStarted = true ;
792- this . outputAudioStartedAt = Date . now ( ) ;
787+ this . outputActivity . markPlaybackStarted ( ) ;
793788 const realtimeConfig = this . realtimeConfig ;
794789 logger . info (
795790 `discord voice: realtime audio playback started guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } mode=${ this . params . mode } model=${ realtimeConfig ?. model ?? "provider-default" } voice=${ realtimeConfig ?. voice ?? "provider-default" } ` ,
@@ -807,7 +802,6 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
807802 this . logOutputAudioStopped ( reason ) ;
808803 this . outputStream = null ;
809804 this . outputPacedBuffer = Buffer . alloc ( 0 ) ;
810- this . outputPlaybackStarted = false ;
811805 this . resetOutputAudioStats ( ) ;
812806 stream ?. end ( ) ;
813807 stream ?. destroy ( ) ;
@@ -818,12 +812,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
818812 { playBuffered = true } : { playBuffered ?: boolean } = { } ,
819813 ) : void {
820814 const stream = this . outputStream ;
821- if ( ! stream || stream . destroyed || this . outputStreamEnding ) {
815+ if ( ! stream || stream . destroyed || this . outputActivity . snapshot ( ) . streamEnding ) {
822816 return ;
823817 }
824- this . outputStreamEnding = true ;
818+ this . outputActivity . markStreamEnding ( ) ;
825819 logger . info (
826- `discord voice: realtime audio playback finishing reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } audioMs=${ Math . floor ( this . outputAudioTimestampMs ) } chunks=${ this . outputAudioChunks } ` ,
820+ `discord voice: realtime audio playback finishing reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } audioMs=${ this . outputAudioMs ( ) } chunks=${ this . outputAudioChunks ( ) } ` ,
827821 ) ;
828822 if ( playBuffered ) {
829823 this . startOutputPlayback ( stream ) ;
@@ -839,14 +833,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
839833
840834 private scheduleOutputPlaybackWatchdog ( reason : string , stream : PassThrough ) : void {
841835 this . clearOutputPlaybackWatchdog ( ) ;
842- if ( ! this . outputAudioStartedAt || this . outputAudioTimestampMs <= 0 ) {
836+ const timeoutMs = this . outputActivity . playbackWatchdogDelayMs ( {
837+ marginMs : DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS ,
838+ } ) ;
839+ if ( timeoutMs === undefined ) {
843840 return ;
844841 }
845- const elapsedMs = Date . now ( ) - this . outputAudioStartedAt ;
846- const timeoutMs = Math . max (
847- 1_000 ,
848- this . outputAudioTimestampMs - elapsedMs + DISCORD_REALTIME_OUTPUT_PLAYBACK_WATCHDOG_MARGIN_MS ,
849- ) ;
850842 this . outputPlaybackWatchdog = setTimeout ( ( ) => {
851843 this . outputPlaybackWatchdog = undefined ;
852844 if ( this . outputStream && this . outputStream !== stream ) {
@@ -857,7 +849,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
857849 return ;
858850 }
859851 logger . warn (
860- `discord voice: realtime audio playback watchdog fired reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } audioMs=${ Math . floor ( this . outputAudioTimestampMs ) } elapsedMs=${ this . outputAudioStartedAt ? Date . now ( ) - this . outputAudioStartedAt : 0 } ` ,
852+ `discord voice: realtime audio playback watchdog fired reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } audioMs=${ this . outputAudioMs ( ) } elapsedMs=${ this . outputActivity . elapsedPlaybackMs ( ) } ` ,
861853 ) ;
862854 this . clearOutputAudio ( "playback-watchdog" ) ;
863855 this . completeExactSpeechResponse ( "playback-watchdog" ) ;
@@ -879,7 +871,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
879871 if ( this . exactSpeechResponseActive || this . hasInterruptibleOutputAudio ( ) ) {
880872 this . queuedExactSpeechMessages . push ( text ) ;
881873 logger . info (
882- `discord voice: realtime exact speech queued guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } queued=${ this . queuedExactSpeechMessages . length } outputAudioMs=${ Math . floor ( this . outputAudioTimestampMs ) } outputActive=${ this . isOutputAudioActive ( ) } ` ,
874+ `discord voice: realtime exact speech queued guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } queued=${ this . queuedExactSpeechMessages . length } outputAudioMs=${ this . outputAudioMs ( ) } outputActive=${ this . isOutputAudioActive ( ) } ` ,
883875 ) ;
884876 return ;
885877 }
@@ -986,11 +978,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
986978 }
987979
988980 private logOutputAudioStopped ( reason : string ) : void {
989- const audioMs = Math . floor ( this . outputAudioTimestampMs ) ;
990- const chunks = this . outputAudioChunks ;
991- const discordBytes = this . outputAudioDiscordBytes ;
992- const realtimeBytes = this . outputAudioRealtimeBytes ;
993- const elapsedMs = this . outputAudioStartedAt ? Date . now ( ) - this . outputAudioStartedAt : 0 ;
981+ const activity = this . outputActivity . snapshot ( ) ;
982+ const audioMs = Math . floor ( activity . audioMs ) ;
983+ const chunks = activity . chunks ;
984+ const discordBytes = activity . sinkAudioBytes ;
985+ const realtimeBytes = activity . sourceAudioBytes ;
986+ const elapsedMs = this . outputActivity . elapsedPlaybackMs ( ) ;
994987 if ( this . outputStream || chunks > 0 || audioMs > 0 ) {
995988 logger . info (
996989 `discord voice: realtime audio playback stopped reason=${ reason } guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } audioMs=${ audioMs } elapsedMs=${ elapsedMs } chunks=${ chunks } discordBytes=${ discordBytes } realtimeBytes=${ realtimeBytes } ` ,
@@ -999,22 +992,28 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
999992 }
1000993
1001994 private resetOutputAudioStats ( ) : void {
1002- this . outputAudioTimestampMs = 0 ;
1003- this . outputAudioDiscordBytes = 0 ;
1004- this . outputAudioRealtimeBytes = 0 ;
1005- this . outputAudioChunks = 0 ;
1006- this . outputAudioStartedAt = undefined ;
1007- this . outputStreamEnding = false ;
1008995 this . outputPacedBuffer = Buffer . alloc ( 0 ) ;
1009- this . outputPlaybackStarted = false ;
996+ this . outputActivity . reset ( ) ;
1010997 }
1011998
1012999 private syncOutputAudioTimestamp ( ) : void {
1013- this . bridge ?. setMediaTimestamp ( Math . floor ( this . outputAudioTimestampMs ) ) ;
1000+ this . bridge ?. setMediaTimestamp ( this . outputAudioMs ( ) ) ;
1001+ }
1002+
1003+ private outputAudioMs ( ) : number {
1004+ return Math . floor ( this . outputActivity . snapshot ( ) . audioMs ) ;
1005+ }
1006+
1007+ private outputAudioChunks ( ) : number {
1008+ return this . outputActivity . snapshot ( ) . chunks ;
1009+ }
1010+
1011+ private isOutputStreamActive ( ) : boolean {
1012+ return Boolean ( this . outputStream && ! this . outputStream . destroyed ) ;
10141013 }
10151014
10161015 private isOutputAudioActive ( ) : boolean {
1017- return Boolean ( this . outputStream && ! this . outputStream . destroyed ) || this . outputAudioChunks > 0 ;
1016+ return this . outputActivity . isActive ( this . isOutputStreamActive ( ) ) ;
10181017 }
10191018
10201019 private logSpeakerTurnClosed ( turn : PendingSpeakerTurn ) : void {
@@ -1385,7 +1384,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
13851384 ) ;
13861385 if ( this . hasInterruptibleOutputAudio ( ) ) {
13871386 logger . info (
1388- `discord voice: realtime forced agent consult preserving active playback guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } outputAudioMs=${ Math . floor ( this . outputAudioTimestampMs ) } outputActive=${ this . isOutputAudioActive ( ) } playbackChunks=${ this . outputAudioChunks } ` ,
1387+ `discord voice: realtime forced agent consult preserving active playback guild=${ this . params . entry . guildId } channel=${ this . params . entry . channelId } outputAudioMs=${ this . outputAudioMs ( ) } outputActive=${ this . isOutputAudioActive ( ) } playbackChunks=${ this . outputAudioChunks ( ) } ` ,
13891388 ) ;
13901389 }
13911390 state . handledByForcedPlayback = true ;
0 commit comments