Metadata-Version: 2.4
Name: sound_rs
Version: 0.1.8
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: License :: OSI Approved :: MIT License
Requires-Dist: numpy>=1.20 ; extra == 'numpy'
Provides-Extra: numpy
Summary: Fast Rust-powered audio playback library for Python
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/Neksand/sound_rs
Project-URL: Issues, https://github.com/Neksand/sound_rs/issues
Project-URL: Repository, https://github.com/Neksand/sound_rs

# 🎵 sound_rs

**sound_rs** is a fast, Rust-powered audio playback library for Python that I built
and maintain — decoding with [symphonia](https://github.com/pdeljanov/Symphonia),
output through [cpal](https://github.com/RustAudio/cpal), so the same code runs on
macOS, Windows and Linux.

---

## 🚀 Features

- **Formats:** WAV, MP3, FLAC, OGG/Vorbis, M4A, AAC — any bit depth, including 24-bit
- **NumPy arrays:** play 1D mono `(samples,)` or 2D `(channels, samples)` float32 arrays
- **Decode to array:** `load(path) -> (ndarray, sample_rate)`, shape `(frames, channels)`
- **Gapless (MP3, OGG, AAC/MP4):** encoder delay and padding are trimmed, so decoded length matches the original. MP4 priming comes from the edit list; at most one trailing frame can remain, because the container does not declare it
- **Fast metadata:** `get_audio_info` reads the container header; for AAC, whose container carries no channel layout, one packet is decoded to learn it
- **Resampling:** linear interpolation to the device rate, channel folding to the device layout
- **Device-native output:** the stream is opened in whatever sample format the device wants (f32, i16, u16, ...)
- **Non-blocking for others:** playback blocks the calling thread but releases the GIL, so other Python threads keep running
- **Tone generator:** `play_sine_wave(440, 2.0, 44100)`

---

## 🧠 Installation

```bash
pip install sound_rs
```

---

## 💻 Usage

### Playback

```python
import sound_rs

sound_rs.play_audio("sound.wav")
sound_rs.play_audio("sound.mp3")
sound_rs.play_audio("sound.flac")
sound_rs.play_audio("sound.m4a")
```

### Inspect a file (no decoding)

```python
sample_rate, channels, duration = sound_rs.get_audio_info("sound.mp3")
sound_rs.is_supported_format("sound.mp3")  # probes the header, does not decode
```

### Decode to a NumPy array

```python
import sound_rs

audio, rate = sound_rs.load("music.flac")   # float32, shape (frames, channels)
```

### From a NumPy array

```python
import numpy as np
import sound_rs

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
```

### Background playback

```python
import threading

threading.Thread(target=sound_rs.play_audio, args=("long.mp3",)).start()
```

### Tone generator

```python
sound_rs.play_sine_wave(440.0, 1.0, 44100)  # frequency, duration, sample rate
```

---

## 🧩 Benchmark

`play_audio` is synchronous, so its wall-clock time is dominated by the track's
own duration. The interesting numbers are **metadata**, **decode** and
**overhead** (wall clock − audio duration) — and decode is only worth measuring
if the output is correct, so every run also compares PCM against libsndfile.

```bash
python examples/benchmark.py                        # generated 3s tone: decode + playback
python examples/benchmark.py --compare              # + sounddevice / soundfile
python examples/benchmark.py --files a.mp3 b.flac   # decode table for real files
```

**Hardware and setup I measured on:** Apple M5 Pro, macOS 26.6.2, Python 3.14.7,
release build (`lto = "fat"`, `opt-level = 3`), 60 s of 44.1 kHz stereo,
median and p95 of 7 runs, competitor is libsndfile (`soundfile` 0.14).

| format | metadata | decode (median / p95) | libsndfile | result | PCM vs libsndfile |
|---|---|---|---|---|---|
| WAV (PCM 16) | 0.02 ms | **9.1 / 11.2 ms** | 11.4 ms | 1.25× faster | identical |
| FLAC | 0.03 ms | **16.3 / 17.7 ms** | 41.0 ms | 2.5× faster | identical |
| MP3 | 0.02 ms | 33.8 / 34.3 ms | **24.5 ms** | 1.4× slower | 1.9e-07 |
| M4A (AAC) | 0.17 ms | 34.7 / 38.2 ms | unsupported | sound_rs only | matches ffmpeg to 8e-08 |

Playback of a 3 s tone, overhead over the track duration (median of 5):

| | median | p95 | overhead |
|---|---|---|---|
| sound_rs | 3.124 s | 3.280 s | +0.124 s |
| sounddevice | 3.125 s | 3.131 s | +0.125 s |

What the numbers say, including the unflattering parts:

- **Metadata no longer decodes the file.** Reading a 2-hour file costs the same
  as reading a 3-second one, because it comes from the container header.
- **PCM output matches libsndfile exactly** for WAV and FLAC, and to 1.9e-07
  for MP3 — both trim the encoder delay and padding, so sample N means the
  same thing in both libraries. For AAC/MP4 the priming is read from the MP4
  edit list, which libsndfile cannot even decode.
- **MP3 decoding is ~1.4× behind libmpg123.** That is the decoder itself, not
  the plumbing; fixing it means a different MP3 decoder.
- **Playback overhead is stream setup and teardown**, ~0.1 s per call. Reducing
  it to single-digit milliseconds needs a persistent stream, which is the next
  item on the roadmap.


---

## 🛠️ Roadmap

| Step | Feature | Description |
|------|---------|-------------|
| ✅ 1 | WAV playback | Core playback |
| ✅ 2 | MP3, FLAC, M4A, AAC, 24-bit | Full symphonia decode path |
| ✅ 3 | NumPy integration | 1D/2D float32 arrays |
| ✅ 4 | Decode to array | `load(path) -> (ndarray, sample_rate)` without playing |
| 🔜 5 | `Player` object | Persistent stream: pause, resume, seek, volume, position |
| 🔜 6 | Device selection | `list_devices()`, choose input/output |
| 🔜 7 | Recording | Microphone → array or WAV |

---

## 🧪 Development

```bash
cargo test                      # resampler and channel conversion
maturin develop --release       # build the extension into your venv
python tests/smoke.py           # end-to-end decode checks
SOUND_RS_PLAY=1 python tests/smoke.py   # also plays (needs an audio device)
python examples/benchmark.py
```

---

## ⚙️ Built With

- 🦀 **Rust** — performance and safety
- 🔗 **PyO3** — Python bridge
- 🧰 **Maturin** — building and publishing
- 🔊 **cpal** + **symphonia** — audio backend and decoders

---

## 👨‍💻 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.

