spacr.qt.sound_synth¶
Synthesize spaCR’s interface sounds and music bed with numpy.
Every sound spaCR can play is made here, in code, from a
SoundTheme: nothing is recorded, sampled or downloaded, so no
sound carries a licence question. A theme is a key, a scale, a tempo, a
chord progression and a handful of timbre numbers; the same theme always
renders the same samples, because every random choice is drawn from a
generator seeded by the theme itself.
The reference theme, ORBIT, is melodic space house in A minor:
warm detuned-saw pads, plucked arpeggio notes with a dotted-eighth
ping-pong delay, a soft sub, a long dark reverb, and — in the music bed
alone, never in an interface sound — a quiet four-on-the-floor kick and
an eighth-note shaker that come and go with the arrangement. Clicks and hovers
are short plucks on notes of the scale, a finished run is an arpeggio
rising through the seventh chord into the tonic under a pad swell, and a
failed run is a slower falling figure over the minor fourth.
This module is Qt-free on purpose. It writes 16-bit PCM WAV files, which
is all PySide6.QtMultimedia.QSoundEffect needs, into a cache
under the spaCR data directory (sound_cache_root()), one folder per
theme and fingerprint so a changed theme or a new synthesiser version can
never be answered from a stale file. spacr.qt.sound renders
through ensure_rendered() on its own audio thread and never on the
GUI thread.
Classes¶
How loudly each part plays in one bar of the music bed. |
|
One rendered sound and the notes it was made from. |
|
Everything that decides how one sound set sounds. |
Functions¶
|
The arrangement, bar by bar, for a loop of |
|
The diatonic chord built in thirds on a scale degree. |
|
Make sure each named sound exists on disk, rendering what is missing. |
|
Programme loudness in LUFS, by ITU-R BS.1770-4. |
|
Frequency of a MIDI note number in equal temperament, A4 = 440 Hz. |
|
Read a 16-bit PCM WAV written by |
|
Synthesize one named sound of a theme. |
|
The MIDI note of a scale degree, any number of octaves away. |
|
Where rendered sounds are kept: |
|
The file stems an event plays, in the order they rotate. |
|
The folder a theme's files live in, named by key and fingerprint. |
|
A short hash of everything that decides what a theme's files hold. |
|
Write stereo float samples as 16-bit PCM, atomically. |
Module Contents¶
- class spacr.qt.sound_synth.BedBar[source]¶
Bases:
NamedTupleHow loudly each part plays in one bar of the music bed.
- Parameters:
section – which entry of
BED_SECTIONSthis bar is in.arp – arpeggio level, 0 to 1.
kick – kick level, 0 to 1, before the theme’s
kick_level.shaker – shaker level, 0 to 1, before
shaker_level.sub – sub level, 0 to 1.
lift – semitones the arpeggio rises by in this bar’s second half.
lead – melody level, 0 to 1, before the theme’s
lead_level. Last, because this is a public tuple and a field added anywhere but the end changes what every position after it means.
- class spacr.qt.sound_synth.Rendered[source]¶
One rendered sound and the notes it was made from.
- Parameters:
name – the sound’s file stem, for example
"click-2".audio – float samples, shape
(2, n), peak at most 1.notes –
(onset seconds, MIDI note, part)for every note played, in onset order.partis"pluck","pad"or"sub". The schedule is what a test reads to check a figure rises or falls without having to transcribe the audio.loop – whether the sound is meant to repeat seamlessly.
- class spacr.qt.sound_synth.SoundTheme[source]¶
Everything that decides how one sound set sounds.
- Parameters:
key – stable identifier, used in the cache path and the stored preference.
label – name shown in Preferences.
description – one sentence shown as the choice’s explanation.
tonic – MIDI note of the key’s tonic, in the octave the pads sit in (57 is A3).
scale – semitones above the tonic of each scale degree.
tempo – beats per minute; sets the arpeggio rate and the delay.
progression – one scale degree per bar of the music bed, each the root of a diatonic seventh chord (0-based: 0 is the tonic).
pad_voices – detuned sawtooth oscillators per pad note.
pad_detune_cents – spread of those oscillators, either side.
pad_cutoff_hz – low-pass cutoff of the pad at rest; it breathes up to roughly twice this.
pad_level – pad level in the mix.
pad_pump – depth of the dip on every beat of the music bus – pads, arpeggio, lead and their reverb, everything but the kick and half of the sub. The side-chain of house music, with or without a kick drum to cause it (0 to 1). THIS NUMBER, NOT THE ROUTING, IS MOST OF HOW DEEP THE PUMP SOUNDS: measured on the reference bed, 300-3000 Hz, the mix dips 1.81 dB with this at 0, 2.46 dB at the 0.3 the pads alone used, and 4.59 dB at the 0.6 the reference now asks for. A theme that sets it back toward 0.3 is asking for a pump shallower than
test_the_music_bus_ducks_on_the_beataccepts.pluck_brightness – how slowly the pluck’s harmonics fall away (0 to 1); higher is brighter.
pluck_decay – time constant of the pluck’s fundamental, seconds.
pluck_level – arpeggio level in the mix.
lead_pattern – the melody, as
(scale degree, beats)pairs read in order and repeated for the length of the bed. A degree ofRESTis a silence of that many beats. An empty pattern, or alead_levelat or belowPART_FLOOR, leaves the melody out of the render entirely.lead_level – melody level in the mix.
lead_octave – whole octaves above the tonic the melody is played in.
lead_attack – seconds the melody’s notes take to speak. Slow enough that it sings rather than plucks.
lead_brightness – how far above each note’s own pitch the melody’s filter opens; higher is brighter. Capped so that no theme can turn the melody into a saw lead that takes the air away from the shaker – this genre’s lead is warm, and the top of the mix belongs to the percussion.
arp_pattern – indexes into the five arpeggio tones of a chord (four chord tones and the root an octave up), one per step.
arp_division – arpeggio steps per beat (2 is eighth notes).
delay_beats – echo spacing in beats; 0.75 is the dotted eighth.
delay_feedback – level of each echo relative to the one before.
delay_mix – how much of the echo reaches the output.
sub_level – level of the sine sub under each chord.
space – reverb send (0 to 1).
reverb_seconds – time for the reverb tail to fall 60 dB.
width – stereo spread of the pad voices (0 is mono).
bed_bars – bars in one loop of the music bed. Rounded up to a whole number of
BED_SECTIONSsections bybed_plan(), so the arrangement always closes where it opened.kick_level – level of the four-on-the-floor kick in the bed, 0 to 1.
0.0leaves the kick out of the render entirely.shaker_level – level of the eighth-note shaker, 0 to 1.
0.0leaves it out entirely.bed_lufs – programme loudness the finished bed is normalised to, in LUFS (
loudness_lufs()). Quieter than anything mastered for release, because this plays under somebody’s work.bed_peak_db – the bed’s peak ceiling in dBFS. Reached with a memoryless soft knee, which is what lets the ceiling be applied after the loop is folded without putting a seam back in.
seed – seeds every random choice, so a theme always renders the same samples.
- spacr.qt.sound_synth.bed_plan(bars: int) List[BedBar][source]¶
The arrangement, bar by bar, for a loop of
barsbars.Every part reaches its section’s level over the section’s first half and holds it, so nothing arrives as a step; the ramp for the FIRST section starts from the LAST one’s levels, which is what makes the arrangement circular rather than merely long.
barsis stretched proportionally acrossBED_SECTIONS, so a theme can ask for a shorter or longer loop and still get the same shape.- Parameters:
bars – bars in the loop; at least one per section.
- Returns:
one
BedBarper bar.
- spacr.qt.sound_synth.chord_tones(theme: SoundTheme, degree: int, count: int = 4) List[int][source]¶
The diatonic chord built in thirds on a scale degree.
- Parameters:
theme – supplies the tonic and the scale.
degree – 0-based scale degree of the chord’s root.
count – chord tones to stack; 4 is a seventh chord.
- Returns:
MIDI notes, root first, ascending.
- spacr.qt.sound_synth.ensure_rendered(theme: SoundTheme, names: Iterable[str], root: pathlib.Path | None = None, sr: int = SAMPLE_RATE, should_stop: Callable[[], bool] | None = None) Dict[str, pathlib.Path][source]¶
Make sure each named sound exists on disk, rendering what is missing.
A file already in the theme’s folder is trusted: the folder name is the fingerprint of everything that decides its contents. Folders left by an earlier fingerprint of the same theme are removed once the current one is in use.
- Parameters:
theme – the sound set.
names – stems from
sound_names().root – cache root;
sound_cache_root()when omitted.sr – sample rate.
should_stop – polled between sounds; returning True abandons the rest, so a render can be cancelled at the next file boundary.
- Returns:
stem -> path, for every file that exists afterwards.
- spacr.qt.sound_synth.loudness_lufs(audio: numpy.ndarray, sr: int = SAMPLE_RATE) float[source]¶
Programme loudness in LUFS, by ITU-R BS.1770-4.
The gated integrated measurement: K-weight both channels, take the mean square over 400 ms blocks overlapping by three quarters, drop every block below -70 LUFS, then drop every block more than 10 LU under the mean of what is left and take the mean of the rest.
A PEAK IS NOT A LOUDNESS, which is the reason this exists. The music bed and the run sounds have similar peaks and are nothing like as loud as each other, and “quiet enough to work under” is a statement about loudness. The coefficients are the standard’s own and are written for 48 kHz; another rate is measured with them anyway and the answer drifts by a fraction of a LU, which is inside what anybody can hear.
- Parameters:
audio – shape
(channels, n).sr – sample rate.
- Returns:
LUFS, or
-inffor silence.
- spacr.qt.sound_synth.midi_to_hz(note: float) float[source]¶
Frequency of a MIDI note number in equal temperament, A4 = 440 Hz.
- Parameters:
note – MIDI note number (69 is A4).
- Returns:
frequency in hertz.
- spacr.qt.sound_synth.read_wav(path) Tuple[numpy.ndarray, int][source]¶
Read a 16-bit PCM WAV written by
write_wav().- Parameters:
path – the file.
- Returns:
(audio, sample rate), audio shaped(channels, n)in -1 to 1.
- spacr.qt.sound_synth.render(theme: SoundTheme, name: str, sr: int = SAMPLE_RATE) Rendered[source]¶
Synthesize one named sound of a theme.
- Parameters:
theme – the sound set.
name – a stem from
sound_names().sr – sample rate.
- Returns:
the rendered audio and its note schedule.
- Raises:
KeyError – for a name no event plays.
- spacr.qt.sound_synth.scale_note(theme: SoundTheme, degree: int, octave: int = 0) int[source]¶
The MIDI note of a scale degree, any number of octaves away.
- Parameters:
theme – supplies the tonic and the scale.
degree – 0-based scale degree; values past the scale wrap into the next octave and negative values into the one below.
octave – whole octaves added on top.
- Returns:
a MIDI note number.
- spacr.qt.sound_synth.sound_cache_root() pathlib.Path[source]¶
Where rendered sounds are kept:
~/.spacr/soundsby default.CACHE_ENVoverrides it. Read on every call, so a test that sets the variable is honoured without any reload.
- spacr.qt.sound_synth.sound_names(event: str) List[str][source]¶
The file stems an event plays, in the order they rotate.
- Parameters:
event – one of
EVENTS.- Returns:
for example
["click-0", ..., "click-3"]for a click.- Raises:
KeyError – for an event this module does not know.
- spacr.qt.sound_synth.theme_cache_dir(theme: SoundTheme, root: pathlib.Path | None = None, sr: int = SAMPLE_RATE) pathlib.Path[source]¶
The folder a theme’s files live in, named by key and fingerprint.
- Parameters:
theme – the sound set.
root – cache root;
sound_cache_root()when omitted.sr – sample rate.
- Returns:
the folder (not created here).
- spacr.qt.sound_synth.theme_fingerprint(theme: SoundTheme, sr: int = SAMPLE_RATE) str[source]¶
A short hash of everything that decides what a theme’s files hold.
- Parameters:
theme – the sound set.
sr – sample rate.
- Returns:
twelve hex digits.
- spacr.qt.sound_synth.write_wav(path, audio: numpy.ndarray, sr: int = SAMPLE_RATE) pathlib.Path[source]¶
Write stereo float samples as 16-bit PCM, atomically.
The file appears under its final name only once it is complete, so a player that looks while it is being written sees the old file or none, never half of one.
- Parameters:
path – destination.
audio – shape
(2, n), values in -1 to 1.sr – sample rate.
- Returns:
the path written.