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