Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Use Foundry Local's live audio transcription API to stream microphone audio and receive transcription results in real time. In this article, you create a console application that captures audio from your microphone, streams it to a local speech model, and prints transcription output as you speak.
Prerequisites
- .NET 9.0 SDK or later installed.
- A working microphone connected to your computer.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/csharp/foundry-local/live-audio-transcription-example
Install packages
If you're developing or shipping on Windows, select the Windows tab. The Windows package integrates with the Windows ML runtime — it provides the same API surface area with a wider breadth of hardware acceleration.
dotnet add package Microsoft.AI.Foundry.Local.WinML
dotnet add package OpenAI
The C# samples in the GitHub repository are preconfigured projects. If you're building from scratch, you should read the Foundry Local SDK reference for more details on how to set up your C# project with Foundry Local.
Install the NAudio package for microphone capture:
dotnet add package NAudio
Live transcribe from microphone
The following code initializes the Foundry Local SDK, loads a streaming speech model, captures audio from your microphone using NAudio, and streams it to the live transcription API. Partial results appear as you speak, and final results are printed on a new line.
Copy and paste the following code into Program.cs:
// Live Audio Transcription — Foundry Local SDK Example
//
// NAudio's WaveInEvent is Windows-only. On non-Windows platforms, the sample
// falls back to synthetic PCM audio.
using Microsoft.AI.Foundry.Local;
using NAudio.Wave;
Console.WriteLine("===========================================================");
Console.WriteLine(" Foundry Local -- Live Audio Transcription Demo");
Console.WriteLine("===========================================================");
Console.WriteLine();
var config = new Configuration
{
AppName = "foundry_local_samples",
LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information
};
await FoundryLocalManager.CreateAsync(config, Utils.GetAppLogger());
var mgr = FoundryLocalManager.Instance;
await Utils.RunWithSpinner("Registering execution providers", mgr.DownloadAndRegisterEpsAsync());
var catalog = await mgr.GetCatalogAsync();
// English-only:
var modelAlias = "nemotron-speech-streaming-en-0.6b";
// Multi-lingual (supports 30+ languages including auto-detect):
// var modelAlias = "nvidia-nemotron-3.5-asr-streaming-multilingual-0.6b";
var model = await catalog.GetModelAsync(modelAlias) ?? throw new Exception($"Model \"{modelAlias}\" not found in catalog");
await model.DownloadAsync(progress =>
{
Console.Write($"\rDownloading model: {progress:F2}%");
if (progress >= 100f)
{
Console.WriteLine();
}
});
Console.Write($"Loading model {model.Id}...");
await model.LoadAsync();
Console.WriteLine("done.");
var audioClient = await model.GetAudioClientAsync();
var session = audioClient.CreateLiveTranscriptionSession();
session.Settings.SampleRate = 16000; // Default is 16000; shown here to match the NAudio WaveFormat below
session.Settings.Channels = 1;
session.Settings.Language = "en"; // English (default)
// Multi-lingual examples:
// session.Settings.Language = "de"; // German
// session.Settings.Language = "zh-CN"; // Chinese (Simplified)
// session.Settings.Language = "auto"; // Auto-detect language
await session.StartAsync();
Console.WriteLine(" Session started");
var readTask = Task.Run(async () =>
{
try
{
await foreach (var result in session.GetStream())
{
var text = result.Content?[0]?.Text;
if (result.IsFinal)
{
Console.WriteLine();
Console.WriteLine($" [FINAL] {text}");
Console.Out.Flush();
}
else if (!string.IsNullOrEmpty(text))
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(text);
Console.ResetColor();
Console.Out.Flush();
}
}
}
catch (OperationCanceledException) { }
});
bool useSynth = args.Contains("--synth");
// NAudio WaveInEvent is Windows-only. On other platforms, fall back to synthetic audio.
if (!useSynth && OperatingSystem.IsWindows())
{
using var waveIn = new WaveInEvent
{
WaveFormat = new WaveFormat(rate: 16000, bits: 16, channels: 1),
BufferMilliseconds = 100
};
// Use a bounded channel to avoid unbounded fire-and-forget AppendAsync calls.
// NAudio's DataAvailable callback is synchronous, so we enqueue PCM chunks and
// await AppendAsync on a dedicated task to respect SDK backpressure.
var audioChannel = System.Threading.Channels.Channel.CreateBounded<byte[]>(
new System.Threading.Channels.BoundedChannelOptions(50)
{
FullMode = System.Threading.Channels.BoundedChannelFullMode.DropOldest
});
var appendTask = Task.Run(async () =>
{
await foreach (var chunk in audioChannel.Reader.ReadAllAsync())
{
await session.AppendAsync(chunk);
}
});
waveIn.DataAvailable += (sender, e) =>
{
if (e.BytesRecorded > 0)
{
var buffer = new byte[e.BytesRecorded];
Buffer.BlockCopy(e.Buffer, 0, buffer, 0, e.BytesRecorded);
audioChannel.Writer.TryWrite(buffer);
}
};
Console.WriteLine();
Console.WriteLine("===========================================================");
Console.WriteLine(" LIVE TRANSCRIPTION ACTIVE");
Console.WriteLine(" Speak into your microphone.");
Console.WriteLine(" Transcription appears in real-time (cyan text).");
Console.WriteLine(" Press ENTER to stop recording.");
Console.WriteLine("===========================================================");
Console.WriteLine();
waveIn.StartRecording();
Console.ReadLine();
waveIn.StopRecording();
audioChannel.Writer.Complete();
await appendTask;
}
else
{
if (!OperatingSystem.IsWindows() && !useSynth)
{
Console.WriteLine("NAudio mic capture is Windows-only. Falling back to synthetic audio...");
}
// Synthetic PCM fallback: 440Hz sine wave, 2 seconds
Console.WriteLine("Pushing synthetic audio (440Hz sine, 2s)...");
const int sampleRate = 16000;
const int duration = 2;
var totalSamples = sampleRate * duration;
var pcmBytes = new byte[totalSamples * 2];
for (int i = 0; i < totalSamples; i++)
{
double t = (double)i / sampleRate;
short sample = (short)(short.MaxValue * 0.5 * Math.Sin(2 * Math.PI * 440 * t));
pcmBytes[i * 2] = (byte)(sample & 0xFF);
pcmBytes[i * 2 + 1] = (byte)((sample >> 8) & 0xFF);
}
int chunkSize = (sampleRate / 10) * 2; // 100ms
for (int offset = 0; offset < pcmBytes.Length; offset += chunkSize)
{
int len = Math.Min(chunkSize, pcmBytes.Length - offset);
await session.AppendAsync(pcmBytes.AsMemory(offset, len));
await Task.Delay(100);
}
Console.WriteLine("✓ Synthetic audio pushed");
await Task.Delay(3000); // Wait for remaining transcription results
}
await session.StopAsync();
await readTask;
await model.UnloadAsync();
The CreateLiveTranscriptionSession method returns a session that accepts raw Pulse-code modulation (PCM) audio and yields transcription results as an async stream. NAudio's WaveInEvent captures microphone audio at 16-kHz mono—the format the session expects.
Run the application:
dotnet run
Speak into your microphone. You see real-time transcription output:
Listening... (press Ctrl+C to stop)
Hello, this is a test of the live transcription feature.
It transcribes audio from the microphone in real time.
Press Ctrl+C to stop recording. The model finishes processing any remaining audio and the application exits.
Prerequisites
- Node.js version 20 or later installed.
- A working microphone connected to your computer.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/javascript/foundry-local/live-audio-transcription-example
Install packages
If you're developing or shipping on Windows, select the Windows tab. The Windows package integrates with the Windows ML runtime — it provides the same API surface area with a wider breadth of hardware acceleration.
npm install foundry-local-sdk-winml openai
Install the naudiodon2 package for microphone capture:
npm install naudiodon2
Live transcribe from microphone
The following code initializes the Foundry Local SDK, loads a streaming speech model, captures audio from your microphone using naudiodon2, and streams it to the live transcription API. Partial results appear as you speak, and final results are printed on a new line.
Copy and paste the following code into app.js:
// Live Audio Transcription Example — Foundry Local JS SDK
//
// Demonstrates real-time microphone-to-text using the JS SDK.
// Requires: npm install foundry-local-sdk naudiodon2
//
// Usage: node app.js
import { FoundryLocalManager } from 'foundry-local-sdk';
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ Foundry Local — Live Audio Transcription (JS SDK) ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log();
// Initialize the Foundry Local SDK
console.log('Initializing Foundry Local SDK...');
const manager = FoundryLocalManager.create({
appName: 'foundry_local_samples',
logLevel: 'info'
});
console.log('✓ SDK initialized');
// Get and load the nemotron model
// English-only:
const modelAlias = 'nemotron-speech-streaming-en-0.6b';
// Multi-lingual (supports 30+ languages including auto-detect):
// const modelAlias = 'nvidia-nemotron-3.5-asr-streaming-multilingual-0.6b';
let model = await manager.catalog.getModel(modelAlias);
if (!model) {
console.error(`ERROR: Model "${modelAlias}" not found in catalog.`);
process.exit(1);
}
console.log(`Found model: ${model.id}`);
console.log('Downloading model (if needed)...');
await model.download((progress) => {
process.stdout.write(`\rDownloading... ${progress.toFixed(2)}%`);
});
console.log('\n✓ Model downloaded');
console.log('Loading model...');
await model.load();
console.log('✓ Model loaded');
// Create live transcription session (same pattern as C# sample).
const audioClient = model.createAudioClient();
const session = audioClient.createLiveTranscriptionSession();
session.settings.sampleRate = 16000; // Default is 16000; shown here for clarity
session.settings.channels = 1;
session.settings.bitsPerSample = 16;
session.settings.language = 'en'; // English (default)
// Multi-lingual examples:
// session.settings.language = 'de'; // German
// session.settings.language = 'zh-CN'; // Chinese (Simplified)
// session.settings.language = 'auto'; // Auto-detect language
console.log('Starting streaming session...');
await session.start();
console.log('✓ Session started');
// Read transcription results in background
const readPromise = (async () => {
try {
for await (const result of session.getStream()) {
const text = result.content?.[0]?.text;
if (!text) continue;
// `is_final` is a transcript-state marker only. It should not stop the app.
if (result.is_final) {
process.stdout.write(`\n [FINAL] ${text}\n`);
} else {
process.stdout.write(text);
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Stream error:', err.message);
}
}
})();
// --- Microphone capture ---
// This example uses naudiodon2 for cross-platform audio capture.
// Install with: npm install naudiodon2
//
// If you prefer a different audio library, just push PCM bytes
// (16-bit signed LE, mono, 16kHz) via session.append().
let audioInput;
try {
const { default: portAudio } = await import('naudiodon2');
audioInput = portAudio.AudioIO({
inOptions: {
channelCount: session.settings.channels,
sampleFormat: session.settings.bitsPerSample === 16
? portAudio.SampleFormat16Bit
: portAudio.SampleFormat32Bit,
sampleRate: session.settings.sampleRate,
// Larger chunk size lowers callback frequency and reduces overflow risk.
framesPerBuffer: 3200,
// Allow deeper native queue during occasional event-loop stalls.
maxQueue: 64
}
});
const appendQueue = [];
let pumping = false;
let warnedQueueDrop = false;
const pumpAudio = async () => {
if (pumping) return;
pumping = true;
try {
while (appendQueue.length > 0) {
const pcm = appendQueue.shift();
await session.append(pcm);
}
} catch (err) {
console.error('append error:', err.message);
} finally {
pumping = false;
// Handle race where new data arrived after loop exit.
if (appendQueue.length > 0) {
void pumpAudio();
}
}
};
audioInput.on('data', (buffer) => {
// Single copy: slice the underlying ArrayBuffer to get an independent Uint8Array.
const copy = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength).slice();
// Keep a bounded queue to avoid unbounded memory growth.
if (appendQueue.length >= 100) {
appendQueue.shift();
if (!warnedQueueDrop) {
warnedQueueDrop = true;
console.warn('Audio append queue overflow; dropping oldest chunk to keep stream alive.');
}
}
appendQueue.push(copy);
void pumpAudio();
});
console.log();
console.log('════════════════════════════════════════════════════════════');
console.log(' LIVE TRANSCRIPTION ACTIVE');
console.log(' Speak into your microphone.');
console.log(' Press Ctrl+C to stop.');
console.log('════════════════════════════════════════════════════════════');
console.log();
audioInput.start();
} catch (err) {
console.warn('⚠ Could not initialize microphone (naudiodon2 may not be installed).');
console.warn(' Install with: npm install naudiodon2');
console.warn(' Falling back to synthetic audio test...');
console.warn();
// Fallback: push 2 seconds of synthetic PCM (440Hz sine wave)
const sampleRate = session.settings.sampleRate;
const duration = 2;
const totalSamples = sampleRate * duration;
const pcmBytes = new Uint8Array(totalSamples * 2);
for (let i = 0; i < totalSamples; i++) {
const t = i / sampleRate;
const sample = Math.round(32767 * 0.5 * Math.sin(2 * Math.PI * 440 * t));
pcmBytes[i * 2] = sample & 0xFF;
pcmBytes[i * 2 + 1] = (sample >> 8) & 0xFF;
}
// Push in 100ms chunks
const chunkSize = (sampleRate / 10) * 2;
for (let offset = 0; offset < pcmBytes.length; offset += chunkSize) {
const len = Math.min(chunkSize, pcmBytes.length - offset);
await session.append(pcmBytes.slice(offset, offset + len));
}
console.log('✓ Synthetic audio pushed');
console.log('Waiting briefly for final transcription results...');
await new Promise((resolve) => setTimeout(resolve, 3000));
await session.stop();
await readPromise;
await model.unload();
console.log('✓ Done');
process.exit(0);
}
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\n\nStopping...');
if (audioInput) {
audioInput.quit();
}
await session.stop();
await readPromise;
await model.unload();
console.log('✓ Done');
process.exit(0);
});
The createLiveTranscriptionSession method returns a session that accepts raw PCM audio and yields transcription results as an async generator. The naudiodon2 AudioIO captures microphone audio at 16 kHz mono 16-bit — the format the session expects.
Run the application:
node app.js
Speak into your microphone. You see real-time transcription output:
Listening... (press Ctrl+C to stop)
Hello, this is a test of the live transcription feature.
It transcribes audio from the microphone in real time.
Press Ctrl+C to stop recording. The model finishes processing any remaining audio and the application exits.
Prerequisites
- Python 3.11 or later installed.
- A working microphone connected to your computer.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/python/foundry-local/live-audio-transcription
Install packages
If you're developing or shipping on Windows, select the Windows tab. The Windows package integrates with the Windows ML runtime — it provides the same API surface area with a wider breadth of hardware acceleration.
pip install foundry-local-sdk-winml openai
Install PyAudio for microphone capture:
pip install pyaudio
Live transcribe from microphone
The following code initializes the Foundry Local SDK, loads a streaming speech model, captures audio from your microphone using PyAudio, and streams it to the live transcription API. Partial results appear as you speak, and final results are printed on a new line.
Copy and paste the following code into app.py:
# Live Audio Transcription — Foundry Local SDK Example (Python)
#
# Tries PyAudio mic capture first; falls back to synthetic PCM if unavailable.
#
# Usage:
# pip install -r requirements.txt
# python src/app.py # Live microphone
# python src/app.py --synth # Synthetic 440Hz sine wave
import math
import signal
import struct
import sys
import threading
import time
from foundry_local_sdk import Configuration, FoundryLocalManager
use_synth = "--synth" in sys.argv
print("===========================================================")
print(" Foundry Local -- Live Audio Transcription Demo (Python)")
print("===========================================================")
print()
config = Configuration(app_name="foundry_local_samples")
FoundryLocalManager.initialize(config)
manager = FoundryLocalManager.instance
manager.download_and_register_eps()
# English-only:
model_alias = "nemotron-speech-streaming-en-0.6b"
# Multi-lingual (supports 30+ languages including auto-detect):
# model_alias = "nvidia-nemotron-3.5-asr-streaming-multilingual-0.6b"
model = manager.catalog.get_model(model_alias)
if model is None:
raise RuntimeError(f'Model "{model_alias}" not found in catalog')
model.download(
lambda progress: print(f"\rDownloading model: {progress:.2f}%", end="", flush=True)
)
print()
print(f"Loading model {model.id}...", end="")
model.load()
print("done.")
audio_client = model.get_audio_client()
session = audio_client.create_live_transcription_session()
session.settings.sample_rate = 16000
session.settings.channels = 1
session.settings.language = "en" # English (default)
# Multi-lingual examples:
# session.settings.language = "de" # German
# session.settings.language = "zh-CN" # Chinese (Simplified)
# session.settings.language = "auto" # Auto-detect language
session.start()
print("✓ Session started")
# --- Background thread reads transcription results (mirrors JS readPromise) ---
def read_results():
for result in session.get_stream():
text = result.content[0].text if result.content else ""
if result.is_final:
print()
print(f" [FINAL] {text}")
elif text:
print(text, end="", flush=True)
read_thread = threading.Thread(target=read_results, daemon=True)
read_thread.start()
# --- Microphone capture (mirrors JS naudiodon2 / C++ PortAudio) ---
# Try PyAudio for mic input; fall back to synthetic PCM on failure.
RATE = 16000
CHANNELS = 1
CHUNK = RATE // 10 # 100ms of audio = 1600 frames
stop_event = threading.Event()
mic_active = False
pa = None
stream = None
if not use_synth:
try:
import pyaudio
pa = pyaudio.PyAudio()
stream = pa.open(
format=pyaudio.paInt16,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK,
)
mic_active = True
print()
print("===========================================================")
print(" LIVE TRANSCRIPTION ACTIVE")
print(" Speak into your microphone.")
print(" Press Ctrl+C to stop.")
print("===========================================================")
print()
def capture_mic():
while not stop_event.is_set():
try:
pcm_data = stream.read(CHUNK, exception_on_overflow=False)
if pcm_data:
session.append(pcm_data)
except Exception as e:
print(f"\n[ERROR] Microphone capture failed: {e}")
stop_event.set()
break
capture_thread = threading.Thread(target=capture_mic, daemon=True)
capture_thread.start()
except Exception as e:
print(f"Could not initialize microphone: {e}")
print("Falling back to synthetic audio test...")
print()
mic_active = False
if stream:
stream.close()
if pa:
pa.terminate()
pa = None
stream = None
# Fallback: push synthetic PCM (440Hz sine wave) — mirrors JS catch block
if not mic_active:
print("Pushing synthetic audio (440Hz sine, 2s)...")
duration = 2
total_samples = RATE * duration
pcm_bytes = bytearray(total_samples * 2)
for i in range(total_samples):
t = i / RATE
sample = int(32767 * 0.5 * math.sin(2 * math.pi * 440 * t))
struct.pack_into("<h", pcm_bytes, i * 2, sample)
chunk_size = (RATE // 10) * 2 # 100ms
for offset in range(0, len(pcm_bytes), chunk_size):
end = min(offset + chunk_size, len(pcm_bytes))
session.append(bytes(pcm_bytes[offset:end]))
time.sleep(0.1)
print("✓ Synthetic audio pushed")
time.sleep(3) # Wait for remaining transcription results
# --- Graceful shutdown (mirrors JS SIGINT handler / C++ SignalHandler) ---
def shutdown(*_args):
print("\n\nStopping...")
stop_event.set()
if stream:
stream.stop_stream()
stream.close()
if pa:
pa.terminate()
session.stop()
read_thread.join(timeout=5)
model.unload()
print("✓ Done")
sys.exit(0)
signal.signal(signal.SIGINT, lambda *a: shutdown())
if mic_active:
# Block until Ctrl+C
stop_event.wait()
else:
shutdown()
The create_live_transcription_session method returns a session that accepts raw PCM audio and yields transcription results as you stream chunks. PyAudio captures microphone audio at 16 kHz mono 16-bit — the format the session expects.
Run the application:
python src/app.py
Speak into your microphone. You see real-time transcription output:
Listening... (press Ctrl+C to stop)
Hello, this is a test of the live transcription feature.
It transcribes audio from the microphone in real time.
Press Ctrl+C to stop recording. The model finishes processing any remaining audio and the application exits.
Prerequisites
- Rust and Cargo installed (Rust 1.70.0 or later).
- A working microphone connected to your computer.
Samples repository
The complete sample code for this article is available in the foundry-samples GitHub repository. To clone the repository and navigate to the sample use:
git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/rust/foundry-local/live-audio-transcription-example
Install packages
If you're developing or shipping on Windows, select the Windows tab. The Windows package integrates with the Windows ML runtime — it provides the same API surface area with a wider breadth of hardware acceleration.
cargo add foundry-local-sdk --features winml
cargo add tokio --features full
cargo add tokio-stream anyhow
The sample uses the cpal crate for cross-platform microphone capture. The dependency is already listed in Cargo.toml.
Live transcribe from microphone
The following code initializes the Foundry Local SDK, loads a streaming speech model, captures audio from your microphone using cpal, and streams it to the live transcription API. Partial results appear as you speak, and final results are printed on a new line.
Replace the contents of src/main.rs with the following code:
// Live Audio Transcription — Foundry Local Rust SDK Example
//
// Tries CPAL mic capture first; falls back to synthetic PCM if unavailable.
//
// Usage:
// cargo run # Live microphone (press Ctrl+C to stop)
// cargo run -- --synth # Synthetic 440Hz sine wave
use std::env;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use foundry_local_sdk::{FoundryLocalConfig, FoundryLocalManager, LiveAudioTranscriptionSession};
use tokio_stream::StreamExt;
// English-only:
const ALIAS: &str = "nemotron-speech-streaming-en-0.6b";
// Multi-lingual (supports 30+ languages including auto-detect):
// const ALIAS: &str = "nvidia-nemotron-3.5-asr-streaming-multilingual-0.6b";
// Global flag for Ctrl+C graceful shutdown (mirrors JS process.on('SIGINT'))
static RUNNING: AtomicBool = AtomicBool::new(true);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let use_synth = env::args().any(|a| a == "--synth");
// Install Ctrl+C handler (mirrors JS SIGINT / C++ SignalHandler)
let running = Arc::new(AtomicBool::new(true));
let running_for_signal = running.clone();
ctrlc::set_handler(move || {
RUNNING.store(false, Ordering::SeqCst);
running_for_signal.store(false, Ordering::SeqCst);
})?;
println!("===========================================================");
println!(" Foundry Local -- Live Audio Transcription Demo (Rust)");
println!("===========================================================");
println!();
let manager = FoundryLocalManager::create(FoundryLocalConfig::new("foundry_local_samples"))?;
let model = manager.catalog().get_model(ALIAS).await?;
println!("Model: {} (id: {})", model.alias(), model.id());
if !model.is_cached().await? {
println!("Downloading model...");
model
.download(Some(|progress: f64| {
print!("\r {progress:.1}%");
io::stdout().flush().ok();
}))
.await?;
println!();
}
println!("Loading model...");
model.load().await?;
println!("✓ Model loaded\n");
let audio_client = model.create_audio_client();
let mut session = audio_client.create_live_transcription_session();
session.settings.language = Some("en".into()); // English (default)
// session.settings.language = Some("de".into()); // German
// session.settings.language = Some("zh-CN".into()); // Chinese (Simplified)
// session.settings.language = Some("auto".into()); // Auto-detect language
let session = Arc::new(session);
session.start(None).await?;
println!("✓ Session started\n");
// --- Background task reads transcription results (mirrors JS readPromise) ---
let mut stream = session.get_stream().await?;
let read_task = tokio::spawn(async move {
while let Some(result) = stream.next().await {
match result {
Ok(r) => {
if let Some(content) = r.content.first() {
let text = &content.text;
if r.is_final {
println!();
println!(" [FINAL] {text}");
} else if !text.is_empty() {
print!("{text}");
io::stdout().flush().ok();
}
}
}
Err(e) => {
eprintln!("\n[ERROR] Stream error: {e}");
break;
}
}
}
});
// --- Microphone capture (mirrors JS naudiodon2 / C++ PortAudio / Python PyAudio) ---
// Try CPAL for mic input; fall back to synthetic PCM on failure.
let mut mic_active = false;
if !use_synth {
match try_start_mic(&session, &running).await {
Ok(()) => {
mic_active = true;
}
Err(e) => {
eprintln!("Could not initialize microphone: {e}");
eprintln!("Falling back to synthetic audio test...\n");
}
}
}
// Fallback: push synthetic PCM (440Hz sine wave) — mirrors JS catch block
if !mic_active {
println!("Pushing synthetic audio (440Hz sine, 2s)...");
let pcm_data = generate_sine_wave_pcm(16000, 2, 440.0);
let chunk_size = 16000 / 10 * 2; // 100ms
let chunk_interval = std::time::Duration::from_millis(100);
for offset in (0..pcm_data.len()).step_by(chunk_size) {
if !running.load(Ordering::SeqCst) {
break;
}
let end = std::cmp::min(offset + chunk_size, pcm_data.len());
session.append(&pcm_data[offset..end], None).await?;
tokio::time::sleep(chunk_interval).await;
}
println!("✓ Synthetic audio pushed");
// Wait for remaining transcription results
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
// Graceful shutdown (mirrors JS SIGINT handler)
println!("\n\nStopping...");
session.stop(None).await?;
read_task.await?;
model.unload().await?;
println!("✓ Done");
Ok(())
}
/// Try to open the default microphone with CPAL and forward PCM to the session.
/// Blocks until Ctrl+C is pressed.
async fn try_start_mic(
session: &Arc<LiveAudioTranscriptionSession>,
running: &Arc<AtomicBool>,
) -> Result<(), Box<dyn std::error::Error>> {
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or("No input audio device available")?;
let default_config = device.default_input_config()?;
let device_rate = default_config.sample_rate().0;
let device_channels = default_config.channels();
let sample_format = default_config.sample_format();
let mic_config = cpal::StreamConfig {
channels: device_channels,
sample_rate: cpal::SampleRate(device_rate),
buffer_size: cpal::BufferSize::Default,
};
// Bounded channel (cap=100) mirrors JS appendQueue / C++ AudioQueue
let (audio_tx, mut audio_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(100);
let err_fn = |err| eprintln!("Microphone stream error: {err}");
// CPAL may deliver f32, i16, or u16 depending on the device/host. Convert
// each supported sample format to f32 in [-1.0, 1.0] before resampling.
let input_stream = match sample_format {
cpal::SampleFormat::F32 => {
let tx = audio_tx.clone();
device.build_input_stream(
&mic_config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
let bytes = convert_audio(data, device_channels, device_rate);
if !bytes.is_empty() {
let _ = tx.try_send(bytes);
}
},
err_fn,
None,
)?
}
cpal::SampleFormat::I16 => {
let tx = audio_tx.clone();
device.build_input_stream(
&mic_config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let samples: Vec<f32> = data
.iter()
.map(|&s| s as f32 / i16::MAX as f32)
.collect();
let bytes = convert_audio(&samples, device_channels, device_rate);
if !bytes.is_empty() {
let _ = tx.try_send(bytes);
}
},
err_fn,
None,
)?
}
cpal::SampleFormat::U16 => {
let tx = audio_tx.clone();
device.build_input_stream(
&mic_config,
move |data: &[u16], _: &cpal::InputCallbackInfo| {
let samples: Vec<f32> = data
.iter()
.map(|&s| (s as f32 / u16::MAX as f32) * 2.0 - 1.0)
.collect();
let bytes = convert_audio(&samples, device_channels, device_rate);
if !bytes.is_empty() {
let _ = tx.try_send(bytes);
}
},
err_fn,
None,
)?
}
other => {
return Err(format!("Unsupported input sample format: {other:?}").into());
}
};
drop(audio_tx);
input_stream.play()?;
println!("===========================================================");
println!(" LIVE TRANSCRIPTION ACTIVE");
println!(" Speak into your microphone.");
println!(" Press Ctrl+C to stop.");
println!("===========================================================");
println!();
// Pump audio from channel to session (mirrors JS pumpAudio / C++ pump loop)
let session_clone = Arc::clone(session);
let forward_task = tokio::spawn(async move {
while let Some(bytes) = audio_rx.recv().await {
if let Err(e) = session_clone.append(&bytes, None).await {
eprintln!("Append error: {e}");
break;
}
}
});
// Block until Ctrl+C
while running.load(Ordering::SeqCst) {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
drop(input_stream);
forward_task.await?;
Ok(())
}
fn convert_audio(data: &[f32], channels: u16, sample_rate: u32) -> Vec<u8> {
let mono: Vec<f32> = if channels > 1 {
data.chunks(channels as usize)
.map(|frame| frame.iter().sum::<f32>() / channels as f32)
.collect()
} else {
data.to_vec()
};
let resampled = if sample_rate != 16000 {
resample(&mono, sample_rate, 16000)
} else {
mono
};
let mut bytes = Vec::with_capacity(resampled.len() * 2);
for &s in &resampled {
let clamped = s.clamp(-1.0, 1.0);
let sample = (clamped * i16::MAX as f32) as i16;
bytes.extend_from_slice(&sample.to_le_bytes());
}
bytes
}
fn generate_sine_wave_pcm(sample_rate: i32, duration_seconds: i32, frequency: f64) -> Vec<u8> {
let total_samples = (sample_rate * duration_seconds) as usize;
let mut pcm_bytes = vec![0u8; total_samples * 2];
for i in 0..total_samples {
let t = i as f64 / sample_rate as f64;
let sample =
(i16::MAX as f64 * 0.5 * (2.0 * std::f64::consts::PI * frequency * t).sin()) as i16;
let bytes = sample.to_le_bytes();
pcm_bytes[i * 2] = bytes[0];
pcm_bytes[i * 2 + 1] = bytes[1];
}
pcm_bytes
}
fn resample(input: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
if from_rate == to_rate || input.is_empty() {
return input.to_vec();
}
let ratio = from_rate as f64 / to_rate as f64;
let out_len = (input.len() as f64 / ratio).ceil() as usize;
let mut output = Vec::with_capacity(out_len);
for i in 0..out_len {
let src_idx = i as f64 * ratio;
let idx = src_idx as usize;
let frac = src_idx - idx as f64;
let s0 = input[idx.min(input.len() - 1)];
let s1 = input[(idx + 1).min(input.len() - 1)];
output.push(s0 + (s1 - s0) * frac as f32);
}
output
}
The create_live_transcription_session method returns a session that accepts raw Pulse-code modulation (PCM) audio and yields transcription results as an async stream. The cpal input stream captures microphone audio at 16-kHz mono, which is the format the session expects.
Run the application:
cargo run
Speak into your microphone. You see real-time transcription output:
Listening... (press Ctrl+C to stop)
Hello, this is a test of the live transcription feature.
It transcribes audio from the microphone in real time.
Press Ctrl+C to stop recording. The model finishes processing any remaining audio and the application exits.