an.sounds#

Sound assets: what the sound layer plays, where it came from, and a synthesizer.

A an.ir.schema.SoundCue names a key in the project’s sounds store; this module is the typed front door to that store. Every sound carries an AssetSource — provenance and licence — and the sha256 of the bytes that licence is attached to, because a sound is third-party work far more often than a character is, and **a licence recorded and never displayed is not compliance**: an credits walks this store like the others.

>>> import tempfile
>>> from an.stores.sounds import SoundsStore
>>> with tempfile.TemporaryDirectory() as d:
...     store = SoundsStore(d)
...     asset = add_sound(store, "beep", synth_tone(440.0, 0.25), source=SYNTH_SOURCE)
...     asset.duration, get_sound(store, "beep")[1][:4]
(0.25, b'RIFF')

The film mix is mono at 44.1 kHz (the per-shot audio’s format), so a stereo asset is mixed down. v1 stores WAV (PCM) only. The mix needs each asset’s exact length to place a fade-out, and a WAV header states it without decoding anything; convert other formats with ffmpeg before adding them.

The synthesizers (synth_tone(), synth_hit(), synth_bed()) are what the demo and the tests use instead of shipping any third-party audio: pure numpy, seeded, so the same call writes the same bytes.

Module Attributes

SYNTH_SOURCE

The provenance of everything synth_tone() / synth_hit() / synth_bed() produce: generated on the user's machine by an from numbers, so no third party's work is in it.

Functions

add_sound(store, key, audio, *, source[, ...])

Put audio (WAV bytes) in store under key, with its provenance.

get_sound(store, key)

(asset, wav_bytes) for key, the bytes checked against the digest.

synth_bed(duration, *[, chord, pulse_hz, ...])

A music bed: a sustained chord with a gentle pulse, loopable end to end.

synth_hit([duration, seed, thump_hz, decay, ...])

A percussive hit: a seeded noise burst over a low thump, decaying fast.

synth_tone(freq, duration, *[, sample_rate, ...])

A sine at freq Hz, with short linear ramps so it does not click.

wav_duration(data)

Seconds of audio in a PCM WAV (wav_info(): the bytes, not the header).

wav_info(data)

(sample_rate, channels, frames) of a PCM WAV: its format from the header, its length from the audio bytes actually present (an#330).

well_formed_wav(data)

data with a data size that states its true length — the SAME bytes whenever it already does (a trailing LIST chunk, a pad byte and the format are kept, so the digest of a healthy file is the digest of the file as supplied).

Classes

SoundAsset(**data)

The sound.json of one entry in the sounds store.

Exceptions

SoundError

A sound the store cannot hold, or holds wrongly.

an.sounds.SYNTH_SOURCE = AssetSource(provider='an.sounds', id='procedural-synthesis', url=None, license='cc0-1.0', license_url=None, attribution=None, source_page_url=None, author='an (procedural synthesis, generated locally)', author_url=None, cacheable=None, sha256=None, cost_usd=None, extra={})#

The provenance of everything synth_tone() / synth_hit() / synth_bed() produce: generated on the user’s machine by an from numbers, so no third party’s work is in it.

class an.sounds.SoundAsset(**data)[source]#

Bases: BaseModel

The sound.json of one entry in the sounds store.

duration: float#

Seconds of audio, from the audio bytes (wav_duration(); before an#330 a pipe-cut WAV recorded its streaming header’s ~22369 s).

model_config: ClassVar[ConfigDict] = {'extra': 'allow'}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

sha256: str#

Digest of audio.wav as it entered the project; checked on every read.

exception an.sounds.SoundError[source]#

Bases: ValueError

A sound the store cannot hold, or holds wrongly.

an.sounds.add_sound(store, key, audio, *, source, description='')[source]#

Put audio (WAV bytes) in store under key, with its provenance.

source is required: a sound with no recorded origin is exactly the asset an credits cannot vouch for. Its license may be None — that is recorded as UNKNOWN and reported as unverified, never as free. A source whose terms forbid keeping the bytes (cacheable=False: a Freesound API pull) is refused: the store keeps bytes (an#332).

Return type:

SoundAsset

an.sounds.get_sound(store, key)[source]#

(asset, wav_bytes) for key, the bytes checked against the digest.

A mismatch raises: the licence is attached to the digest, so different bytes under the same key are an asset nobody recorded.

Return type:

tuple[SoundAsset, bytes]

an.sounds.synth_bed(duration, *, chord=(220.0, 277.18, 329.63), pulse_hz=2.0, sample_rate=44100, amplitude=0.3)[source]#

A music bed: a sustained chord with a gentle pulse, loopable end to end.

The pulse is a whole number of cycles over duration when duration * pulse_hz is whole, so a looped bed does not bump at the seam.

Return type:

bytes

>>> len(synth_bed(1.0)) > 44 and synth_bed(1.0) == synth_bed(1.0)
True
an.sounds.synth_hit(duration=0.35, *, seed=0, thump_hz=90.0, decay=0.06, sample_rate=44100, amplitude=0.6)[source]#

A percussive hit: a seeded noise burst over a low thump, decaying fast.

Return type:

bytes

>>> synth_hit(seed=1) == synth_hit(seed=1), synth_hit(seed=1) == synth_hit(seed=2)
(True, False)
an.sounds.synth_tone(freq, duration, *, sample_rate=44100, amplitude=0.3, attack=0.005, release=0.02)[source]#

A sine at freq Hz, with short linear ramps so it does not click.

Return type:

bytes

>>> synth_tone(440.0, 0.1) == synth_tone(440.0, 0.1)
True
an.sounds.wav_duration(data)[source]#

Seconds of audio in a PCM WAV (wav_info(): the bytes, not the header).

Return type:

float

>>> wav_duration(synth_tone(440.0, 0.5, sample_rate=8000))
0.5
an.sounds.wav_info(data)[source]#

(sample_rate, channels, frames) of a PCM WAV: its format from the header, its length from the audio bytes actually present (an#330).

Return type:

tuple[int, int, int]

>>> wav_info(synth_tone(440.0, 0.5, sample_rate=8000))
(8000, 1, 4000)
an.sounds.well_formed_wav(data)[source]#

data with a data size that states its true length — the SAME bytes whenever it already does (a trailing LIST chunk, a pad byte and the format are kept, so the digest of a healthy file is the digest of the file as supplied). What add_sound() stores, so a WAV cut to a pipe is kept as a file every reader agrees on: only its two size fields are patched and anything past its audio dropped, the format chunk untouched (WAVE_FORMAT_EXTENSIBLE and its channel mask included).

Return type:

bytes

>>> wav = synth_tone(440.0, 0.1, sample_rate=8000)
>>> well_formed_wav(wav) is wav
True