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
The provenance of everything |
Functions
|
Put |
|
|
|
A music bed: a sustained chord with a gentle pulse, loopable end to end. |
|
A percussive hit: a seeded noise burst over a low thump, decaying fast. |
|
A sine at |
|
Seconds of audio in a PCM WAV ( |
|
|
|
|
Classes
|
The |
Exceptions
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 byanfrom numbers, so no third party’s work is in it.
- class an.sounds.SoundAsset(**data)[source]#
Bases:
BaseModelThe
sound.jsonof one entry in thesoundsstore.- 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).
- exception an.sounds.SoundError[source]#
Bases:
ValueErrorA sound the store cannot hold, or holds wrongly.
- an.sounds.add_sound(store, key, audio, *, source, description='')[source]#
Put
audio(WAV bytes) instoreunderkey, with its provenance.sourceis required: a sound with no recorded origin is exactly the assetan creditscannot vouch for. Itslicensemay beNone— 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:
- an.sounds.get_sound(store, key)[source]#
(asset, wav_bytes)forkey, 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:
- 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
durationwhenduration * pulse_hzis whole, so a looped bed does not bump at the seam.- Return type:
>>> 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:
>>> 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
freqHz, with short linear ramps so it does not click.- Return type:
>>> 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:
>>> 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).>>> wav_info(synth_tone(440.0, 0.5, sample_rate=8000)) (8000, 1, 4000)
- an.sounds.well_formed_wav(data)[source]#
datawith a data size that states its true length — the SAME bytes whenever it already does (a trailingLISTchunk, a pad byte and the format are kept, so the digest of a healthy file is the digest of the file as supplied). Whatadd_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:
>>> wav = synth_tone(440.0, 0.1, sample_rate=8000) >>> well_formed_wav(wav) is wav True