Processing a Single Stream - Sanas Developer Hub
Documentation Index
Fetch the complete documentation index at: /llms.txt
Use this file to discover all available pages before exploring further.
This tutorial walks through the core Sanas SDK flow using the sdk_example.py example. You’ll read a WAV file, run it through a model, and write the processed audio back out. Along the way you’ll use two shared helper modules — wav_utils.py (stdlib-only WAV I/O) and helpers.py (pipeline readiness and real-time pacing) — which every example depends on.
The SDK works with interleaved float32 PCM in [-1, 1]. You bring your own audio; the SDK never opens a microphone or speaker for you.
The core flow
Every audio-processing session follows the same five steps:
create_sdk→activate_api_key(synchronous, returns anSdkResult)create_audio_processorwith anaudio_pipeline_state_notifycallback- Wait for
PipelineState.RUNNINGbefore feeding frames - Feed 20 ms frames at real time, then drain the buffered tail with silence
- Save the processed audio
process_frame is synchronous and returns the processed frame directly. The model buffers ~50–100 ms internally, so the output lags the input. After the real audio is exhausted you push silence to pull the buffered tail back out.
Prerequisites
Set your credentials and pick an input file via environment variables and CLI flags:
| Variable | Required | Default | Purpose |
|---|---|---|---|
SANAS_API_KEY |
Yes | — | Licence key |
SANAS_STORAGE_DIR |
No | ./storage |
SDK data + logs (created if missing) |
export SANAS_API_KEY="your-key"
export SANAS_STORAGE_DIR=./storage
python sdk_example.py --model VI_G_SE --input test_input.wav --output output.wav
Command-line flags: --model (model key, default VI_G_SE), --input, --output, and --pcm16 (opt into raw 16-bit PCM uplink — cloud inference only).
Step 1: Create and activate the SDK
Activation is inline and blocking. Always check the returned SdkResult before continuing.
import sanas
sdk = sanas.create_sdk(sanas.InitParams(storage_dir=storage_dir))
res = sdk.activate_api_key(api_key) # blocks, returns SdkResult
if not res.success:
sys.exit(f"Activation failed: {res.message}")
print(f"[activation] OK (SDK {sdk.version})")
Step 2: Load the input audio
load_samples (from helpers.py) wraps read_wav and returns the audio as an array('f') ready to slice into AudioFrames.
from helpers import load_samples
samples, sample_rate, channels = load_samples(args.input)
print(f"[input] {sample_rate} Hz, {channels} ch, {len(samples) // channels} frames")
Under the hood, wav_utils.read_wav parses the WAV header directly (Python’s wave module only handles PCM) so it accepts both 16-bit PCM and 32-bit IEEE float input, including WAVE_FORMAT_EXTENSIBLE. It returns interleaved little-endian float32 regardless of host endianness.
Step 3: Build the processor with a state callback
The pipeline initializes asynchronously, so you must wait for PipelineState.RUNNING before feeding frames. PipelineWaiter (from helpers.py) bridges the audio_pipeline_state_notify callback to a waitable value.
from helpers import PipelineWaiter
waiter = PipelineWaiter(on_state=lambda s: print(f"[pipeline] {s}"))
cloud_params = sanas.CloudInferencingParams()
cloud_params.use_pcm16 = args.pcm16
attrs = sanas.ProcessorAttributes(
audio_attributes=sanas.AudioAttributes(
sampling_rate=sample_rate,
channels=channels,
model_name=args.model,
cloud_inferencing_params=cloud_params,
audio_pipeline_state_notify=waiter.callback,
)
)
Why a Condition instead of a plain Event?
A threading.Event only signals “something happened” and carries no value. PipelineWaiter uses a threading.Condition that notifies on every state change while keeping the latest state in last_state. Waiters wake on any transition and then inspect the value to decide whether to proceed (RUNNING), fail (NOT_RUNNING), or keep waiting. The state callback fires on a background thread, so publishing under a lock is required.
Step 4: Feed frames and drain the tail
Open the processor, wait for RUNNING, then hand off to feed_and_drain.
with sdk.create_audio_processor(attrs) as proc:
print("[pipeline] waiting for RUNNING ...")
waiter.wait_until_running(timeout=30.0)
def _progress(pushed, total):
pct = 100.0 * pushed / total if total else 100.0
print(f"\r[feed] {pct:5.1f}%%", end="", flush=True)
result = feed_and_drain(proc, samples, sample_rate, channels, progress=_progress)
print()
feed_and_drain splits the audio into fixed 20 ms chunks and calls process_frame on each. When realtime=True (the default), it paces the feed against a fixed monotonic schedule so timing self-corrects and doesn’t drift:
next_deadline += chunk_seconds
sleep_until(next_deadline)
The final short chunk is padded with silence so no audio is dropped. After the input is exhausted, the helper drains the buffered tail. For low-latency models (NC/SE) it stops once the pipeline goes quiet (three consecutive empty frames); for high-latency pipelines you pass drain_seconds to pump silence for a fixed window instead.
sleep_until sleeps until ~1 ms before the deadline, then busy-waits the last millisecond for tighter pacing than a bare time.sleep().
Step 5: Save the output
from wav_utils import save_wav
combined = result["output"]
if combined:
save_wav(args.output, combined, sample_rate, channels)
print(f"[output] {len(combined) // 4} samples -> {args.output}")
else:
print("[output] no processed frames received.")
save_wav clamps each sample to [-1, 1] and writes a 16-bit PCM WAV.
Full example
import argparse
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
import sanas
from helpers import PipelineWaiter, feed_and_drain, load_samples
from wav_utils import save_wav
def _run():
parser = argparse.ArgumentParser(
description="Process a WAV file through the Sanas SDK (single stream)."
)
parser.add_argument("--model", default="VI_G_SE", help="ml model key")
parser.add_argument("--input", default="test_input.wav", help="input WAV path")
parser.add_argument("--output", default="output.wav", help="output WAV path")
parser.add_argument(
"--pcm16",
action="store_true",
help="negotiate L16 (raw 16-bit PCM); cloud inference only",
)
args = parser.parse_args()
api_key = os.environ.get("SANAS_API_KEY")
if not api_key:
sys.exit("SANAS_API_KEY is not set.")
storage_dir = os.environ.get("SANAS_STORAGE_DIR", "./storage")
sdk = sanas.create_sdk(sanas.InitParams(storage_dir=storage_dir))
res = sdk.activate_api_key(api_key)
if not res.success:
sys.exit(f"Activation failed: {res.message}")
print(f"[activation] OK (SDK {sdk.version})")
samples, sample_rate, channels = load_samples(args.input)
print(f"[input] {sample_rate} Hz, {channels} ch, {len(samples) // channels} frames")
waiter = PipelineWaiter(on_state=lambda s: print(f"[pipeline] {s}"))
cloud_params = sanas.CloudInferencingParams()
cloud_params.use_pcm16 = args.pcm16
attrs = sanas.ProcessorAttributes(
audio_attributes=sanas.AudioAttributes(
sampling_rate=sample_rate,
channels=channels,
model_name=args.model,
cloud_inferencing_params=cloud_params,
audio_pipeline_state_notify=waiter.callback,
)
)
with sdk.create_audio_processor(attrs) as proc:
print("[pipeline] waiting for RUNNING ...")
waiter.wait_until_running(timeout=30.0)
def _progress(pushed, total):
pct = 100.0 * pushed / total if total else 100.0
print(f"\r[feed] {pct:5.1f}%%", end="", flush=True)
result = feed_and_drain(proc, samples, sample_rate, channels, progress=_progress)
print()
combined = result["output"]
if combined:
save_wav(args.output, combined, sample_rate, channels)
print(f"[output] {len(combined) // 4} samples -> {args.output}")
else:
print("[output] no processed frames received.")
if __name__ == "__main__":
_run()