Note: it is best to listen to the video below with good headphones or through a good stereo system so that you can properly hear the bass line. I will post a higher-quality recording shortly.
Bebop in C is generative art piece produced as output of the app Seurat given a “score” as input. At the moment the score is designed for four voices/instruments, soprano, alto, tenor and base. Below is the bass part for this composition.
Algorithm Summaries
Video
The “movie” is a layered accumulation: 11 nested golden-spiral frames, each filled with wandering particles that draw shapes. Because the frame fill has near-zero alpha, previous drawings persist with slow fade. The interpreter script changes the visual parameters over time — colors, shapes, sizes, spacing — creating an evolving pointillist animation where new marks overlay old ones in an ever-shifting color field.
Music
The music is a procedural SATB composition. A score file defines the orchestration (which voices play when), and each voice generates its melody in real time via multiplicative Brownian motion through interval arrays — constrained random walks in frequency space. The result is an evolving, non-repeating polyphonic texture where voices enter and exit according to the cue sheet, each wandering through its register with characteristic interval preferences, accented by metric pulses and occasional rhythmic subdivisions. The interpreter script layers in dynamic changes (volume, tempo) to shape the arc of the performance.
Score
<part: bass>
<cues: 0 1 0 1 1 1 0 1 1 0 1 1 1 1 0 1 0 1 0 1 1 0 1 0 1 1 1 1>
<volume: 0.50>
<maxPitch: 120>
<minPitch: 30>
<firstPitch: 90>
<intervalArrayKey: ia2345B>
<newPitchDirectionProbability: 0.15>
<meter: 5>
<notesPerBeat: 1>
<relativeNoteDuration: 0.02>
<restProbability: 0.03>
<doubleNoteProbability: 0.01>
<beatsOfPhraseOverlap: 10>
Seurat was written in 2013 using Processing, with Minim used for audio processing. The program had lain dormant for 12 years, many dependencies were out-of-date or not installed on my current machine, and I had no memory of how to run it. Fortunately Claude Code came to the rescue. After ten minutes of intense work, Seurat was up and running.
Generating music and video
Producing synthetic music and video requires one to adhere classical prinicples: a coherent stucture and proper balance between unity and variety. (To be continued …)
Algorithms
Music
Music Algorithm
- Architecture Overview
The music system is a procedural composition engine for SATB (Soprano-Alto-Tenor-Bass) voices. It has four layers:
Score (ScoreClasses.pde) — parsed from a score file, defines the structure: which voices play in which sections Generator (Generator.pde) — one per voice, generates melodies via constrained Brownian motion through interval arrays Ensemble (Ensemble.pde) — the conductor: triggers generators at phrase boundaries based on the score’s cue sheet Sound/ToneInstrument (Sound.pde, ToneInstrument.pde) — Minim audio synthesis, triangle-wave oscillators with ADSR envelopes 2. Score File Format (e.g., op3n1)
A score file is divided into parts separated by –. Each part specifies:
Cues: a binary array (1s and 0s) — one entry per section. 1 = the voice plays during that section, 0 = silence. This is the orchestration map. Pitch parameters: firstPitch, minPitch, maxPitch (in Hz), intervalArrayKey (which set of intervals to use), newPitchDirectionProbability Rhythm parameters: meter, notesPerBeat, relativeNoteDuration, restProbability, doubleNoteProbability, beatsOfPhraseOverlap Volume For example in op3n1, the soprano is silent for 13 sections, then enters sporadically. The bass enters in section 2 and plays most of the time. This creates a gradual textural build from bass up to full SATB.
- Timing: Phrases and Sections
Music.pde / Ensemble.pde:38:
Time is divided into phrases of fixed length (beatsPerPhrase, default 25 beats) framesPerPhrase = 60 * frameRate * beatsPerPhrase / bpm — converts musical time to animation frames Every time frameCount % framesPerPhrase == 0, a new phrase begins Sections advance one per phrase: localPhase = phase % numberOfSections When all sections have played, the score cycles (wraps around) 4. The Ensemble Conductor
Ensemble.play1() (Ensemble.pde:53) — called once per phrase:
For each voice (part i):
Check part.plays(localPhase) — consult the cue array for the current section If the cue is 1, call generator.play(beatsPerPhrase + random overlap) If the cue is 0, the voice is silent this phrase The beatsOfPhraseOverlap parameter adds a random number of extra beats (0 to N), so phrases overlap slightly — notes from the end of one phrase bleed into the next, avoiding hard gaps.
- The Generator: Brownian Melody Construction
Generator.play() (Generator.pde:83) is the heart of the system. For each phrase, it generates notesPerBeat * beatsPerPhrase notes:
- Pitch Selection (Constrained Brownian Motion)
The generator walks through pitch space using interval multiplication/division:
For each note: 1. With probability 0.2: pick a new random interval ARRAY (e.g., switch from “seconds” to “thirds”) 2. With probability 0.5: pick a new random INTERVAL from the current array 3. With probability P: flip direction (up vs. down) 4. If direction == up: currentPitch = lastPitch * currentInterval If direction == down: currentPitch = lastPitch / currentInterval This is multiplicative Brownian motion in frequency space — the pitch wanders up and down by musical intervals (seconds, thirds, fourths, etc.), randomly changing which interval it steps by and which direction it goes.
- Pitch Constraints
If pitch falls below minPitch: jump up by a fifth (x1.5) or an octave (x2), chosen randomly If pitch exceeds maxPitch: jump down by a fourth (/4/3) or an octave (/2) If pitch equals the last pitch: nudge by a whole step (x ws) This keeps each voice in its register (soprano ~405-1012 Hz, bass ~30-120 Hz).
- Interval Arrays (Pitch.pde)
The interval arrays are sets of equal-tempered frequency ratios:
Key Contents Musical character ia2 half step, whole step Chromatic/cluster ia3 minor 3rd, major 3rd Triadic/sweet ia23 seconds + thirds Moderate variety ia23b 3x seconds, 1x thirds Step-heavy, occasional leap ia23456 hs through M6 Wide melodic range ia2345B hs, ws, ws, m3, M3, p4, p5 Balanced (weighted toward steps) Each interval array is a 2D array — an array of arrays. The generator first picks which sub-array to use (with probability 0.2), then picks an interval within it (with probability 0.5). This two-level random selection creates clusters of similar intervals interrupted by sudden changes — the melody tends to move in seconds for a while, then suddenly shift to thirds, etc.
- Rhythm
For each note slot i:
With probability restProbability (~0.16): the note is silent (volume = 0) With probability 0.02: play a triplet (3 notes in one slot) With probability doubleNoteProbability (~0.05): play a double (2 notes in one slot) With probability pickupProbability (0.02): play a pickup (note on the second half of the slot) Otherwise: play a single note Meter accent: every meter-th note gets volume x 1.33, creating a rhythmic pulse (e.g., groups of 3, 4, or 5).
Note spacing = beatSpacing / notesPerBeat, and note duration = relativeNoteDuration * noteSpacing. Short durations (0.02-0.25) produce staccato; longer values produce legato.
- Sound Synthesis
ToneInstrument (ToneInstrument.pde): Each note is a triangle wave oscillator (Waves.TRIANGLE) patched through an ADSR envelope (attack 0.01s, decay 0.05s, sustain 0.5, release 0.5s). Triangle waves give a warmer, more muted tone than pure sine waves.
NoiseInstrument (NoiseInstrument.pde): Available but not used in the default score — white noise through two bandpass filters creating a “whistling wind” texture.
Notes are scheduled on the Minim AudioOutput using playNote(startTime, duration, instrument). All notes for a phrase are scheduled at once at the start of the phrase, then Minim plays them back in real time.
- The Interpreter’s Role
The program file can change music parameters mid-performance:
volume — fade in/out bpm — accelerando/ritardando beatsperphrase — change phrase length score — load a different score file entirely In program1, the volume gradually rises from 0.5 to 1.1 while the tempo accelerates from 144 to 170 bpm, then cuts to 0 for a dramatic silence before fading back in.
Summary
The music is a procedural SATB composition. A score file defines the orchestration (which voices play when), and each voice generates its melody in real time via multiplicative Brownian motion through interval arrays — constrained random walks in frequency space. The result is an evolving, non-repeating polyphonic texture where voices enter and exit according to the cue sheet, each wandering through its register with characteristic interval preferences, accented by metric pulses and occasional rhythmic subdivisions. The interpreter script layers in dynamic changes (volume, tempo) to shape the arc of the performance.
Video
Video Algorithm
The Rendering Pipeline
- Spatial Layout: Golden Ratio Spiral of Frames
At startup, FrameSet.makeFrames() (FrameSet.pde:57) constructs a golden ratio spiral of nested square frames inside a golden rectangle that fills the screen.
The first frame is the full rectangle Each subsequent frame is placed via nextSquare() (FrameSet.pde:370), which rotates through 4 orientations (right, below, left, above) — exactly like the squares in a Fibonacci spiral Each new frame’s side length is φ⁻¹ (≈0.618) times the previous one The last frame is stretched to a rectangle to fill the remaining golden-ratio sliver (lastFrame(), line 401) Default: 11 frames deep 2. Per-Frame State: Color Tori
Each SEFrame carries its own color (r, g, b, a) and a color velocity (dr, dg, db). On every draw cycle, SEFrame.changeColor() (Frames.pde:159) applies:
r += speed * dr (with small random perturbation of dr each step) g += speed * dg b += speed * db all values mod 256 → wrapping creates smooth color cycling (“torus” in color space) The initial colors are interpolated between two endpoint colors c1 and c2 set by setColorTori2() (FrameSet.pde:252). Each frame gets a slightly different color velocity (perturbed by ±10%), so the frames diverge chromatically over time.
- Per-Frame State: Particles
Each frame contains 32 particles (default). Each particle has:
Position (x, y) — starts at frame center Radius — slowly grows from minRadius to maxRadius, then resets (sawtooth) Color (r, g, b, a) — drifts via its own color velocity Shape type — one of: circle, triangle, square, quad, star, letter, or word 4. The Draw Loop (every frame)
Seurat.pde:60 — draw():
Interpreter advances — interpreter.run(frameCount) checks if the current frameCount matches the next instruction’s trigger frame. If so, it executes opcodes that change colors, shapes, radii, spacing, alpha, etc. This is the script that choreographs the movie.
Music plays — music.ensemble.play() (separate from visuals)
display() calls frameSet.display() (FrameSet.pde:190), which iterates all frames:
For each frame i (outer to inner):
- Draw the frame rectangle (Frames.pde:234-239):
Fill with the frame’s current (r, g, b, a) color — crucially, alpha is very low (default 2.0), so this is a near-transparent wash over whatever was drawn before This creates persistence/trails — old particle drawings fade slowly rather than being erased b. Update particles (Frames.pde:246-247):
particle.change(m, M) does three things: Brownian motion: position += spacingFactor * speed * random(-radius, radius) — particles wander randomly, with larger particles taking bigger steps Wall reflection: if a particle hits the frame boundary, it bounces back by a random amount Radius growth: radius += rspeed * dradius, wrapping from max back to min Color drift: each RGB channel drifts by colorVelocity * delta, wrapping at 0/255 c. Draw particles (Frames.pde:250-251):
Each particle draws its shape (circle/triangle/quad/star/letter/word) at its current position, size, and color Shapes like triangle and quad use randomized vertices around the particle center, so each draw produces a slightly different polygon — giving a “pointillist” feel (hence “Seurat”) d. Scale down for next frame:
maxRadius and minRadius are multiplied by φ⁻¹ for the next (smaller) frame, so particles in deeper frames are proportionally smaller 5. The Scripted “Movie”
The program file (e.g., program1) defines a timeline of parameter changes:
Negative frames (init phase): set framerate, tempo, score file Positive frames: at specific frame counts, change colors, shapes, radii, particle spacing, display words, fade volume, halt/restart display, etc. For example in program1:
Frame 1: set colors to red/blue Frame 3: shape = quad Frame 999: tighten particle spacing Frame 1000: switch to word “Bebop!” Frame 4352: halt display (blank screen) Frame 4801: restart display with triangles at smaller scale The interpreter supports labels and loops (opcode 8), so a program can cycle through configurations indefinitely.
Summary
The “movie” is a layered accumulation: 11 nested golden-spiral frames, each filled with wandering particles that draw shapes. Because the frame fill has near-zero alpha, previous drawings persist with slow fade. The interpreter script changes the visual parameters over time — colors, shapes, sizes, spacing — creating an evolving pointillist animation where new marks overlay old ones in an ever-shifting color field.