Metadata-Version: 2.4
Name: libsonare
Version: 1.5.3
Summary: Fast audio analysis library with C++ and Python bindings
Project-URL: Homepage, https://github.com/libraz/libsonare
Project-URL: Repository, https://github.com/libraz/libsonare
Project-URL: Issues, https://github.com/libraz/libsonare/issues
Author-email: libraz <libraz@libraz.net>
License-Expression: Apache-2.0
Keywords: analysis,audio,bpm,dsp,key,mir,music,tempo
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: numpy>=1.24
Provides-Extra: dev
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# libsonare

[![PyPI](https://img.shields.io/pypi/v/libsonare)](https://pypi.org/project/libsonare/)
[![npm](https://img.shields.io/npm/v/@libraz/libsonare)](https://www.npmjs.com/package/@libraz/libsonare)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/libraz/libsonare/blob/main/LICENSE)

**Turn audio into data and back, from Python.** Analyze songs (BPM, key, chords,
loudness), master and mix to broadcast loudness, and render MIDI through built-in
instruments — a fast C++ core with NumPy as its only dependency.

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); analysis defaults match librosa where the
two overlap (validated against generated librosa reference values in CI).
Apache-2.0, no model weights.

## Installation

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

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

```python
import libsonare

audio = libsonare.Audio.from_file("recording.wav")
# Blind estimation from ordinary audio; tuning options (n_octave_bands, …) are optional.
blind = audio.detect_acoustic()
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)
```

`estimate_room` infers room geometry (`volume`, `length`/`width`/`height`,
`drr_db`, per-band `absorption_bands` / `rt60_bands`) from a recording.
`synthesize_rir` renders an image-source-model RIR for a shoebox room, and
`room_morph` convolves audio with such an RIR to relocate it into that room.

```python
import numpy as np
import libsonare

room = libsonare.estimate_room(samples, sample_rate=48000)
print(room.volume, room.length, room.width, room.height, room.rt60_bands)

rir = libsonare.synthesize_rir(
    length_m=6.0, width_m=4.0, height_m=2.8,
    absorption=0.2, sample_rate=48000, ism_order=3,
)
print(len(rir.rir), rir.sample_rate)

wet = libsonare.room_morph(
    samples, 48000, length_m=6.0, width_m=4.0, height_m=2.8, wet=0.5,
)
```

### 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)`).

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

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

```python
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 specialist processors

For direct access to individual dynamics and restoration processors (each
returns `(processed_ndarray, latency_samples)` for the dynamics family, and a
processed `ndarray` for the repair family):

```python
import libsonare

comp, latency = libsonare.mastering_dynamics_compressor(
    samples, sample_rate=sr, threshold_db=-18.0, ratio=2.0, attack_ms=10.0,
)
gated, _ = libsonare.mastering_dynamics_gate(samples, sample_rate=sr, threshold_db=-50.0)
shaped, _ = libsonare.mastering_dynamics_transient_shaper(
    samples, sample_rate=sr, attack_gain_db=3.0,
)

declicked = libsonare.mastering_repair_declick(samples, sample_rate=sr)
declipped = libsonare.mastering_repair_declip(samples, sample_rate=sr)
decrackled = libsonare.mastering_repair_decrackle(samples, sample_rate=sr)
dehummed = libsonare.mastering_repair_dehum(samples, sample_rate=sr, fundamental_hz=50.0)
denoised = libsonare.mastering_repair_denoise_classical(samples, sample_rate=sr)
dereverbed = libsonare.mastering_repair_dereverb_classical(samples, sample_rate=sr)
trimmed = libsonare.mastering_repair_trim_silence(samples, sample_rate=sr)
```

Insert (FX) introspection for scene/UI builders:

```python
inserts = libsonare.mastering_insert_names()              # e.g. "dynamics.compressor"
params = libsonare.mastering_insert_param_names("dynamics.compressor")  # camelCase keys
```

### 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]``).

```python
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`) and the per-stage names
(`stages`).

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

```python
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_name="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

```python
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, _sr = 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.

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

### Instruments and synthesis

A plain `Project.bounce()` renders MIDI tracks to silence (no instrument is
bound). The `bounce_with_*_instrument` methods route bound MIDI destinations
through a sound source: the simple built-in oscillator (`BuiltinSynthConfig`),
the full patch-driven NativeSynth (`SynthPatch` / preset name), a SoundFont
player (`Sf2InstrumentConfig`, after `load_soundfont`), or your own host
instrument (`bounce_with_instruments`). `synth_preset_names()` lists the
NativeSynth catalog, and `synth_preset_patch(name)` returns a tweakable
`SynthPatch`.

```python
import libsonare

print(libsonare.synth_preset_names())   # ['sine', 'saw-lead', 'e-piano', ...]

with libsonare.Project() as project:
    project.set_sample_rate(48000)
    _, 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),
        libsonare.Project.midi_note_off(2.0, 0, 0, 60),
    ])

    # NativeSynth preset (or a SynthPatch, or None for the default patch).
    audio = project.bounce_with_synth_instrument("saw-lead", num_channels=2)

    # Build a patch from a preset base, overriding only what you need.
    patch = libsonare.SynthPatch(preset="e-piano", cutoff_hz=4000.0, gain=0.8)
    audio2 = project.bounce_with_synth_instrument(patch, num_channels=2)

    # Simple built-in oscillator.
    audio3 = project.bounce_with_builtin_instrument(
        libsonare.BuiltinSynthConfig(waveform="saw", gain=0.5),
    )
```

### Real-time engine

`RealtimeEngine` is a block-based transport + clip/MIDI playback engine with
instrument binding, MIDI input/output, parameter automation, capture, an
offline bounce, and a telemetry ring. It is a context manager.

```python
import libsonare
from libsonare import EngineBounceOptions

with libsonare.RealtimeEngine(sample_rate=48000.0, max_block_size=128) as engine:
    engine.set_tempo(120.0)
    engine.set_synth_instrument("saw-lead", destination_id=0)
    engine.push_midi_note_on(0, 0, 0, 60, 100)   # destination, group, channel, note, velocity
    engine.play()

    out = engine.process([[0.0] * 128, [0.0] * 128])  # -> planar [[...left], [...right]]
    telemetry = engine.drain_telemetry()

    bounced = engine.bounce_offline(EngineBounceOptions(total_frames=48000, num_channels=2))
    print(bounced.frames, bounced.num_channels, bounced.integrated_lufs)
```

Capabilities:

- Transport: `play` / `stop` / `seek_sample` / `seek_ppq` / `set_tempo` / `set_time_signature` / `set_loop`.
- Clips: `set_clips([EngineClip(...)])`, paged streaming via `ClipPageProvider` / `FileClipPageProvider`.
- Instruments: `set_synth_instrument` / `set_builtin_instrument` / `set_sf2_instrument` (after `load_soundfont`).
- MIDI: `push_midi_note_on` / `push_midi_note_off` / `push_midi_cc` / `push_midi_panic`, live input via `push_midi_input_*`, and `bind_midi_cc` to parameters.
- Rendering: `process` / `process_with_monitor` / `render_offline` / `bounce_offline` / `freeze_offline`.
- Automation, markers, metronome, capture (`arm_capture` / `captured_audio`), and `drain_telemetry` / `drain_meter_telemetry`.

### Real-time voice changer

`RealtimeVoiceChanger` runs a block-based vocal chain (retune, formant shift,
EQ, gate, compressor, de-esser, reverb, limiter) configured from a named preset
or a config mapping. `voice_change_realtime` is a one-shot convenience for an
entire buffer. Preset helpers list, fetch, and validate configs.

```python
import libsonare

print(libsonare.realtime_voice_changer_preset_names())  # ['neutral-monitor', 'bright-idol', ...]

with libsonare.RealtimeVoiceChanger(48000, "bright-idol", max_block_size=128) as vc:
    out_block = vc.process_mono([0.0] * 128)      # -> np.ndarray
    vc.set_config("deep-narrator")                # swap preset mid-stream
    cfg = vc.config_pod()                         # RealtimeVoiceChangerConfig (POD)

# One-shot over a whole buffer.
processed = libsonare.voice_change_realtime(samples, sample_rate=48000, preset="bright-idol")

cfg = libsonare.realtime_voice_changer_preset_config("neutral-monitor")
check = libsonare.validate_realtime_voice_changer_preset_json(
    libsonare.realtime_voice_changer_preset_json("bright-idol")
)
print(check["ok"])  # True
```

### Streaming frame analyzer

`StreamAnalyzer` analyzes audio incrementally: feed chunks with `process`, then
drain analyzed frames (mel / chroma / onset / RMS / spectral / chord) with
`read_frames` (or the quantized `read_frames_u8` / `read_frames_i16`). Query the
running BPM/key/chord estimate via `stats`. It is a context manager.

```python
import libsonare

with libsonare.StreamAnalyzer(libsonare.StreamConfig(sample_rate=22050)) as sa:
    for chunk in audio_chunks:
        sa.process(chunk)
    sa.finalize()

    frames = sa.read_frames(sa.available_frames())
    print(frames.n_frames, frames.n_mels)

    stats = sa.stats()
    print(stats.bpm, stats.key, stats.chord_root)
```

### Metering

A compact metering family operates on plain sample buffers. Names (all
`libsonare.<name>`):

| Function | Returns |
|----------|---------|
| `metering_peak_db` / `metering_rms_db` / `metering_true_peak_db` | `float` |
| `metering_crest_factor_db` / `metering_dc_offset` | `float` |
| `metering_stereo_correlation` / `metering_stereo_width` | `float` (L/R) |
| `metering_detect_clipping` | `ClippingReport` |
| `metering_dynamic_range` | `DynamicRangeReport` |
| `metering_spectrum` / `metering_spectrum_frame` | `SpectrumReport` |
| `metering_vectorscope` / `metering_vectorscope_decimated` | `VectorscopeReport` |
| `metering_phase_scope` / `metering_phase_scope_decimated` | `PhaseScopeReport` |
| `waveform_peaks` / `waveform_peak_pyramid` | `WaveformPeaksReport` (min/max buckets) |

```python
import libsonare

print(libsonare.metering_true_peak_db(samples, sample_rate=sr))
report = libsonare.metering_detect_clipping(samples, sample_rate=sr)
peaks = libsonare.waveform_peaks(samples, channels=1, samples_per_bucket=512)
```

### Error handling

Native return-code failures raise `libsonare.SonareError`, a subclass of
`RuntimeError` carrying a numeric `.code`. Input-validation failures (empty /
NaN / Inf buffers, bad shapes) raise `ValueError`, and missing-feature builds
raise `RuntimeError`.

```python
import libsonare

try:
    libsonare.synth_preset_patch("does-not-exist")
except libsonare.SonareError as exc:
    print(exc.code, str(exc))
```

### 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.

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

```python
import libsonare
libsonare.__version__   # e.g. "1.3.2"  (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:

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

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

```bash
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 rhythm song.mp3           # Rhythm primitives
sonare timbre song.mp3           # Timbre / spectral shape
sonare dynamics song.mp3         # Dynamics / loudness analysis
sonare lufs song.mp3 --series    # Integrated LUFS (+ momentary/short-term)
sonare tempogram song.mp3        # Autocorrelation tempogram
sonare plp song.mp3              # Predominant local pulse
sonare nnls-chroma song.mp3      # NNLS chroma
sonare onset-envelope song.mp3   # Onset strength envelope

sonare acoustic recording.wav                 # Blind RT60 / EDT estimation
sonare estimate-room recording.wav            # Inferred room geometry
sonare synthesize-rir -o rir.wav              # Render a shoebox-room RIR
sonare room-morph song.wav -o wet.wav         # Convolve into a synthesized room

sonare master song.wav -o mastered.wav --preset pop
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-chain song.wav -o out.wav --params loudness.targetLufs=-14
sonare mastering-processors           # List solo processors
sonare mastering-pair-processors      # List two-input processors
sonare mastering-pair-analyze source.wav --reference reference.wav --analysis match.referenceLoudness
sonare mastering-presets              # List mastering preset names
sonare mastering-suggest song.wav     # Suggested chain as JSON
sonare eq song.wav -o eq.wav --frequency-hz 1000 --gain-db 3 --q 1.0
sonare declip song.wav -o fixed.wav   # Repair clipped audio

sonare pitch-correct vocal.wav -o tuned.wav   # Snap pitch to a scale/MIDI
sonare note-stretch song.wav -o out.wav       # Per-note time stretch
sonare voice-change vocal.wav -o out.wav --preset bright-idol
sonare voice-presets                  # List realtime voice-changer presets
sonare voice-preset --preset bright-idol --json   # Print a voice-changer preset

sonare mixing-presets                 # List mixer scene presets
sonare mixing-preset --preset vocalReverbSend
sonare mix --preset vocalReverbSend
sonare mix --scene scene.json --input a.wav --input b.wav -o mix.wav

sonare project synth-presets          # List NativeSynth presets

# Headless project / MIDI
sonare project new --sample-rate 48000 -o project.json
sonare project bounce --in project.json -o out.wav --synth saw-lead
sonare project export-smf --in project.json -o out.mid
sonare project import-smf --smf in.mid -o project.json
sonare midi-render --in project.json -o out.wav --synth saw-lead
```

## Features

- **Detection**: BPM (`float`), key (`detect_key` / `detect_key_candidates`), beats, downbeats (`detect_downbeats`), onsets, chords (`detect_chords` / `chord_functional_analysis`)
- **Analysis**: Full music analysis (`analyze`), plus `analyze_melody`, `analyze_sections`, `analyze_rhythm`, `analyze_dynamics`, `analyze_timbre`
- **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
- **Mastering & metering**: 66 named processors, preset chains (`master_audio`), dynamics / repair specialist functions, and a `metering_*` / `waveform_peaks` family
- **Mixing**: scene-driven `Mixer`, `mix_stereo`, built-in scene presets
- **Instruments & synthesis**: NativeSynth (`SynthPatch` / presets — 12 synthesis engines incl. physically-modeled piano/strings/winds, being tuned over time), built-in oscillator, SoundFont (SF2) playback, host-instrument bounce
- **Real-time**: `RealtimeEngine` (transport / clips / MIDI / capture), `RealtimeVoiceChanger`, `StreamAnalyzer`
- **Room acoustics**: blind RT60 / EDT, `estimate_room`, `synthesize_rir`, `room_morph`
- **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

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

## License

Apache-2.0
