Skip to main content

spectrorama

PyPI Python License: GPL-3.0-or-later pipeline

Log-frequency Gabor transforms, time-frequency projections, and spectrogram panoramas: videos of sound (dramas), with the sound.

One sound, the projections

spectrorama is built in three layers. Each one is usable on its own, and each has its mathematics written down:

Layer What it does Depends on Math
1 · Transform Constant-Q Gabor frame; complex coefficients from one FFT, exact inversion; regions and masks for atom-wise filtering; level overviews numpy, scipy frame · inversion
2 · Projection Maps the time-frequency plane to a drawing plane — a catalogue classified by what it preserves, like map projections; a View (projection + rectangle + pixels) and a Lookup (pixels → atoms, with footprints) give a still at any moment in ~0.1 s numpy projections
3 · Stills and video Stills: any view, any moment, from scratch (plot, picture, save_picture). Video: a Drama swept through time -- tracks computed once, exposure motion blur, burned-in annotation and clock, a mono or stereo Soundtrack played as a tape-speed change (slowed down, ultrasound becomes audible), MP4 metadata and a sidecar JSON; frames painted in parallel, x264 or a hardware encoder + matplotlib, ffmpeg stills and video

Decisions, the measurements behind them, and the vocabulary are in the design notes; where the package stands, how it got there and what could come next in the status.

spectrorama is general: it knows samples, Hz, seconds, an optional absolute start time, a level reference and its unit. Reading recordings, choosing channels, calibration conventions, file names and branding are the caller's (for the WhaleSonic recordings: example notebooks in the whalesonic repo).

Start here: the notebooks in notebooks: QuickStart · Transform · Projections · Video. They need no data; install with pip install -e ".[docs]".

Install

pip install spectrorama           # Python >= 3.10
pip install -e ".[test]"         # from a clone, with the tests

Videos need ffmpeg on the PATH.

Quick start

import spectrorama as sr

fs = 48000
x = ...                                  # 1-D signal, NaN marks missing samples

# Layer 1: transform and exact inverse
frame = sr.LogFrequencyFrame.for_range(20, 20000, bands_per_octave=48)
coefs = sr.analyze(x, fs, frame)         # complex, ragged per band
y = sr.synthesize(coefs)                 # == x (single-precision storage: ~1e-7)

# Layer 2: views and stills
exponent = sr.PowerTime.exponent_for((20, 60), (2000, 3))   # 60 s at 20 Hz, 3 s at 2 kHz
view = sr.View.fit(sr.PowerTime(Q=frame.Q, exponent=exponent),
                   f_range=(frame.f_min, frame.f_max), seconds=(20, 60), size=(1600, 800))
sr.plot(coefs, view, now=30.0)           # a still with axes, looking from t = 30 s
levels = sr.Lookup(view, coefs).sample(now=31.0)   # (H, W) dB, ~0.1 s

# Layer 3: a video with sound, 4x slower (ultrasound transposed down 2 octaves)
spec = sr.Spec(projection='power-time', exponent=exponent, seconds=(20, 60), speed=0.25)
print(spec.describe(fs))                 # what that choice implies
drama = spec.drama(coefs, sound=sr.Soundtrack(x, fs), path='out.mp4',
                   metadata={'title': 'test', 'copyright': '...'})
drama.preview(30.0)                      # exactly the video frame at t = 30 s
drama.write()                            # out.mp4 + out.json; or sr.render(x, fs, 'out.mp4', spec)
drama.write(encoding=sr.Encoding(codec='h264_qsv'))   # GPU encoder (Intel; also h264_nvenc)

For calibrated levels pass the reference and its unit: a signal in Pa with analyze(..., ref=1e-6, unit='µPa') gives dB re 1 µPa²/Hz. Signals whose coefficients would exceed the memory budget (max_bytes, default 8 GB) are rejected before any work.

Pipelines: a look file, the command line, masters

A Spec is plain data (Spec.from_dict / to_dict, unknown keys rejected), so a look can live in a TOML file. It has two parts: the [analysis] is what the transform computes (changing it needs new coefficients), everything else is drawn from the same coefficients:

canvas = [1920, 1080]
projection = "power-time"
exponent = 1.21
seconds = [20, 120]          # 120 s visible at 20 Hz
[analysis]                   # f_min, f_max, bands_per_octave, support, psd
f_min = 20                   # bands from 19.7 Hz (third octaves at or below)
bands_per_octave = 48
[colour]
cmap = "inferno"
vlim = [40, 100]             # fixed: the same colours in every video of a series
[overlay]                    # the burned-in layers; `overlay = false`: bare picture
iso_ages = []
clock = true
spectrorama describe --spec look.toml --fs 48000          # what the look implies
spectrorama vlim clips/*.wav --spec look.toml             # one vlim for a series
spectrorama still clip.wav clip.png --spec look.toml --now 60 --now 90
spectrorama render clip.wav --spec look.toml --encoding preview --part 60 80
spectrorama render clip.wav master.mp4 --spec look.toml --encoding master --sound none     --ref 1e-7 --unit µPa --t0 2023-12-21T09:53:18 --title "..."

The level reference describes the data, not the look, so it comes with the data: --ref 1e-7 --unit µPa when 1.0 in the WAV is 10 Pa (levels in dB re 1 µPa), in Python spec.analysis.analyze(x, fs, ref=1e-7, unit='µPa').

The picture always comes from one channel (--channel, default 0); the sound has one or two (--sound 0,1, default the file's channels if at most two), never mixed into the picture. Every video gets a sidecar <video>.json with its parameters and its geometry: canvas, view rectangle, now-line, the y of round frequencies, clock and colour-bar boxes, frame rate, frame count, time range, colour limits -- everything an editor needs to composite on top. Encoding.master() (x264 crf 12, 4:4:4 at 10 bits: SSIM 0.988 at 28 Mbit/s, where 4:2:0 reaches 0.955) is meant for a second encoding by an editor or upload site, not for browsers; a drama without sound gives a file without an audio stream. In Python the same is Spec.from_dict(tomllib.load(f)), spec.analysis.analyze(...), spec.drama(coefs, ...) (which checks that the coefficients are of the spec's analysis), drama.geometry(), sr.series_vlim(dramas), drama.write(encoding=sr.Encoding.master(), part=(60, 80)).

The ends of the signal. A frame at now reads the signal around it: across the view, plus about one atom (Q / f) at each band. Near the start the low bands fill up while their windows fill; before the start and after the end there is no data, shown in colour.nodata (grey; nodata = "low" shows it as silence, in the colour map's lowest colour). Cut from a longer recording? Include extra audio around the clip and play only the part you want (render --part). Otherwise trim the video.

The legend is a separate layer for an editor to fade in and out: a frequency ruler on the now-line and a colour bar on one rounded, semi-opaque box, as a transparent RGBA PNG at canvas size (render --legend writes <video>.legend.png next to the video, spectrorama legend only the PNG; in Python drama.legend(path)). A [legend] table sets its side, ticks, spacing, opacity and font size; the box is in the sidecar's geometry.legend. Pixels outside the projection's domain (around a disc) take the background colour, not the nodata colour.

The projections

preserves time shift on screen
Rectangular — (classic constant-Q spectrogram) rigid scroll
Horocyclic area; exact shape on the now-line; clicks are vertical each row scrolls at a speed proportional to f
PowerTime(exponent) — ; window ∝ f^−exponent, from Rectangular (0) through Horocyclic (1) and beyond; PowerTime.exponent_for((f1, s1), (f2, s2)) from two windows rows scroll at speed ∝ f^exponent
HalfPlane angles (atoms stay round) rigid scroll
LinearFrequency area (looks like an STFT spectrogram) rigid scroll
PoincareDisk angles, and the whole plane fits in a disc Möbius motion
KleinDisk geodesics are straight lines projective motion

New projections subclass Projection (implement forward and inverse) or RowProjection (implement x_scale, y_of_f, f_of_y) to get the fast row lookup. Projection.tissot measures distortion anywhere.

The repository

src/spectrorama/ the package
tests/ the tests
math/ the mathematics, one page per topic
notebooks/ the tutorials; their videos go to notebooks/output/ (not in git), 02-Projections draws the picture above
dev/ for developers: design notes (decisions, measurements, vocabulary, the contract with callers) and status (history, speed, open items)

Development

pip install -e ".[test,docs]"
pytest                # reconstruction, frame bounds, calibration, projection properties,
                      # overview exactness, footprints, exposure, a real ffmpeg render
ruff check src tests

Conventions, for people and coding assistants alike:

  • Three layers, imported bottom-up only. Keep layers 1 and 2 free of matplotlib; import it lazily where needed.
  • spectrorama renders the data it is given. Reading recordings, rates, calibration, channel choice, file names and branding are the caller's job.
  • Callers rely on Spec.from_dict, the sidecar geometry, Encoding.master() and the command line: keep them stable (the contract is in the design notes).
  • Frozen dataclasses for configuration; plain functions or small classes for computation. Docstrings state definitions and why; formulas match math/.
  • Every mathematical claim has a numerical test.
  • Vocabulary: frame is the mathematical frame only; say video frame, band, level (dB), and t / t0 / now / tau (see the design notes).
  • The notebooks run without data; execute them after changes (jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb).
  • The version is single-sourced in src/spectrorama/__init__.py; keep dev/status.md current.
  • On Windows with conda, the env's Library/bin must be on PATH, or numpy's BLAS fails to load without a message.
  • New files carry the header Copyright (C) <year> J. Rychen, ETH Zürich / SPDX-License-Identifier: GPL-3.0-or-later.

License

GPL-3.0-or-later. Copyright (C) 2026 J. Rychen, ETH Zürich.

To cite spectrorama, see CITATION.cff; changes per version are in CHANGELOG.md.

The icon is a still of humpback whale song (Poincaré disk) from the WhaleSonic dataset, doi:10.3929/ethz-b-000722140, CC BY 4.0.

Metadata

Release files for spectrorama 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spectrorama 1.1.0
File Size Uploaded Provenance
spectrorama-1.1.0.tar.gz 7.8 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for spectrorama 1.1.0
File Interpreter ABI Platform
spectrorama-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 7.8 MB

Release files / spectrorama-1.1.0.tar.gz

Download URL spectrorama-1.1.0.tar.gz
Size 7.8 MB
Tags Source
SHA-256 checksum
How to use checksums
d7e72667c0397cb0a3e1d85148a08eb84b510af719d39fb90c65c4ffe873cc37
BLAKE2b-256 checksum
How to use checksums
fe534317b0cd143a1ac9b51e7ffdd715e5c341c8821b1cdfdd75ce21c735c100
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / spectrorama-1.1.0-py3-none-any.whl

Download URL spectrorama-1.1.0-py3-none-any.whl
Size 89.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0c02c1b19c02696e862967106d3c0a2e50239d3b2645f22664815d2fb0891454
BLAKE2b-256 checksum
How to use checksums
8e0b76e59698519094a1d5a039d11e23bfb248b7f194f8e2f8a802a2baa6ea0d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Security for spectrorama 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page