Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Drift Wood

A stochastic multi-speaker texture sequencer.

Drift Wood plays a library of long, pre-rendered sounds across a ring of speakers with the audience in the middle. A composed arc decides how full the room is and how fast it changes. Chance decides everything else — which texture, on which speaker, for how long, and when it moves.

Sounds move between speakers rather than crossfading in place. One speaker fades a texture out while a different one brings another in, so the piece travels around the room instead of sitting in front of you. Silence is a legitimate state for any speaker, which lets activity concentrate in one part of the ring and then migrate somewhere else.

It runs unattended for as long as you ask it to, and it never plays the same session twice unless you tell it to.


The arc

Five states, walked once, each given a share of the session in proportion to its weight:

sparse  ->  growing  ->  dense  ->  fragmenting  ->  collapse

Each state carries a weighted draw pool of sound families, a density range (how many speakers are sounding), and a dwell range (how long before a speaker reconsiders what it is doing). That table, at the top of driftwood/arc.py, is the composition. Everything else in this repository exists to play it.

Inside a state, nothing is fixed. Which family gets drawn, which file from it, which speaker changes next and how long it holds — all sampled. Two constraints hold throughout:

  • No two speakers play the same texture at once. That reads as one doubled source rather than as two places, which is the opposite of the point.
  • No two textures enter within three seconds of each other. Entrances are spaced on the moment the sound actually starts, not the moment the decision was made, so an all-at-once onset is structurally impossible.

Hearing it before you build it

A session runs against a virtual clock in well under a second:

python3 -m driftwood path/to/sounds --duration 40 --dry-run
seed 42  duration 40.0 min  dwell x1  6 speakers  library 18 files in 6 families

  time   n  event
--------------------------------------------------------------
 00:00   0  == SPARSE ==
 00:00   1  speaker 1 <- sparse_02.flac
 01:40   2  speaker 4 <- sparse_03.flac
 01:44   1  speaker 1 fades out
 02:11   2  speaker 3 <- sparse_01.flac
 03:59   1  speaker 4 fades out
...
--------------------------------------------------------------
 40:00   1  == OUTRO ==

105 texture entrances, 18 distinct files used
  sparse       mean 1.4 speakers sounding
  growing      mean 2.8 speakers sounding
  dense        mean 5.2 speakers sounding
  fragmenting  mean 4.1 speakers sounding
  collapse     mean 1.6 speakers sounding

A seed reproduces a session exactly, which is the only honest way to compare two libraries — otherwise you are comparing two different compositions.

python3 -m driftwood sounds --seed 42 --dry-run     # same session, every time
python3 -m driftwood sounds --dwell-scale 0.5 --dry-run   # twice the transitions

--dwell-scale is not a speed control. State boundaries stay on wall-clock time while decisions compress, and all the voices share one random stream, so a scaled session is a different session with the same shape.

Watching it

Watch a session replay (21s) — or open ring.html yourself.

ring.html replays a recorded session. Open it — no server, no build step, no network. It ships with a session embedded, and takes your own:

python3 -m driftwood sounds --duration 12 --dry-run --json my-session.json

The rings are the real volume envelope, not an animation. Every value drawn is a value that was sent, so a fade you watch dissipate is the fade that was heard.


Running it on your own speakers

Drift Wood has no idea how sound reaches a speaker, and does not want one. It needs exactly four methods:

class MyBackend:
    def loop(self, speaker: int, file: str) -> None: ...   # start looping, silently
    def stop(self, speaker: int) -> None: ...
    def volume(self, speaker: int, vol: int) -> None: ...   # 0-100
    def close(self) -> None: ...
from driftwood import Session, RealClock, scan_library

lib  = scan_library("sounds")
sess = Session(lib, duration_s=40 * 60, seed=42,
               clock=RealClock(), backend=MyBackend(), speakers=6)
sess.run()

Two assumptions it makes about you:

  1. One stream per speakerloop() replaces, it does not layer. This is what forces transitions to happen across speakers instead of within one.
  2. loop() starts silent — the session sends volume 0, then ramps up. A texture should arrive, not appear.

volume() is called a couple of thousand times in a long session and is fire-and-forget: a lost step is inaudible and the next one corrects it. Don't make it block.

Backends included

Backend Use
null --dry-run. The trace is the output.
stdout One line per cue. Pipe it into anything.
osc OSC over UDP — Max/MSP, Reaper, SuperCollider, Ableton, Pd.
python3 -m driftwood sounds --backend osc --osc-port 9000
/driftwood/loop    ,is   speaker, filename
/driftwood/stop    ,i    speaker
/driftwood/volume  ,ii   speaker, 0-100

The OSC encoder is written out by hand, because this repository has no dependencies at all. Python 3.8+, nothing else.

Your sounds

A flat directory. Files are named <family>_<anything>.<ext>:

sparse_01.flac   dense_01.flac   sustained_01.flac
transient_01.wav low_02.aiff     chaotic_03.flac

Families are sparse, dense, sustained, transient, low, chaotic — the six the arc is written against. A prefix it does not recognise is a hard error naming the file, because rendering ten textures, mistyping one, and never noticing it was absent is a worse outcome than a loud complaint.

You do not have to name anything. If no filename matches a family, the library collapses to a single pool and the arc still runs: density, pacing and the order of the states are all preserved. What is lost is timbral selection — nothing can tell which of your sounds is the "dense" one. A mixed library is still an error, because one odd name among seventeen correct ones is exactly what a typo looks like; pass --free if you meant it.

Material wants to be long (a minute or more), loops seamlessly, and is matched in loudness across the library — the same volume is sent whichever file a speaker drew, so a level difference is heard directly on every entrance.

Speakers

Six by default, because that is what it was composed on. --speakers N works for any N: the density column is expressed in sixths and scaled. At six the multiplier is exactly 1.0, so the original composition is untouched — that is asserted in the test suite, not assumed.

Tests

python3 tests/test_driftwood.py     # 82 checks, no hardware, no waiting
python3 tools/mutation_test.py      # reintroduce 15 bugs; all must be caught
bash    tools/scrub_check.sh        # nothing here names anyone's hardware

Every check was confirmed to fail when the bug it guards is reintroduced. Four of them exist because the mutation pass caught guards that proved nothing: the outro paths and the starved-swap case are rare enough that a normal session may never take them, so they are driven directly rather than sampled.

Licence

MIT. See LICENSE.


Drift Wood is by Merlin Goldman. The sound library that goes with the original installation is not part of this repository — bring your own.

About

A stochastic multi-speaker texture sequencer — a composed arc, and sounds that move between speakers rather than crossfading in place

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages