---
title: audio
---

<div class="python-reference">
<a id="fastvideo.eval.io.audio-classes"></a>
<a id="fastvideo.eval.io.audio-functions"></a>
<a id="fastvideo.eval.io.audio.extract_audio_track--parameters"></a>
<a id="fastvideo.eval.io.audio.extract_audio_track--returns"></a>
<a id="fastvideo.eval.io.audio.extract_audio_track--raises"></a>
<a id="fastvideo.eval.io.audio"></a>
<p>Audio-extraction helper for video files.</p>
<p>Audio metrics (<code>audio.clap_score</code>, <code>audio.frechet_distance</code>, …) read
file paths under <code>sample["audio"]</code> and do their own per-metric
preprocessing (CLAP at 48 kHz, PaSST at 32 kHz, whisper at 16 kHz mono,
…).  When the source video carries an audio track (V2A / T2A-V model
outputs), :func:<code>samples_from(extract_audio=True)</code> calls
:func:<code>extract_audio_track</code> to pull a <code>.wav</code> next to it once per video.</p>
<p>Why a separate utility instead of in-pool decode: every audio metric
wants a different sample rate / channel count / format, so the pool
can't usefully pre-load audio into a single canonical tensor the way it
pre-loads video frames.  Paths-in / paths-out is the lingua franca for
audio in this codebase.</p>
<h2 id="fastvideo.eval.io.audio.NoAudioStreamError"><code>fastvideo.eval.io.audio.NoAudioStreamError</code></h2>
<pre class="doc-signature"><code>NoAudioStreamError()</code></pre>
<p>Raised when a video file has no audio stream to extract.</p>
<details><summary>Source</summary><pre><code>class NoAudioStreamError(ValueError):
    &quot;&quot;&quot;Raised when a video file has no audio stream to extract.&quot;&quot;&quot;</code></pre></details>
<h2 id="fastvideo.eval.io.audio.extract_audio_track"><code>fastvideo.eval.io.audio.extract_audio_track</code></h2>
<pre class="doc-signature"><code>extract_audio_track(video_path: str | Path, *, output_dir: str | Path, sample_rate: int | None = None, codec: str = &#x27;pcm_s16le&#x27;) -&gt; Path</code></pre>
<p>Pull the first audio stream from <em>video_path</em> to a <code>.wav</code> in <em>output_dir</em>.</p>
<p>Idempotent — if <code>output_dir / &#123;stem&#125;.wav</code> already exists, the
existing file is returned without re-decoding.  This makes the
helper safe to call repeatedly from parallel workers on overlapping
inputs (the cache hit is the fast path).</p>
<h2>Parameters</h2>
<p>video_path :
    Source video file (anything PyAV can open: mp4, mkv, webm, …).
output_dir :
    Directory the <code>.wav</code> lands in.  Created if it doesn't exist.
sample_rate :
    If set, resample to this rate.  Default (<code>None</code>) preserves the
    source rate — most audio metrics resample again internally to
    their own target rate, so adding a resample step here would just
    be wasted work.
codec :
    PCM codec for the output.  <code>pcm_s16le</code> (default) gives 16-bit
    little-endian PCM, the most broadly compatible WAV format.</p>
<h2>Returns</h2>
<p>Path
    Absolute path to the extracted <code>.wav</code>.</p>
<h2>Raises</h2>
<p>NoAudioStreamError
    If <em>video_path</em> has no audio stream.</p>
<details><summary>Source</summary><pre><code>def extract_audio_track(
    video_path: str | Path,
    *,
    output_dir: str | Path,
    sample_rate: int | None = None,
    codec: str = &quot;pcm_s16le&quot;,
) -&gt; Path:
    &quot;&quot;&quot;Pull the first audio stream from *video_path* to a ``.wav`` in *output_dir*.

    Idempotent — if ``output_dir / &#123;stem&#125;.wav`` already exists, the
    existing file is returned without re-decoding.  This makes the
    helper safe to call repeatedly from parallel workers on overlapping
    inputs (the cache hit is the fast path).

    Parameters
    ----------
    video_path :
        Source video file (anything PyAV can open: mp4, mkv, webm, …).
    output_dir :
        Directory the ``.wav`` lands in.  Created if it doesn&#x27;t exist.
    sample_rate :
        If set, resample to this rate.  Default (``None``) preserves the
        source rate — most audio metrics resample again internally to
        their own target rate, so adding a resample step here would just
        be wasted work.
    codec :
        PCM codec for the output.  ``pcm_s16le`` (default) gives 16-bit
        little-endian PCM, the most broadly compatible WAV format.

    Returns
    -------
    Path
        Absolute path to the extracted ``.wav``.

    Raises
    ------
    NoAudioStreamError
        If *video_path* has no audio stream.
    &quot;&quot;&quot;
    import av

    in_path = Path(video_path)
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    out_path = (out_dir / f&quot;&#123;in_path.stem&#125;.wav&quot;).resolve()
    if out_path.exists():
        return out_path

    container = av.open(str(in_path))
    try:
        audio_streams = &#91;s for s in container.streams if s.type == &quot;audio&quot;&#93;
        if not audio_streams:
            raise NoAudioStreamError(f&quot;No audio stream in &#123;in_path&#125;&quot;)
        in_stream = audio_streams&#91;0&#93;
        target_rate = sample_rate or in_stream.rate

        out = av.open(str(out_path), &quot;w&quot;, format=&quot;wav&quot;)
        try:
            out_stream = out.add_stream(codec, rate=target_rate)
            # Match input layout (mono / stereo / surround) — audio metrics
            # that want mono will downmix themselves.
            if in_stream.layout is not None:
                out_stream.layout = in_stream.layout
            resampler = None
            if sample_rate is not None and sample_rate != in_stream.rate:
                resampler = av.AudioResampler(format=out_stream.format, layout=out_stream.layout, rate=target_rate)
            for frame in container.decode(in_stream):
                frames_out = resampler.resample(frame) if resampler is not None else &#91;frame&#93;
                for f in frames_out:
                    # PyAV needs pts cleared so the encoder reassigns.
                    f.pts = None
                    for packet in out_stream.encode(f):
                        out.mux(packet)
            for packet in out_stream.encode():  # flush
                out.mux(packet)
        finally:
            out.close()
    finally:
        container.close()
    return out_path</code></pre></details>
</div>
