Skip to content

Reject non-positive audio recording durations before writing WAVs #113

Description

@Justinabox

Summary

AudioPipeline.record() fails closed when the pipeline is inactive, but it still accepts a non-positive max_duration while the pipeline is running. For max_duration=0 (and similarly negative values), it opens the WAV file, immediately exits the loop, and returns a success-looking 44-byte/0-frame recording.

That creates the same PBX/voicemail hazard as an inactive pipeline: callers can treat an empty artifact as a successful recording instead of surfacing a configuration/programming error.

Evidence

Affected code:

  • callstack/voice/audio.py:66-118 checks self._running, but does not validate max_duration before opening wave.open(output_path, "wb").
  • tests/test_audio.py:165-188 covers inactive-pipeline fail-closed behavior, but not non-positive durations.

No-hardware probe on main (92e0f02042bb7496d7bca2fdd7f29a0625592672):

PYTHONPATH=. uv run --no-project --with pyserial-asyncio python - <<'PY'
import asyncio, os, tempfile, wave
from callstack.events.bus import EventBus
from callstack.transport.mock import MockTransport
from callstack.voice.audio import AudioPipeline

async def main():
    out = os.path.join(tempfile.gettempdir(), 'callstack-zero-duration-record.wav')
    try:
        os.remove(out)
    except FileNotFoundError:
        pass
    pipeline = AudioPipeline(MockTransport(), EventBus())
    await pipeline.start()
    try:
        result = await pipeline.record(out, max_duration=0)
        print('record_returned:', result)
        print('file_exists:', os.path.exists(out))
        with wave.open(out, 'rb') as wf:
            print('frames:', wf.getnframes())
            print('file_bytes:', os.path.getsize(out))
    finally:
        await pipeline.stop()

asyncio.run(main())
PY

Actual output:

record_returned: /var/folders/.../callstack-zero-duration-record.wav
file_exists: True
frames: 0
file_bytes: 44

Expected behavior

A recording request with max_duration <= 0 (and ideally non-finite values such as NaN/inf) should be rejected before writing or returning an output path.

Suggested fix direction

Validate max_duration at the top of AudioPipeline.record() before wave.open(...), similar to the positive-finite config validation pattern in callstack/config.py. Raise AudioPipelineError or ValueError consistently with the audio API's existing failure style, and ensure no empty WAV file is left behind for invalid durations.

Acceptance criteria

  • AudioPipeline.record(..., max_duration=0) fails before creating a 0-frame WAV.
  • Negative and non-finite durations are rejected deterministically.
  • CallSession.record() propagates the invalid-duration failure.
  • Positive short durations still record/return normally.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_audio.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Duplicate check

Checked open/closed issues for AudioPipeline record max_duration zero empty WAV, recording max_duration empty WAV non-positive, and related closed issue #108. #108 fixed inactive-pipeline recording; this is a distinct still-running invalid-duration path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions