@@ -8,40 +8,41 @@ namespace Azure.AI.VoiceLive.Samples;
8
8
9
9
/// <summary>
10
10
/// Handles real-time audio capture and playback for the voice assistant.
11
- ///
11
+ /// </summary>
12
+ /// <remarks>
12
13
/// Threading Architecture:
13
14
/// - Main thread: Event loop and UI
14
15
/// - Capture thread: NAudio input stream reading
15
16
/// - Send thread: Async audio data transmission to VoiceLive
16
17
/// - Playback thread: NAudio output stream writing
17
- /// </summary >
18
+ /// </remarks >
18
19
public class AudioProcessor : IDisposable
19
20
{
20
21
private readonly VoiceLiveSession _session ;
21
22
private readonly ILogger < AudioProcessor > _logger ;
22
-
23
+
23
24
// Audio configuration - PCM16, 24kHz, mono as specified
24
25
private const int SampleRate = 24000 ;
25
26
private const int Channels = 1 ;
26
27
private const int BitsPerSample = 16 ;
27
-
28
+
28
29
// NAudio components
29
30
private WaveInEvent ? _waveIn ;
30
31
private WaveOutEvent ? _waveOut ;
31
32
private BufferedWaveProvider ? _playbackBuffer ;
32
-
33
+
33
34
// Audio capture and playback state
34
35
private bool _isCapturing ;
35
36
private bool _isPlaying ;
36
-
37
+
37
38
// Audio streaming channels
38
39
private readonly Channel < byte [ ] > _audioSendChannel ;
39
40
private readonly Channel < byte [ ] > _audioPlaybackChannel ;
40
41
private readonly ChannelWriter < byte [ ] > _audioSendWriter ;
41
42
private readonly ChannelReader < byte [ ] > _audioSendReader ;
42
43
private readonly ChannelWriter < byte [ ] > _audioPlaybackWriter ;
43
44
private readonly ChannelReader < byte [ ] > _audioPlaybackReader ;
44
-
45
+
45
46
// Background tasks
46
47
private Task ? _audioSendTask ;
47
48
private Task ? _audioPlaybackTask ;
@@ -57,44 +58,45 @@ public AudioProcessor(VoiceLiveSession session, ILogger<AudioProcessor> logger)
57
58
{
58
59
_session = session ?? throw new ArgumentNullException ( nameof ( session ) ) ;
59
60
_logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
60
-
61
+
61
62
// Create unbounded channels for audio data
62
63
_audioSendChannel = Channel . CreateUnbounded < byte [ ] > ( ) ;
63
64
_audioSendWriter = _audioSendChannel . Writer ;
64
65
_audioSendReader = _audioSendChannel . Reader ;
65
-
66
+
66
67
_audioPlaybackChannel = Channel . CreateUnbounded < byte [ ] > ( ) ;
67
68
_audioPlaybackWriter = _audioPlaybackChannel . Writer ;
68
69
_audioPlaybackReader = _audioPlaybackChannel . Reader ;
69
-
70
+
70
71
_cancellationTokenSource = new CancellationTokenSource ( ) ;
72
+ _playbackCancellationTokenSource = new CancellationTokenSource ( ) ;
71
73
72
74
_logger . LogInformation ( "AudioProcessor initialized with {SampleRate}Hz PCM16 mono audio" , SampleRate ) ;
73
75
}
74
-
76
+
75
77
/// <summary>
76
78
/// Start capturing audio from microphone.
77
79
/// </summary>
78
80
public Task StartCaptureAsync ( )
79
81
{
80
82
if ( _isCapturing )
81
83
return Task . CompletedTask ;
82
-
84
+
83
85
_isCapturing = true ;
84
-
86
+
85
87
try
86
88
{
87
89
_waveIn = new WaveInEvent
88
90
{
89
91
WaveFormat = new WaveFormat ( SampleRate , BitsPerSample , Channels ) ,
90
92
BufferMilliseconds = 50 // 50ms buffer for low latency
91
93
} ;
92
-
94
+
93
95
_waveIn . DataAvailable += OnAudioDataAvailable ;
94
96
_waveIn . RecordingStopped += OnRecordingStopped ;
95
97
96
98
_logger . LogInformation ( $ "There are { WaveIn . DeviceCount } devices available.") ;
97
- for ( int i = 0 ; i < WaveIn . DeviceCount ; i ++ )
99
+ for ( int i = 0 ; i < WaveIn . DeviceCount ; i ++ )
98
100
{
99
101
var deviceInfo = WaveIn . GetCapabilities ( i ) ;
100
102
@@ -103,10 +105,10 @@ public Task StartCaptureAsync()
103
105
_waveIn . DeviceNumber = 0 ; // Default to first device
104
106
105
107
_waveIn . StartRecording ( ) ;
106
-
108
+
107
109
// Start audio send task
108
110
_audioSendTask = ProcessAudioSendAsync ( _cancellationTokenSource . Token ) ;
109
-
111
+
110
112
_logger . LogInformation ( "Started audio capture" ) ;
111
113
return Task . CompletedTask ;
112
114
}
@@ -117,17 +119,17 @@ public Task StartCaptureAsync()
117
119
throw ;
118
120
}
119
121
}
120
-
122
+
121
123
/// <summary>
122
124
/// Stop capturing audio.
123
125
/// </summary>
124
126
public async Task StopCaptureAsync ( )
125
127
{
126
128
if ( ! _isCapturing )
127
129
return ;
128
-
130
+
129
131
_isCapturing = false ;
130
-
132
+
131
133
if ( _waveIn != null )
132
134
{
133
135
_waveIn . StopRecording ( ) ;
@@ -136,49 +138,49 @@ public async Task StopCaptureAsync()
136
138
_waveIn . Dispose ( ) ;
137
139
_waveIn = null ;
138
140
}
139
-
141
+
140
142
// Complete the send channel and wait for the send task
141
143
_audioSendWriter . TryComplete ( ) ;
142
144
if ( _audioSendTask != null )
143
145
{
144
146
await _audioSendTask . ConfigureAwait ( false ) ;
145
147
_audioSendTask = null ;
146
148
}
147
-
149
+
148
150
_logger . LogInformation ( "Stopped audio capture" ) ;
149
151
}
150
-
152
+
151
153
/// <summary>
152
154
/// Initialize audio playback system.
153
155
/// </summary>
154
156
public Task StartPlaybackAsync ( )
155
157
{
156
158
if ( _isPlaying )
157
159
return Task . CompletedTask ;
158
-
160
+
159
161
_isPlaying = true ;
160
-
162
+
161
163
try
162
164
{
163
165
_waveOut = new WaveOutEvent
164
166
{
165
167
DesiredLatency = 100 // 100ms latency
166
168
} ;
167
-
169
+
168
170
_playbackBuffer = new BufferedWaveProvider ( new WaveFormat ( SampleRate , BitsPerSample , Channels ) )
169
171
{
170
172
BufferDuration = TimeSpan . FromMinutes ( 5 ) , // 5 second buffer
171
173
DiscardOnBufferOverflow = true
172
174
} ;
173
-
175
+
174
176
_waveOut . Init ( _playbackBuffer ) ;
175
177
_waveOut . Play ( ) ;
176
178
177
179
_playbackCancellationTokenSource = new CancellationTokenSource ( ) ;
178
180
179
181
// Start audio playback task
180
182
_audioPlaybackTask = ProcessAudioPlaybackAsync ( ) ;
181
-
183
+
182
184
_logger . LogInformation ( "Audio playback system ready" ) ;
183
185
return Task . CompletedTask ;
184
186
}
@@ -189,34 +191,35 @@ public Task StartPlaybackAsync()
189
191
throw ;
190
192
}
191
193
}
192
-
194
+
193
195
/// <summary>
194
196
/// Stop audio playback and clear buffer.
195
197
/// </summary>
196
198
public async Task StopPlaybackAsync ( )
197
199
{
198
200
if ( ! _isPlaying )
199
201
return ;
200
-
202
+
201
203
_isPlaying = false ;
202
-
204
+
203
205
// Clear the playback channel
204
- while ( _audioPlaybackReader . TryRead ( out _ ) ) { }
205
-
206
+ while ( _audioPlaybackReader . TryRead ( out _ ) )
207
+ { }
208
+
206
209
if ( _playbackBuffer != null )
207
210
{
208
211
_playbackBuffer . ClearBuffer ( ) ;
209
212
}
210
-
213
+
211
214
if ( _waveOut != null )
212
215
{
213
216
_waveOut . Stop ( ) ;
214
217
_waveOut . Dispose ( ) ;
215
218
_waveOut = null ;
216
219
}
217
-
220
+
218
221
_playbackBuffer = null ;
219
-
222
+
220
223
// Complete the playback channel and wait for the playback task
221
224
_playbackCancellationTokenSource . Cancel ( ) ;
222
225
@@ -225,10 +228,10 @@ public async Task StopPlaybackAsync()
225
228
await _audioPlaybackTask . ConfigureAwait ( false ) ;
226
229
_audioPlaybackTask = null ;
227
230
}
228
-
231
+
229
232
_logger . LogInformation ( "Stopped audio playback" ) ;
230
233
}
231
-
234
+
232
235
/// <summary>
233
236
/// Queue audio data for playback.
234
237
/// </summary>
@@ -240,7 +243,7 @@ public async Task QueueAudioAsync(byte[] audioData)
240
243
await _audioPlaybackWriter . WriteAsync ( audioData ) . ConfigureAwait ( false ) ;
241
244
}
242
245
}
243
-
246
+
244
247
/// <summary>
245
248
/// Event handler for audio data available from microphone.
246
249
/// </summary>
@@ -250,15 +253,15 @@ private void OnAudioDataAvailable(object? sender, WaveInEventArgs e)
250
253
{
251
254
byte [ ] audioData = new byte [ e . BytesRecorded ] ;
252
255
Array . Copy ( e . Buffer , 0 , audioData , 0 , e . BytesRecorded ) ;
253
-
256
+
254
257
// Queue audio data for sending (non-blocking)
255
258
if ( ! _audioSendWriter . TryWrite ( audioData ) )
256
259
{
257
260
_logger . LogWarning ( "Failed to queue audio data for sending - channel may be full" ) ;
258
261
}
259
262
}
260
263
}
261
-
264
+
262
265
/// <summary>
263
266
/// Event handler for recording stopped.
264
267
/// </summary>
@@ -269,19 +272,19 @@ private void OnRecordingStopped(object? sender, StoppedEventArgs e)
269
272
_logger . LogError ( e . Exception , "Audio recording stopped due to error" ) ;
270
273
}
271
274
}
272
-
275
+
273
276
/// <summary>
274
277
/// Background task to process audio data and send to VoiceLive service.
275
278
/// </summary>
276
279
private async Task ProcessAudioSendAsync ( CancellationToken cancellationToken )
277
280
{
278
281
try
279
282
{
280
- await foreach ( byte [ ] audioData in _audioSendReader . ReadAllAsync ( cancellationToken ) )
283
+ await foreach ( byte [ ] audioData in _audioSendReader . ReadAllAsync ( cancellationToken ) . ConfigureAwait ( false ) )
281
284
{
282
285
if ( cancellationToken . IsCancellationRequested )
283
286
break ;
284
-
287
+
285
288
try
286
289
{
287
290
// Send audio data directly to the session
@@ -303,7 +306,7 @@ private async Task ProcessAudioSendAsync(CancellationToken cancellationToken)
303
306
_logger . LogError ( ex , "Error in audio send processing" ) ;
304
307
}
305
308
}
306
-
309
+
307
310
/// <summary>
308
311
/// Background task to process audio playback.
309
312
/// </summary>
@@ -314,11 +317,11 @@ private async Task ProcessAudioPlaybackAsync()
314
317
CancellationTokenSource combinedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( _playbackCancellationTokenSource . Token , _cancellationTokenSource . Token ) ;
315
318
var cancellationToken = combinedTokenSource . Token ;
316
319
317
- await foreach ( byte [ ] audioData in _audioPlaybackReader . ReadAllAsync ( cancellationToken ) )
320
+ await foreach ( byte [ ] audioData in _audioPlaybackReader . ReadAllAsync ( cancellationToken ) . ConfigureAwait ( false ) )
318
321
{
319
322
if ( cancellationToken . IsCancellationRequested )
320
323
break ;
321
-
324
+
322
325
try
323
326
{
324
327
if ( _playbackBuffer != null && _isPlaying )
@@ -342,36 +345,38 @@ private async Task ProcessAudioPlaybackAsync()
342
345
_logger . LogError ( ex , "Error in audio playback processing" ) ;
343
346
}
344
347
}
345
-
348
+
346
349
/// <summary>
347
350
/// Clean up audio resources.
348
351
/// </summary>
349
352
public async Task CleanupAsync ( )
350
353
{
351
354
await StopCaptureAsync ( ) . ConfigureAwait ( false ) ;
352
355
await StopPlaybackAsync ( ) . ConfigureAwait ( false ) ;
353
-
356
+
354
357
_cancellationTokenSource . Cancel ( ) ;
355
-
358
+
356
359
// Wait for background tasks to complete
357
360
var tasks = new List < Task > ( ) ;
358
- if ( _audioSendTask != null ) tasks . Add ( _audioSendTask ) ;
359
- if ( _audioPlaybackTask != null ) tasks . Add ( _audioPlaybackTask ) ;
360
-
361
+ if ( _audioSendTask != null )
362
+ tasks . Add ( _audioSendTask ) ;
363
+ if ( _audioPlaybackTask != null )
364
+ tasks . Add ( _audioPlaybackTask ) ;
365
+
361
366
if ( tasks . Count > 0 )
362
367
{
363
368
await Task . WhenAll ( tasks ) . ConfigureAwait ( false ) ;
364
369
}
365
-
370
+
366
371
_logger . LogInformation ( "Audio processor cleaned up" ) ;
367
372
}
368
-
373
+
369
374
/// <summary>
370
375
/// Dispose of resources.
371
376
/// </summary>
372
377
public void Dispose ( )
373
378
{
374
- CleanupAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
379
+ CleanupAsync ( ) . Wait ( ) ;
375
380
_cancellationTokenSource . Dispose ( ) ;
376
381
}
377
382
}
0 commit comments