Metadata-Version: 2.4
Name: sound_rs
Version: 0.3.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: numpy>=1.20 ; extra == 'numpy'
Provides-Extra: numpy
License-File: LICENSE
Summary: Fast Rust audio for Python: play and stream MP3, FLAC, WAV, OGG and M4A without ffmpeg
Keywords: audio,playback,player,streaming,mp3,flac,wav,aac,m4a,ogg,numpy,resampling,rust,pyo3,sound,dsp
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/Neksand/sound_rs/releases
Project-URL: Documentation, https://neksand.github.io/sound_rs/
Project-URL: Homepage, https://pypi.org/project/sound-rs/
Project-URL: Issues, https://github.com/Neksand/sound_rs/issues
Project-URL: Repository, https://github.com/Neksand/sound_rs
Project-URL: Source, https://github.com/Neksand/sound_rs

# 🎵 sound_rs

[![PyPI](https://img.shields.io/pypi/v/sound_rs)](https://pypi.org/project/sound-rs/)
[![Downloads](https://img.shields.io/pypi/dm/sound_rs)](https://pypi.org/project/sound-rs/)
[![Python](https://img.shields.io/pypi/pyversions/sound_rs)](https://pypi.org/project/sound-rs/)
[![Platforms](https://img.shields.io/badge/platforms-Linux%20%7C%20Windows%20%7C%20macOS-blue)](#installation)
[![Typed](https://img.shields.io/badge/typing-typed-blue)](sound_rs.pyi)
[![Docs](https://img.shields.io/badge/docs-neksand.github.io%2Fsound__rs-ff5a36)](https://neksand.github.io/sound_rs/)
[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

**sound_rs** is a fast, Rust-powered audio library for Python that I built and
maintain — decoding with [symphonia](https://github.com/pdeljanov/Symphonia) (plus
minimp3 for MP3), resampling with [rubato](https://github.com/HEnquist/rubato),
output through [cpal](https://github.com/RustAudio/cpal).

**Documentation:** [neksand.github.io/sound_rs](https://neksand.github.io/sound_rs/) — guides,
API reference, the `soundrs` command and the benchmark methodology.

---

## 🚀 Features

- **Formats:** WAV, MP3, FLAC, OGG/Vorbis, M4A, AAC — any bit depth, including 24-bit
- **Streaming playback:** a two-hour file costs ~4 MB of RAM, not gigabytes, and the
  first sound arrives in ~9 ms instead of after the whole decode
- **`Player`:** one output stream stays open — `play`, `pause`, `resume`, `seek`,
  `stop`, `volume`, `position`, `duration`
- **Decode to NumPy:** `load(path, sample_rate=, mono=, offset=, duration=)` — resample to
  16 kHz mono in one call, or decode only a window
- **Parallel decode:** `load_many(paths, sample_rate=, mono=)` uses every core, GIL released
- **Recording:** `Recorder()` captures from the microphone to a float32 array
- **Queues and live input:** `Player.enqueue(path)` chains tracks; `begin_feed/feed` push
  samples from Python with backpressure
- **Gapless (MP3, OGG, AAC/MP4):** encoder delay and padding are trimmed, so decoded
  audio lines up with the original
- **Fast metadata:** `get_audio_info` reads the container header, ~1 ms regardless of
  file length
- **Resampling:** rubato's FFT resampler with a proper anti-aliasing filter
- **Device-native output:** the stream opens in whatever sample format the device
  wants (f32, f64, i16, i32, u8, u16)
- **Errors:** everything raises `sound_rs.SoundRsError` with a stable `.code`
  (`io`, `format`, `device`, `stalled`, `unsupported`, `usage`)
- **Tested guarantees:** no panics on corrupt input (mutation fuzzing), deterministic decode
  (PCM hashes in CI), underrun reporting. The full list is on the docs site.
- **No ffmpeg, no system libraries:** MP3, FLAC, OGG, AAC and WAV are decoded inside
  the wheel; `pip install sound_rs` needs nothing else
- **Typed:** ships `sound_rs.pyi` and `py.typed`, so editors autocomplete everything

---

## 🧠 Installation

```bash
pip install sound_rs
```

Wheels are built with the stable ABI (`abi3-py310`): one per platform — Linux
x86_64 and aarch64, Windows amd64, macOS arm64 and Intel — each covering Python
3.10 and newer. Other platforms build from source.

---

## ⌨️ Command line

Installing the package also installs `soundrs`, so you can check the setup and read the docs
without opening Python:

```bash
soundrs --help       # commands and the documentation link
soundrs devices      # output devices, default marked with *
soundrs info a.flac  # rate, channels, duration from the header
soundrs play a.flac --volume 0.5
soundrs docs         # prints https://neksand.github.io/sound_rs/
```

Exit codes: `0` success, `1` runtime failure, `2` bad usage.

## 💻 Usage

### One-shot playback

```python
import sound_rs

sound_rs.play_audio("sound.wav")          # blocks, GIL released
sound_rs.play_audio("sound.mp3", device="MacBook Pro Speakers")
```

### The player

```python
import sound_rs

with sound_rs.Player() as player:
    player.play_file("music.flac")   # returns immediately, streams from disk
    player.volume = 0.7
    player.seek(42.0)
    player.pause()
    player.resume()
    print(player.position, player.duration)
    player.wait()                    # block until it ends

sound_rs.list_devices()              # [(0, "MacBook Pro Speakers", True), ...]
```

### Record

```python
import sound_rs

with sound_rs.Recorder() as recorder:
    audio, rate = recorder.record(5.0)     # float32, (frames, channels)
```

### Queue and live input

```python
import numpy as np
import sound_rs

with sound_rs.Player() as player:
    player.play_file("a.flac")
    player.enqueue("b.mp3")                # plays next, own rate and layout
    player.wait()

player = sound_rs.Player()
player.begin_feed(48000)
player.feed(np.zeros(4800, dtype=np.float32))   # blocks when the buffer is full
player.end_feed()
player.wait()
```

### Inspect and decode

```python
sample_rate, channels, duration = sound_rs.get_audio_info("music.mp3")  # no decode
audio, rate = sound_rs.load("music.flac")                               # (frames, channels) float32
audio, rate = sound_rs.load("music.flac", sample_rate=16000, mono=True) # dataset shape

pairs = sound_rs.load_many(["a.flac", "b.flac", "c.flac"])              # parallel
```

### From a NumPy array

```python
import numpy as np

rate = 44100
t = np.linspace(0, 2.0, rate * 2, endpoint=False, dtype=np.float32)
sound_rs.play_array(np.sin(2 * np.pi * 440 * t), rate)   # mono, 1D
sound_rs.play_array(np.stack([left, right]), rate)       # 2D, channels first
sound_rs.play_sine_wave(440.0, 1.0, 44100)
```

---

## 🧩 Benchmark

Every measurement runs in its own process: peak RSS is process-wide, so timing
and memory cannot share one. Median and p95 over repeated runs, competitors run
side by side on the same files.

```bash
python bench/run.py                 # sound_rs only
python bench/run.py --compare       # + soundfile / sounddevice
python bench/run.py --reps 9
```

**Hardware I measured on:** Apple M5 Pro, macOS 26.6.2, Python 3.14.7, release
build (`lto = "fat"`, `opt-level = 3`). Competitors: libsndfile 1.2
(`soundfile` 0.14), PortAudio (`sounddevice`). Corpus: 3 s and 10 min stereo
44.1 kHz, generated by the harness.

**Scoreboard** — every axis in this section, side by side:

| axis | sound_rs | best competitor | ratio |
|---|---|---|---|
| import | 1.1 ms | 31.8 ms (soundfile) | 29× |
| metadata (60 s file) | 1.2 ms | 31.7 ms (soundfile) | 26× |
| decode WAV | 130 ms | 157 ms | 1.2× |
| decode FLAC | 265 ms | 455 ms | 1.7× |
| decode MP3 | 241 ms | 268 ms | 1.1× |
| decode AAC | 349 ms | not supported | — |
| time to first sound (10 min) | 11 ms | 302 ms | 27× |
| peak RAM (10 min file) | 43 MB | 255 MB | 6× |
| playback overhead | 30 ms | 241 ms | 8× |
| batch decode (2 files) | 177 ms | 299 ms | 1.7× |

**Import time** (cold process):

| | time | peak RSS |
|---|---|---|
| sound_rs | **1.1 ms** | 25 MB |
| soundfile | 31.8 ms | 34 MB |
| sounddevice | 75.3 ms | 32 MB |

**Metadata** (60 s file): sound_rs **1.2 ms**, soundfile 31.7 ms.

**Decode** 60 s stereo, median / p95:

| format | sound_rs | libsndfile | result | PCM check |
|---|---|---|---|---|
| WAV | **131 / 160 ms** | 155 ms | 1.2× faster | identical |
| FLAC | **265 / 267 ms** | 465 ms | 1.8× faster | identical |
| MP3 | **247 / 249 ms** | 272 ms | 1.1× faster | 1.6e-05, 47-sample offset |
| M4A (AAC) | 361 / 365 ms | unsupported | sound_rs only | matches ffmpeg to 8e-08 |

**Streaming a 10-minute FLAC** — time to the first sound and peak memory:

| | first sound | peak RSS |
|---|---|---|
| sound_rs `Player` | **11.3 ms** | **43 MB** |
| soundfile + sounddevice | 302 ms | 256 MB |

**Playback of a 3 s tone** — overhead over the track duration:

| | overhead |
|---|---|
| sound_rs `Player` (stream kept open) | **+30 ms** |
| `sounddevice.play` (opens a stream per call) | +241 ms |
| `sound_rs.play_array` (one-shot, opens a stream) | +182 ms |

**Batch decode** two FLAC files: `load_many` **272 ms** vs a `soundfile` loop 448 ms.

What the numbers do not hide:

- **MP3 output is offset by 47 samples (~1 ms) versus libsndfile and ffmpeg.**
  Sample values agree to 1.6e-05 once aligned; the offset comes from how the
  decoder delay is trimmed, and the length matches exactly.
- **WAV decode is a memcpy race** with libsndfile; the gap there is small by
  nature, the real wins are FLAC, metadata, memory and latency.
- **AAC decoding is slower than everything else** — it is a newer, less tuned
  decoder — but libsndfile cannot read AAC at all.

---

## 🛠️ Roadmap

| Step | Feature | Description |
|------|---------|-------------|
| ✅ 1 | Playback + formats | WAV, MP3, FLAC, OGG, M4A, 24-bit |
| ✅ 2 | NumPy integration | 1D/2D arrays, `load`, `load_many` |
| ✅ 3 | `Player` | Persistent stream: pause, seek, volume, position |
| ✅ 4 | Streaming decode | Constant memory, ~9 ms to first sound |
| 🔜 5 | Device selection UI | Input devices, hot-swap on disconnect |
| 🔜 6 | Recording | Microphone to array or WAV |
| 🔜 7 | Effects | Basic gain/fade/mix utilities |

---

## 🧪 Development

```bash
cargo test                      # decode, gapless, seek, fuzzing, properties, player
cargo clippy --all-targets -- -D warnings
maturin develop --release       # build into your venv
python tests/smoke.py           # end-to-end decode + API checks
SOUND_RS_PLAY=1 python tests/smoke.py   # also plays (needs an audio device)
python tests/decode_regression.py  # PCM hashes; must not change
python docs/check_api.py        # every public name is documented
python bench/run.py --compare --report bench/REPORT.md
python3 docs/_build.py          # rebuild the documentation site
```

---

## ⚙️ Built With

- 🦀 **Rust** — performance and safety
- 🔗 **PyO3** — Python bridge
- 🧰 **Maturin** — building and publishing
- 🔊 **cpal**, **symphonia**, **minimp3**, **rubato** — output, decoding, resampling

---

## 👨‍💻 Author

**Nurassyl Mussainov**
Kazakhstan 🇰🇿
Python / Rust Developer

🔗 GitHub: [Neksand](https://github.com/Neksand)
📦 PyPI: [sound_rs](https://pypi.org/project/sound_rs)

---

## 📜 License

Licensed under the **MIT License** — free for personal and commercial use.

