Processing Multiple Streams - Sanas Developer Hub

Documentation Index

Fetch the complete documentation index at: /llms.txt

Use this file to discover all available pages before exploring further.

A single SDK instance can drive many audio processors at once. Because process_frame releases the GIL, streams running in separate threads process in parallel. This tutorial follows multi_stream_example.py, a benchmark that creates one SDK, runs N processors concurrently (one per thread), and reports success/failure counts, peak concurrency, per-stream wall time, and optional memory growth.

Reuse one Sdk object across all streams — you activate once, then create a processor per thread. Do not create an SDK per stream.

Prerequisites

Variable Required Default Purpose
SANAS_API_KEY Yes Licence key
SANAS_STORAGE_DIR No ./storage SDK data + logs
export SANAS_API_KEY="your-key"

python multi_stream_example.py --streams 8 --concurrency 4 --rate 2 \
    --model VI_G_SE --input test_input.wav

Command-line flags

Flag Default Meaning
--streams 4 Total number of streams to run
--concurrency = --streams Max streams running at once
--rate 0 Stream start rate per second (0 = all at once)
--model VI_G_SE Model key
--input test_input.wav Input WAV path
--max-throughput off Feed as fast as possible instead of real-time pacing
--pcm16 off Negotiate raw 16-bit PCM (cloud inference only)
--save-wav-out-period N 0 Save every Nth stream’s output for spot-checking

Install psutil (pip install psutil) to also report process memory growth — useful for spotting leaks in long soak runs. The benchmark runs fine without it; the memory lines are simply omitted.

Step 1: Create and activate one SDK

import sanas

sdk = sanas.create_sdk(
    sanas.InitParams(storage_dir=os.environ.get("SANAS_STORAGE_DIR", "./storage"))
)
res = sdk.activate_api_key(os.environ["SANAS_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)

Step 2: Define a per-stream worker

Each stream builds its own PipelineWaiter, processor, and cloud params, then feeds the shared samples. Exceptions are caught per stream so one failure doesn’t take down the others. Note collect_output is only turned on when you actually intend to save audio — leaving it off keeps the memory-growth metric honest.

def _run_stream(sdk, idx, samples, sample_rate, channels, model,
                realtime, use_pcm16, save_period, stats):
    result = {"idx": idx, "ok": False, "error": None, "wall_s": 0.0, "saved": None}
    stats.stream_started()
    t0 = time.monotonic()
    try:
        cloud_params = sanas.CloudInferencingParams()
        cloud_params.use_pcm16 = use_pcm16
        waiter = PipelineWaiter()
        attrs = sanas.ProcessorAttributes(
            audio_attributes=sanas.AudioAttributes(
                sampling_rate=sample_rate,
                channels=channels,
                model_name=model,
                cloud_inferencing_params=cloud_params,
                audio_pipeline_state_notify=waiter.callback,
            )
        )
        with sdk.create_audio_processor(attrs) as proc:
            waiter.wait_until_running(timeout=30.0)
            drained = feed_and_drain(
                proc, samples, sample_rate, channels,
                realtime=realtime, collect_output=save_period > 0,
            )
        result["wall_s"] = time.monotonic() - t0
        if save_period and idx % save_period == 0 and drained["output"]:
            out_path = f"stream_{idx}.wav"
            save_wav(out_path, drained["output"], sample_rate, channels)
            result["saved"] = out_path
        result["ok"] = True
    except Exception as e:   # report per-stream, keep others going
        result["error"] = f"{type(e).__name__}: {e}"
        result["wall_s"] = time.monotonic() - t0
    finally:
        stats.stream_finished(result["ok"])
    return result

Step 3: Track live statistics

LiveStats is a thread-safe counter aggregated across streams. Its active and peak_concurrent fields reveal how many streams actually overlapped — something per-stream result dicts can’t show on their own.

class LiveStats:
    def __init__(self):
        self._lock = threading.Lock()
        self.started = self.completed = self.failed = self.active = 0
        self.peak_concurrent = 0
        self.start_time = time.monotonic()

def stream_started(self):
        with self._lock:
            self.started += 1
            self.active += 1
            self.peak_concurrent = max(self.peak_concurrent, self.active)

def stream_finished(self, ok):
        with self._lock:
            self.active -= 1
            if ok:
                self.completed += 1
            else:
                self.failed += 1

A background MemorySampler thread records peak RSS every 0.5 s (a no-op without psutil). Sampling — rather than reading once at the end — captures the true high-water mark while streams overlap, so a leak or transient spike shows up in the report.

Step 4: Launch streams on a thread pool

Use a ThreadPoolExecutor sized to --concurrency. Optionally stagger starts with --rate to model a ramp-up instead of a thundering herd.

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=concurrency) as pool:
    futures = []
    for i in range(args.streams):
        futures.append(pool.submit(
            _run_stream, sdk, i, samples, sample_rate, channels,
            args.model, realtime, args.pcm16, args.save_wav_out_period, stats,
        ))
        if args.rate > 0 and i + 1 < args.streams:
            time.sleep(1.0 / args.rate)   # stagger stream starts
    for fut in as_completed(futures):
        r = fut.result()
        tag = "ok" if r["ok"] else f"FAILED ({r["error"]})"
        print(f"[stream {r['idx']}] {tag}  wall={r['wall_s']:.2f}s")

Step 5: Read the report

============================================================
Benchmark summary
============================================================
streams        : 8  (ok=8, failed=0)
success rate   : 100.0%
peak concurrent: 4
uptime         : 12.34s
memory (RSS)   : start=95.2 MB  peak=180.6 MB  growth=+85.4 MB

Interpreting it: success rate flags whether the configured concurrency is sustainable; peak concurrent confirms streams actually overlapped up to your --concurrency limit; memory growth should stay flat across a long soak run — steady climbing suggests a leak.

Tuning tips

Use --max-throughput to measure how fast the pipeline can process (no real-time pacing) versus the default real-time feed that models a live call. Raise --concurrency gradually and watch the success rate and memory growth to find a safe ceiling for your hardware and model. Leave --save-wav-out-period at 0 for benchmarking; set it to spot-check output quality on a sample of streams.