Benchmark wav

This example measuers the performance of loading WAV audio.

It compares three different approaches for loading WAV files:

  • spdl.io.load_wav(): Fast native WAV parser optimized for simple PCM formats

  • spdl.io.load_audio(): General-purpose audio loader using FFmpeg backend

  • soundfile (libsndfile): Popular third-party audio I/O library

The benchmark suite evaluates performance across multiple dimensions:

  • Various audio configurations (sample rates, channels, bit depths, durations)

  • Different thread counts (1, 2, 4, 8, 16) to measure parallel scaling

  • Statistical analysis with 95% confidence intervals using Student’s t-distribution

  • Queries per second (QPS) as the primary performance metric

Example

$ numactl --membind 0 --cpubind 0 python benchmark_wav.py --output wav_benchmark_results.csv
# Plot results
$ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot.png
# Plot results without load_wav
$ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot_2.png --filter '3. spdl.io.load_wav'

Result

The following plot shows the QPS (measured by the number of files processed) of each functions with different audio durations.

../_static/data/example-benchmark-wav.webp

The spdl.io.load_wav() is a lot faster than the others, because all it does is reinterpret the input byte string as array. It shows the same performance for audio with longer duration.

And since parsing WAV is instant, the spdl.io.load_wav function spends more time on creation of NumPy Array. It needs to acquire the GIL, thus the performance does not scale in multi-threading. (This performance pattern of this function is pretty same as the spdl.io.load_npz.)

The following is the same plot without load_wav.

../_static/data/example-benchmark-wav-2.webp

libsoundfile has to process data iteratively (using io.BytesIO) because it does not support directly loading from byte string, so it takes longer to process longer audio data. The performance trend (single thread being the fastest) suggests that it does not release the GIL majority of the time.

The spdl.io.load_audio() function (the generic FFmpeg-based implementation) does a lot of work so its overall performance is not as good, but it scales in multi-threading as it releases the GIL almost entirely.

Free-threaded Python

The cases above where multi-threading does not help are caused by contention on the GIL — most visibly soundfile, which holds the GIL for most of its work, so a single thread ends up being the fastest. This contention disappears on a free-threaded (no-GIL) build of Python (e.g. 3.14t). The following is the same benchmark run on 3.14t.

../_static/data/example-benchmark-wav-freethreading.png

Without the GIL, soundfile now scales with the number of threads instead of slowing down, improving its throughput at 16 threads by more than an order of magnitude. spdl.io.load_audio(), which already releases the GIL, is essentially unchanged.

The numbers from both runs are tabulated below.

Soundfile

Duration

1s

10s

60s

Build

3.14

3.14t

3.14

3.14t

3.14

3.14t

1

9,904

11,910

5,864

6,500

1,869

2,054

2

5,481

19,120

4,188

10,645

1,481

3,599

4

3,142

30,447

2,491

16,438

1,201

6,280

8

2,375

45,220

1,993

25,208

1,163

10,725

16

2,643

54,900

2,121

40,948

1,017

14,103

spdl.io.load_audio

Duration

1s

10s

60s

Build

3.14

3.14t

3.14

3.14t

3.14

3.14t

1

116

119

43

45

38

38

2

258

271

85

88

74

75

4

512

511

168

176

144

152

8

1,011

1,035

337

352

286

296

16

1,915

2,012

652

698

538

574

spdl.io.load_wav

Duration

1s

10s

60s

Build

3.14

3.14t

3.14

3.14t

3.14

3.14t

1

51,585

79,572

47,905

107,119

49,404

76,757

2

49,882

98,500

49,057

97,641

45,787

98,364

4

51,084

87,790

49,228

91,243

50,026

94,748

8

48,710

77,335

49,124

73,603

47,977

76,574

16

38,366

65,094

37,587

62,512

38,210

63,498

Source

Source

Click here to see the source.
  1# Copyright (c) Meta Platforms, Inc. and affiliates.
  2# All rights reserved.
  3#
  4# This source code is licensed under the BSD-style license found in the
  5# LICENSE file in the root directory of this source tree.
  6
  7
  8"""This example measuers the performance of loading WAV audio.
  9
 10It compares three different approaches for loading WAV files:
 11
 12- :py:func:`spdl.io.load_wav`: Fast native WAV parser optimized for simple PCM formats
 13- :py:func:`spdl.io.load_audio`: General-purpose audio loader using FFmpeg backend
 14- ``soundfile`` (``libsndfile``): Popular third-party audio I/O library
 15
 16The benchmark suite evaluates performance across multiple dimensions:
 17
 18- Various audio configurations (sample rates, channels, bit depths, durations)
 19- Different thread counts (1, 2, 4, 8, 16) to measure parallel scaling
 20- Statistical analysis with 95% confidence intervals using Student's t-distribution
 21- Queries per second (QPS) as the primary performance metric
 22
 23**Example**
 24
 25.. code-block:: shell
 26
 27   $ numactl --membind 0 --cpubind 0 python benchmark_wav.py --output wav_benchmark_results.csv
 28   # Plot results
 29   $ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot.png
 30   # Plot results without load_wav
 31   $ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot_2.png --filter '3. spdl.io.load_wav'
 32
 33**Result**
 34
 35The following plot shows the QPS (measured by the number of files processed) of each
 36functions with different audio durations.
 37
 38.. image:: ../../_static/data/example-benchmark-wav.webp
 39
 40
 41The :py:func:`spdl.io.load_wav` is a lot faster than the others, because all it
 42does is reinterpret the input byte string as array.
 43It shows the same performance for audio with longer duration.
 44
 45And since parsing WAV is instant, the spdl.io.load_wav function spends more time on
 46creation of NumPy Array.
 47It needs to acquire the GIL, thus the performance does not scale in multi-threading.
 48(This performance pattern of this function is pretty same as the
 49:ref:`spdl.io.load_npz <example-benchmark-numpy>`.)
 50
 51The following is the same plot without ``load_wav``.
 52
 53.. image:: ../../_static/data/example-benchmark-wav-2.webp
 54
 55``libsoundfile`` has to process data iteratively (using ``io.BytesIO``) because
 56it does not support directly loading from byte string, so it takes longer to process
 57longer audio data.
 58The performance trend (single thread being the fastest) suggests that
 59it does not release the GIL majority of the time.
 60
 61The :py:func:`spdl.io.load_audio` function (the generic FFmpeg-based implementation) does
 62a lot of work so its overall performance is not as good,
 63but it scales in multi-threading as it releases the GIL almost entirely.
 64
 65**Free-threaded Python**
 66
 67The cases above where multi-threading does not help are caused by contention on
 68the GIL — most visibly ``soundfile``, which holds the GIL for most of its work,
 69so a single thread ends up being the fastest. This contention disappears on a
 70free-threaded (no-GIL) build of Python (e.g. ``3.14t``). The following is the
 71same benchmark run on ``3.14t``.
 72
 73.. image:: ../../_static/data/example-benchmark-wav-freethreading.png
 74
 75Without the GIL, ``soundfile`` now scales with the number of threads instead of
 76slowing down, improving its throughput at 16 threads by more than an order of
 77magnitude. :py:func:`spdl.io.load_audio`, which already releases the GIL, is
 78essentially unchanged.
 79
 80The numbers from both runs are tabulated below.
 81
 82**Soundfile**
 83
 84+----------+--------+---------+--------+--------+--------+---------+
 85| Duration |       1s         |       10s       |       60s        |
 86+----------+--------+---------+--------+--------+--------+---------+
 87|  Build   |  3.14  |   3.14t |   3.14 |  3.14t |  3.14  |   3.14t |
 88+==========+========+=========+========+========+========+=========+
 89|        1 |  9,904 |  11,910 |  5,864 |  6,500 |  1,869 |   2,054 |
 90+----------+--------+---------+--------+--------+--------+---------+
 91|        2 |  5,481 |  19,120 |  4,188 | 10,645 |  1,481 |   3,599 |
 92+----------+--------+---------+--------+--------+--------+---------+
 93|        4 |  3,142 |  30,447 |  2,491 | 16,438 |  1,201 |   6,280 |
 94+----------+--------+---------+--------+--------+--------+---------+
 95|        8 |  2,375 |  45,220 |  1,993 | 25,208 |  1,163 |  10,725 |
 96+----------+--------+---------+--------+--------+--------+---------+
 97|       16 |  2,643 |  54,900 |  2,121 | 40,948 |  1,017 |  14,103 |
 98+----------+--------+---------+--------+--------+--------+---------+
 99
100
101**spdl.io.load_audio**
102
103+----------+--------+---------+--------+--------+--------+---------+
104| Duration |       1s         |       10s       |       60s        |
105+----------+--------+---------+--------+--------+--------+---------+
106|  Build   |  3.14  |   3.14t |   3.14 |  3.14t |  3.14  |   3.14t |
107+==========+========+=========+========+========+========+=========+
108|        1 |    116 |     119 |     43 |     45 |     38 |      38 |
109+----------+--------+---------+--------+--------+--------+---------+
110|        2 |    258 |     271 |     85 |     88 |     74 |      75 |
111+----------+--------+---------+--------+--------+--------+---------+
112|        4 |    512 |     511 |    168 |    176 |    144 |     152 |
113+----------+--------+---------+--------+--------+--------+---------+
114|        8 |  1,011 |   1,035 |    337 |    352 |    286 |     296 |
115+----------+--------+---------+--------+--------+--------+---------+
116|       16 |  1,915 |   2,012 |    652 |    698 |    538 |     574 |
117+----------+--------+---------+--------+--------+--------+---------+
118
119**spdl.io.load_wav**
120
121+----------+--------+--------+--------+---------+--------+--------+
122| Duration |       1s        |       10s        |       60s       |
123+----------+--------+--------+--------+---------+--------+--------+
124|  Build   |  3.14  |  3.14t |   3.14 |   3.14t |  3.14  |  3.14t |
125+==========+========+========+========+=========+========+========+
126|        1 | 51,585 | 79,572 | 47,905 | 107,119 | 49,404 | 76,757 |
127+----------+--------+--------+--------+---------+--------+--------+
128|        2 | 49,882 | 98,500 | 49,057 |  97,641 | 45,787 | 98,364 |
129+----------+--------+--------+--------+---------+--------+--------+
130|        4 | 51,084 | 87,790 | 49,228 |  91,243 | 50,026 | 94,748 |
131+----------+--------+--------+--------+---------+--------+--------+
132|        8 | 48,710 | 77,335 | 49,124 |  73,603 | 47,977 | 76,574 |
133+----------+--------+--------+--------+---------+--------+--------+
134|       16 | 38,366 | 65,094 | 37,587 |  62,512 | 38,210 | 63,498 |
135+----------+--------+--------+--------+---------+--------+--------+
136
137
138"""
139
140__all__ = [
141    "BenchmarkConfig",
142    "create_wav_data",
143    "load_sf",
144    "load_spdl_audio",
145    "load_spdl_wav",
146    "main",
147]
148
149import argparse
150import io
151import os
152from collections.abc import Callable
153from dataclasses import dataclass
154
155import numpy as np
156import scipy.io.wavfile
157import soundfile as sf
158import spdl.io
159from numpy.typing import NDArray
160
161try:
162    from examples.benchmark_utils import (  # pyre-ignore[21]
163        BenchmarkResult,
164        BenchmarkRunner,
165        ExecutorType,
166        get_default_result_path,
167        save_results_to_csv,
168    )
169except ImportError:
170    from spdl.examples.benchmark_utils import (
171        BenchmarkResult,
172        BenchmarkRunner,
173        ExecutorType,
174        get_default_result_path,
175        save_results_to_csv,
176    )
177
178
179DEFAULT_RESULT_PATH: str = get_default_result_path(__file__)
180
181
182@dataclass(frozen=True)
183class BenchmarkConfig:
184    """BenchmarkConfig()
185
186    Configuration for a single WAV benchmark run.
187
188    Combines both audio file parameters and benchmark execution parameters.
189    """
190
191    function_name: str
192    """Name of the function being tested"""
193
194    function: Callable[[bytes], NDArray]
195    """The actual function to benchmark"""
196
197    sample_rate: int
198    """Audio sample rate in Hz"""
199
200    num_channels: int
201    """Number of audio channels"""
202
203    bits_per_sample: int
204    """Bit depth per sample (16 or 32)"""
205
206    duration_seconds: float
207    """Duration of the audio file in seconds"""
208
209    num_threads: int
210    """Number of concurrent threads"""
211
212    iterations: int
213    """Number of iterations per run"""
214
215    num_runs: int
216    """Number of runs for statistical analysis"""
217
218
219def create_wav_data(
220    sample_rate: int = 44100,
221    num_channels: int = 2,
222    bits_per_sample: int = 16,
223    duration_seconds: float = 1.0,
224) -> tuple[bytes, NDArray]:
225    """Create a WAV file in memory for benchmarking.
226
227    Args:
228        sample_rate: Sample rate in Hz
229        num_channels: Number of audio channels
230        bits_per_sample: Bits per sample (16 or 32)
231        duration_seconds: Duration of audio in seconds
232
233    Returns:
234        Tuple of (WAV file as bytes, audio samples array)
235    """
236    num_samples = int(sample_rate * duration_seconds)
237
238    dtype_map = {
239        16: np.int16,
240        32: np.int32,
241    }
242    dtype = dtype_map[bits_per_sample]
243    max_amplitude = 32767 if bits_per_sample == 16 else 2147483647
244
245    t = np.linspace(0, duration_seconds, num_samples)
246    frequencies = np.asarray(440.0 + np.arange(num_channels) * 110.0)
247    sine_waves = np.sin(2 * np.pi * frequencies[:, np.newaxis] * t)
248    samples = (sine_waves.T * max_amplitude).astype(dtype)
249
250    wav_buffer = io.BytesIO()
251    scipy.io.wavfile.write(wav_buffer, sample_rate, samples)
252    wav_data = wav_buffer.getvalue()
253
254    return wav_data, samples
255
256
257def load_sf(wav_data: bytes) -> NDArray:
258    """Load WAV data using soundfile library.
259
260    Args:
261        wav_data: WAV file data as bytes
262
263    Returns:
264        Audio samples array as int16 numpy array
265    """
266    audio_file = io.BytesIO(wav_data)
267    data, _ = sf.read(audio_file, dtype="int16")
268    return data
269
270
271def load_spdl_audio(wav_data: bytes) -> NDArray:
272    """Load WAV data using :py:func:`spdl.io.load_audio` function.
273
274    Args:
275        wav_data: WAV file data as bytes
276
277    Returns:
278        Audio samples array as numpy array
279    """
280    return spdl.io.to_numpy(spdl.io.load_audio(wav_data, filter_desc=None))
281
282
283def load_spdl_wav(wav_data: bytes) -> NDArray:
284    """Load WAV data using :py:func:`spdl.io.load_wav` function.
285
286    Args:
287        wav_data: WAV file data as bytes
288
289    Returns:
290        Audio samples array as numpy array
291    """
292    return spdl.io.to_numpy(spdl.io.load_wav(wav_data))
293
294
295def _parse_args() -> argparse.Namespace:
296    """Parse command line arguments for the benchmark script.
297
298    Returns:
299        Parsed command line arguments
300    """
301    parser = argparse.ArgumentParser(description="Benchmark WAV loading performance")
302    parser.add_argument(
303        "--output",
304        type=lambda p: os.path.realpath(p),
305        default=DEFAULT_RESULT_PATH,
306        help="Output file path.",
307    )
308    return parser.parse_args()
309
310
311def main() -> None:
312    """Run comprehensive benchmark suite for WAV loading performance.
313
314    Benchmarks multiple configurations of audio files with different durations,
315    comparing spdl.io.load_wav, spdl.io.load_audio, and soundfile libraries
316    across various thread counts (1, 2, 4, 8, 16).
317    """
318    args = _parse_args()
319
320    # Define audio configurations to test
321    audio_configs = [
322        # (sample_rate, num_channels, bits_per_sample, duration_seconds)
323        # (8000, 1, 16, 1.0),  # Low quality mono
324        # (16000, 1, 16, 1.0),  # Speech quality mono
325        # (48000, 2, 16, 1.0),  # High quality stereo
326        # (48000, 8, 16, 1.0),  # Multi-channel audio
327        (44100, 2, 16, 1.0),  # CD quality stereo
328        (44100, 2, 16, 10.0),  #
329        (44100, 2, 16, 60.0),  #
330        # (44100, 2, 24, 1.0),  # 24-bit audio
331    ]
332
333    thread_counts = [1, 2, 4, 8, 16]
334
335    # Define benchmark function configurations
336    # (function_name, function, iterations_multiplier, num_runs)
337    benchmark_functions = [
338        ("3. spdl.io.load_wav", load_spdl_wav, 100, 100),  # Fast but unstable
339        ("2. spdl.io.load_audio", load_spdl_audio, 10, 5),  # Slower but stable
340        ("1. soundfile", load_sf, 10, 5),  # Slower but stable
341    ]
342
343    results: list[BenchmarkResult[BenchmarkConfig]] = []
344
345    for sample_rate, num_channels, bits_per_sample, duration_seconds in audio_configs:
346        # Create WAV data for this audio configuration
347        wav_data, ref = create_wav_data(
348            sample_rate=sample_rate,
349            num_channels=num_channels,
350            bits_per_sample=bits_per_sample,
351            duration_seconds=duration_seconds,
352        )
353
354        print(
355            f"\n{sample_rate}Hz, {num_channels}ch, {bits_per_sample}bit, {duration_seconds}s"
356        )
357        print(
358            f"Threads,"
359            f"SPDL WAV QPS ({duration_seconds} sec),CI Lower,CI Upper,"
360            f"SPDL Audio QPS ({duration_seconds} sec),CI Lower,CI Upper,"
361            f"soundfile QPS ({duration_seconds} sec),CI Lower,CI Upper"
362        )
363
364        for num_threads in thread_counts:
365            thread_results: list[BenchmarkResult[BenchmarkConfig]] = []
366
367            with BenchmarkRunner(
368                executor_type=ExecutorType.THREAD,
369                num_workers=num_threads,
370            ) as runner:
371                for (
372                    function_name,
373                    function,
374                    iterations_multiplier,
375                    num_runs,
376                ) in benchmark_functions:
377                    config = BenchmarkConfig(
378                        function_name=function_name,
379                        function=function,
380                        sample_rate=sample_rate,
381                        num_channels=num_channels,
382                        bits_per_sample=bits_per_sample,
383                        duration_seconds=duration_seconds,
384                        num_threads=num_threads,
385                        iterations=iterations_multiplier * num_threads,
386                        num_runs=num_runs,
387                    )
388
389                    result, output = runner.run(
390                        config,
391                        lambda fn=function, data=wav_data: fn(data),
392                        config.iterations,
393                        num_runs=config.num_runs,
394                    )
395
396                    output_to_validate = output
397                    if output_to_validate.ndim == 1:
398                        output_to_validate = output_to_validate[:, None]
399                    np.testing.assert_array_equal(output_to_validate, ref)
400
401                    thread_results.append(result)
402                    results.append(result)
403
404            # Print results for this thread count (all 3 benchmarks)
405            spdl_wav_result = thread_results[0]
406            spdl_audio_result = thread_results[1]
407            soundfile_result = thread_results[2]
408            print(
409                f"{num_threads},"
410                f"{spdl_wav_result.qps:.2f},{spdl_wav_result.ci_lower:.2f},{spdl_wav_result.ci_upper:.2f},"
411                f"{spdl_audio_result.qps:.2f},{spdl_audio_result.ci_lower:.2f},{spdl_audio_result.ci_upper:.2f},"
412                f"{soundfile_result.qps:.2f},{soundfile_result.ci_lower:.2f},{soundfile_result.ci_upper:.2f}"
413            )
414
415    save_results_to_csv(results, args.output)
416    print(
417        f"\nBenchmark complete. To generate plots, run:\n"
418        f"python benchmark_wav_plot.py --input {args.output} "
419        f"--output {args.output.replace('.csv', '.png')}"
420    )
421
422
423if __name__ == "__main__":
424    main()

API Reference

Functions

create_wav_data(sample_rate: int = 44100, num_channels: int = 2, bits_per_sample: int = 16, duration_seconds: float = 1.0) tuple[bytes, NDArray][source]

Create a WAV file in memory for benchmarking.

Parameters:
  • sample_rate – Sample rate in Hz

  • num_channels – Number of audio channels

  • bits_per_sample – Bits per sample (16 or 32)

  • duration_seconds – Duration of audio in seconds

Returns:

Tuple of (WAV file as bytes, audio samples array)

load_sf(wav_data: bytes) NDArray[source]

Load WAV data using soundfile library.

Parameters:

wav_data – WAV file data as bytes

Returns:

Audio samples array as int16 numpy array

load_spdl_audio(wav_data: bytes) NDArray[source]

Load WAV data using spdl.io.load_audio() function.

Parameters:

wav_data – WAV file data as bytes

Returns:

Audio samples array as numpy array

load_spdl_wav(wav_data: bytes) NDArray[source]

Load WAV data using spdl.io.load_wav() function.

Parameters:

wav_data – WAV file data as bytes

Returns:

Audio samples array as numpy array

main() None[source]

Run comprehensive benchmark suite for WAV loading performance.

Benchmarks multiple configurations of audio files with different durations, comparing spdl.io.load_wav, spdl.io.load_audio, and soundfile libraries across various thread counts (1, 2, 4, 8, 16).

Classes

class BenchmarkConfig[source]

Configuration for a single WAV benchmark run.

Combines both audio file parameters and benchmark execution parameters.

bits_per_sample: int

Bit depth per sample (16 or 32)

duration_seconds: float

Duration of the audio file in seconds

function: Callable[[bytes], NDArray]

The actual function to benchmark

function_name: str

Name of the function being tested

iterations: int

Number of iterations per run

num_channels: int

Number of audio channels

num_runs: int

Number of runs for statistical analysis

num_threads: int

Number of concurrent threads

sample_rate: int

Audio sample rate in Hz