Skip to main content

Fast audio analysis library with C++ and Python bindings

Project description

libsonare

PyPI npm License

A dependency-free audio DSP toolkit for Python — librosa-compatible analysis plus broadcast-grade mastering, mixing, and editing.

Built on a C++ core with zero Python dependencies. Analysis defaults match librosa (validated against generated librosa reference values in CI), and mastering ships 66 named DSP processors implemented against published references (ITU-R BS.1770-4 true-peak limiting, Linkwitz-Riley crossovers, Vicanek matched-Z biquads, ADAA-antialiased saturation) — Apache-2.0, no model weights.

Installation

pip install libsonare

Supported platforms: Linux (x86_64, aarch64), macOS (Apple Silicon).

Quick Start

Audio is the recommended entry point. The top-level libsonare.detect_* / libsonare.analyze functions are thin wrappers around Audio for one-shot calls and are kept for convenience.

Load from a file

import libsonare

audio = libsonare.Audio.from_file("song.mp3")  # or "song.wav"
print(f"BPM: {audio.detect_bpm():.1f}")
print(f"Key: {audio.detect_key().root.name} {audio.detect_key().mode.name}")

# Advanced key options are opt-in; defaults preserve existing behavior.
key_with_options = audio.detect_key(
    use_hpss=True,
    loudness_weighted=True,
    high_pass_hz=80.0,
)

result = audio.analyze()  # BPM + key + time signature + beats
print(f"BPM: {result.bpm:.1f}  Key: {result.key.root.name} {result.key.mode.name}")

Room acoustics

Use detect_acoustic for blind RT60/EDT estimation from ordinary audio. Use analyze_impulse_response when you have a measured impulse response and need clarity metrics (c50, c80, d50). Blind mode returns nan for clarity metrics because they are not reliable without an impulse response.

import libsonare

audio = libsonare.Audio.from_file("recording.wav")
blind = audio.detect_acoustic(
    n_octave_bands=6,
    n_third_octave_subbands=24,
    min_decay_db=30.0,
    noise_floor_margin_db=10.0,
)
print(blind.rt60, blind.edt, blind.is_blind)

ir = libsonare.Audio.from_file("room_ir.wav")
params = ir.analyze_impulse_response()
print(params.rt60, params.c50, params.c80, params.d50)

Load from a numpy array / in-memory samples

from_buffer accepts any 1D float sequence. With numpy, use a mono float32 array (stereo must be downmixed first, e.g. samples.mean(axis=1)).

import numpy as np
import libsonare

sr = 22050
samples = np.asarray(my_mono_float32_signal, dtype=np.float32)

audio = libsonare.Audio.from_buffer(samples, sample_rate=sr)
bpm = audio.detect_bpm()

# Or call the function directly (equivalent shortcut)
bpm = libsonare.detect_bpm(samples, sample_rate=sr)

Pitch, timbre, and spectral APIs

Pitch tracking keeps unvoiced f0 frames as nan by default. Pass fill_na=True when downstream code needs finite values and should treat unvoiced frames as 0. Timbre analysis returns aggregate metrics plus timbre_over_time; timbreOverTime is also available as an alias.

yin = libsonare.pitch_yin(samples, sample_rate=sr, fill_na=True)
pyin = audio.pitch_pyin(fill_na=True)

timbre = libsonare.analyze_timbre(samples, sample_rate=sr)
print(timbre.brightness, timbre.timbre_over_time[0].brightness)

contrast = libsonare.spectral_contrast(samples, sample_rate=sr)
poly = libsonare.poly_features(samples, sample_rate=sr)
crossings = libsonare.zero_crossings(samples)
tuning = libsonare.estimate_tuning(samples, sample_rate=sr)
offset = libsonare.pitch_tuning(yin.f0)

w, h = libsonare.decompose(spectrogram, n_features, n_frames, 8)
filtered = libsonare.nn_filter(spectrogram, n_features, n_frames)
remixed = libsonare.remix(samples, [0, sr, 2 * sr, 3 * sr], sample_rate=sr)
stretched = libsonare.phase_vocoder(samples, sample_rate=sr, rate=1.5)
hpss = libsonare.hpss_with_residual(samples, sample_rate=sr)

multi = libsonare.lufs_interleaved(interleaved, channels=2, sample_rate=sr)
lra = libsonare.ebur128_loudness_range(samples, sample_rate=sr)

Mastering

The Python binding exposes the same mastering processors as the C, CLI, Node, and WASM APIs.

import libsonare

names = libsonare.mastering_processor_names()  # e.g. "dynamics.compressor"

compressed = libsonare.mastering_process(
    "dynamics.compressor",
    samples,
    sample_rate=sr,
    params={"thresholdDb": -24.0, "ratio": 1.5},
)

widened = libsonare.mastering_process_stereo(
    "stereo.imager",
    left,
    right,
    sample_rate=sr,
    params={"width": 1.1},
)

pair_names = libsonare.mastering_pair_processor_names()  # e.g. "match.abCrossfade"
crossfaded = libsonare.mastering_pair_process(
    "match.abCrossfade",
    source,
    reference,
    sample_rate=sr,
    params={"mix": 0.25},
)

loudness_json = libsonare.mastering_pair_analyze(
    "match.referenceLoudness",
    source,
    reference,
    sample_rate=sr,
)
mono_compat_json = libsonare.mastering_stereo_analyze(
    "stereo.monoCompatCheck",
    left,
    right,
    sample_rate=sr,
)

Mastering chain

mastering_chain runs the full configurable mastering pipeline (EQ, dynamics, saturation, repair, stereo, loudness, ...). The Python binding accepts flat dot-notation keys for the config dict — addressing any module parameter directly — and an optional on_progress(progress, stage) callback that is invoked after each stage (progress in [0, 1]).

import libsonare

result = libsonare.mastering_chain(
    samples,
    sample_rate=sr,
    config={
        "eq.tilt.tiltDb": 0.5,
        "dynamics.compressor.thresholdDb": -24.0,
        "dynamics.compressor.ratio": 1.5,
        "dynamics.transientShaper.attackGainDb": 2.0,
        "repair.declick.enabled": True,
        "loudness.targetLufs": -14.0,
        "loudness.ceilingDb": -1.0,
    },
    on_progress=lambda p, stage: print(f"[{p * 100:5.1f}%] {stage}"),
)

stereo_result = libsonare.mastering_chain_stereo(
    left, right, sample_rate=sr,
    config={"stereo.imager.width": 1.1, "loudness.targetLufs": -14.0},
)

MasteringChainResult exposes the rendered samples plus loudness telemetry (input_lufs, output_lufs, applied_gain_db, latency_samples).

Mastering presets

Named presets ship sensible defaults for common targets. master_audio applies a preset and lets you override any individual parameter using the same flat dot-notation keys as mastering_chain.

import libsonare

libsonare.mastering_preset_names()
# -> ['pop', 'edm', 'acoustic', 'hipHop', 'aiMusic', 'speech', 'streaming', 'youtube', 'broadcast', 'podcast', 'audiobook', 'cinema', 'jpop', 'ambient', 'lofi', 'classical', 'drumAndBass', 'techno', 'metal', 'trap', 'rnb', 'jazz', 'kpop', 'trance', 'gameOst']

result = libsonare.master_audio(
    samples,
    sample_rate=sr,
    preset="aiMusic",
    overrides={
        "loudness.targetLufs": -13.0,
        "dynamics.multibandComp.enabled": True,
    },
)

# Audio shortcut
audio = libsonare.Audio.from_file("song.wav")
pop_mastered = audio.master_audio("pop")

Mixing

import libsonare

libsonare.mixing_scene_preset_names()
# -> ['vocalReverbSend', ...]
scene_json = libsonare.mixing_scene_preset_json("vocalReverbSend")

offline = libsonare.mix_stereo(
    [(vocal_l, vocal_r), (music_l, music_r)],
    sample_rate=sr,
    input_trim_db=[3.0, 0.0],
    fader_db=[-3.0, -12.0],
    pan=[0.0, -0.2],
    width=[1.0, 0.9],
)

mixer = libsonare.Mixer.from_scene_json(scene_json, sample_rate=sr, block_size=512)
try:
    block_l, block_r = mixer.process_stereo(
        [vocal_block_l, return_block_l],
        [vocal_block_r, return_block_r],
    )
finally:
    mixer.close()

Headless DAW project

Project is a headless arrangement model: audio & MIDI tracks and clips, MIDI sequencing, SMF / MIDI 2.0 Clip File I/O, deterministic JSON save/load, and an offline bounce. Every mutation routes through an undoable history, musical positions are PPQ (quarter notes), and the object is a context manager.

import libsonare

with libsonare.Project() as project:
    project.set_sample_rate(48000)
    track_id, clip_id = project.add_midi_clip(0.0, 4.0)
    project.set_midi_events(clip_id, [
        libsonare.Project.midi_note_on(0.0, 0, 0, 60, 100),  # ppq, group, channel, note, velocity
        libsonare.Project.midi_note_off(1.0, 0, 0, 60),
    ])

    json_str = project.to_json()            # deterministic, byte-stable within a build
    smf = project.export_smf()              # bytes — Standard MIDI File
    midi2 = project.export_clip_file()      # bytes — MIDI 2.0 Clip File (lossless)

    result = project.compile()              # has_timeline / messages / diagnostics
    audio = project.bounce(num_channels=2)  # (frames, channels) float32 ndarray

Streaming mastering chain

StreamingMasteringChain runs the same pipeline block-by-block for real-time or chunked workflows. The constructor takes the same flat dot-notation config as mastering_chain; non-streamable stages (repair.denoise, loudness) cause the constructor to raise, so omit those keys for streaming use. The object is also a context manager.

import libsonare

with libsonare.StreamingMasteringChain({
    "eq.tilt.tiltDb": 0.5,
    "dynamics.compressor.thresholdDb": -20.0,
    "dynamics.transientShaper.attackGainDb": 1.5,
}) as chain:
    chain.prepare(sample_rate=48000, max_block_size=512, num_channels=1)
    print(chain.stage_names())
    print(f"latency = {chain.latency_samples()} samples")

    out_block = chain.process_mono([0.0] * 512)
    # Stereo:
    # chain.prepare(48000, 512, 2)
    # out_l, out_r = chain.process_stereo(left_block, right_block)
    chain.reset()

Library version

import libsonare
libsonare.__version__   # e.g. "1.0.4"  (preferred — matches importlib.metadata)
libsonare.version()     # native library version string

Supported audio formats

Format Default build With FFmpeg support
WAV (PCM 16/24/32, float32) yes yes
MP3 yes yes
M4A / AAC / FLAC / OGG / Opus / WMA / ... no yes

If Audio.from_file() is given an unsupported format you will see a clear error such as:

RuntimeError: Unsupported audio format: '.m4a'. Supported: WAV, MP3.
Rebuild with -DSONARE_WITH_FFMPEG=ON for M4A/AAC/FLAC/OGG,
or convert via: ffmpeg -i "song.m4a" output.wav

The PyPI wheels are pinned to SONARE_WITH_FFMPEG=OFF so the distributed wheel never silently links against the build host's FFmpeg. From-source builds default to AUTO detection via pkg-config: if FFmpeg dev libraries are present they are enabled, otherwise WAV/MP3 only.

To enable FFmpeg-backed decoding when building from source:

git clone https://github.com/libraz/libsonare
cd libsonare
SONARE_FFMPEG=1 bash bindings/python/build_wheel.sh   # require FFmpeg
# or
SONARE_FFMPEG=AUTO bash bindings/python/build_wheel.sh # detect via pkg-config
pip install bindings/python/dist/*.whl

This links against the system FFmpeg shared libraries (LGPL by default), so ensure they are installed (e.g. brew install ffmpeg, apt install libavformat-dev libavcodec-dev libavutil-dev libswresample-dev).

Function vs Audio method

Both APIs return the same results. Use whichever is more convenient:

# Functional (good for ad-hoc numpy work)
bpm = libsonare.detect_bpm(samples, sample_rate=22050)        # -> float
key = libsonare.detect_key(samples, sample_rate=22050)        # -> Key(root, mode, confidence)
key_hpss = libsonare.detect_key(
    samples,
    sample_rate=22050,
    n_fft=4096,
    hop_length=512,
    use_hpss=True,
    loudness_weighted=True,
    high_pass_hz=80.0,
)
result = libsonare.analyze(samples, sample_rate=22050)        # -> AnalysisResult

# Audio class (recommended for files, multiple operations on the same audio)
audio = libsonare.Audio.from_file("song.wav")
bpm = audio.detect_bpm()
key = audio.detect_key()
result = audio.analyze()

Audio caches the decoded samples and is the only way to load files, so it is the recommended entry point when you do more than a single computation on the same signal.

Input format expectations

API dtype shape range
Audio.from_buffer(samples, sample_rate=...) float32 (float64 also accepted) 1D mono nominally [-1.0, 1.0]
Audio.from_memory(data) bytes of an encoded WAV / MP3 / (FFmpeg) file
Audio.from_file(path) path to an encoded audio file
libsonare.detect_bpm(samples, sample_rate=...) etc. float32 (float64 also accepted) 1D mono nominally [-1.0, 1.0]

Stereo input is not downmixed automatically when passed as samples — downmix yourself (e.g. samples.mean(axis=1, dtype=np.float32)). File loaders downmix to mono internally.

CLI

sonare analyze song.mp3
# > Estimated BPM : 161.00 BPM  (conf 75.0%)
# > Estimated Key : C major  (conf 100.0%)

sonare bpm song.mp3 --json       # {"bpm": 161.0}
sonare key song.mp3              # Key: C major (confidence: 100.0%)
sonare spectral song.mp3         # Spectral features table
sonare pitch song.mp3            # Pitch tracking (pYIN)
sonare mel song.mp3              # Mel spectrogram shape
sonare chroma song.mp3           # Chromagram with visualization

sonare mastering song.wav -o mastered.wav --target-lufs -14
sonare mastering-processor song.wav --processor dynamics.compressor --params thresholdDb=-24,ratio=1.5
sonare mastering-pair-processors
sonare mastering-pair-analyze source.wav --reference reference.wav --analysis match.referenceLoudness
sonare mix --preset vocalReverbSend

Features

  • Detection: BPM (float), key (Key(root, mode, confidence)), beats (list[float] seconds), onsets (list[float] seconds)
  • Analysis: Full music analysis (AnalysisResult with BPM, key, time signature, beat times)
  • Effects: HPSS, HPSS with residual, pitch shift, time stretch, phase vocoder, normalize, trim, remix
  • Features: STFT, mel spectrogram, MFCC, chroma, CQT/VQT, spectral contrast, poly features, zero crossings, pitch tracking (YIN / pYIN with optional fill_na)
  • Decomposition & loudness: NMF decomposition, nearest-neighbour filtering, multichannel LUFS, EBU R128 LRA
  • Conversions: Hz / mel / MIDI / note, frames / time
  • Headless DAW: Project arrangement model — audio/MIDI tracks & clips, undo/redo, MIDI sequencing, SMF / MIDI 2.0 Clip File I/O, deterministic JSON, offline bounce
  • I/O: Load WAV / MP3 (and M4A/AAC/FLAC/OGG when built with FFmpeg), resample

librosa-compatible defaults

Parameter Default
Sample rate 22050 Hz
n_fft 2048
hop_length 512
n_mels 128
fmin / fmax 0.0 / sr/2

Also available

npm install @libraz/libsonare  # JavaScript / TypeScript (WASM)

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

libsonare-1.3.0-py3-none-manylinux_2_17_x86_64.whl (2.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

libsonare-1.3.0-py3-none-manylinux_2_17_aarch64.whl (2.6 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

libsonare-1.3.0-py3-none-macosx_11_0_arm64.whl (2.0 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file libsonare-1.3.0-py3-none-manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for libsonare-1.3.0-py3-none-manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 586a022073a1ec5674512e3232109bd9e5ca391f1cc87b19218db995c7be4b41
MD5 6cc1c46e75310c0a9c53408fd2034b49
BLAKE2b-256 4d011a1cb93149e27f7cbf3d82689463b075b8c4b98f75dba76b08714b5d508f

See more details on using hashes here.

Provenance

The following attestation bundles were made for libsonare-1.3.0-py3-none-manylinux_2_17_x86_64.whl:

Publisher: publish.yml on libraz/libsonare

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libsonare-1.3.0-py3-none-manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for libsonare-1.3.0-py3-none-manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 64a94b91c671215b69ea11a0d4dc40b114e0c31765962702113fe425ad7b759f
MD5 d9d8aaa6519232973254e58c70dff438
BLAKE2b-256 7527028ddf7acb53a354200a8536e8e5a922195278273ea3f80e7ad94726a3e6

See more details on using hashes here.

Provenance

The following attestation bundles were made for libsonare-1.3.0-py3-none-manylinux_2_17_aarch64.whl:

Publisher: publish.yml on libraz/libsonare

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libsonare-1.3.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for libsonare-1.3.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 78be7d27f3eb13b8d034e88447bc7cd367772649360d2904af8ae0525e2dd8f3
MD5 3ca78954029e3bc3716efce0484c05d0
BLAKE2b-256 62c61a89fa9b6409238d1c38deb6a830450f88dfbb2a7f0f3a5250f91da2c0ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for libsonare-1.3.0-py3-none-macosx_11_0_arm64.whl:

Publisher: publish.yml on libraz/libsonare

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page