Bebop In C: Technical Details

The canvas

The screen starts black. Filling it is a golden-rectangle spiral: 11 squares, each scaled by \(1/\phi\) from the previous, arranged right-down-left-up in a spiral pattern. The last one is stretched to a rectangle to close the gap. Each square containd 32 particles (only 1 active by default). There are 11 frames in total.

Each time the screen is rendered, we alter the composition of each of the 11 frames in order from largest to smallest:

Step 1 — Wash the frame. A filled rectangle is drawn over the entire frame at the frame’s current color but with very low alpha (frameAlpha, default 2.0 out of 255). This doesn’t erase the previous image — it barely tints it. Over hundreds of frames, old marks gradually fade under accumulating semi-transparent washes. This is the persistence mechanism: the image is never cleared, just slowly buried.

Step 2 — Drift the frame color. Each frame has its own RGB color that changes every frame by \((dr, dg, db) \times "speed"\), where \("speed" = "0.1."\) The deltas themselves are perturbed each frame by +/- 10% random jitter. RGB channels wrap at 256. So the wash color wanders slowly through color space — each frame independently.

Step 3 — Move each active particle using particle.change() to modify three things:

  • Position: Use Brownian motion. Step by spacingFactor * speed * random(-radius, radius) in x and y. The step size is proportional to the particle’s current radius, so large particles take big steps, small ones take small steps. If the particle hits a frame wall, it teleports to a random position near the opposite wall (a soft bounce).

  • Radius: linear growth with wraparound. Radius increases by rspeed * dradius each frame. When it exceeds maxRadius for this frame, it resets to minRadius. Both limits are scaled by \(1/\phi\) per frame level — inner frames have proportionally smaller particles. So each particle cycles from small to large, jumps to small, grows again.

  • Color: linear drift with wrap. Each RGB channel increments by colorVelocity * delta each frame. Channels wrap at 0/255. The alpha oscillates between 50 and 100. So particle colors rotate steadily through RGB space.

Step 4 — Draw each active particle. The particle stamps its current shape at its current position, radius, and color. The shape (set globally) is one of: circle, random-vertex triangle, square, random-vertex quadrilateral, star, outlined square, a letter character, or a word string. Triangles and quads get new random vertices each frame, so they jitter. The stamp is drawn with noStroke() and the particle’s current RGBA fill.

The result over time

Because the background is never cleared — only washed with alpha 2 — every particle stamp persists for hundreds of frames, slowly fading. The visual effect:

  • Early frames: sparse marks on black, gradually building a textured background
  • Middle frames: a dense, layered accumulation. Particle trails form ghostly paths. The slow color drift of both the wash and the particles creates shifting hues across the image.
  • Inner frames: smaller particles, tighter spacing, higher spacing factors — they produce finer, denser textures in the smaller squares of the spiral.

The Interpreter script drives the evolution over thousands of frames:

Frames.   What changes
│ 0      15 fps, red-to-blue color, quads, alpha 1.0, radius 100-200
│ 999       │ tighter particle spacing (0.9)                                │ 1000      │ shape becomes word "Bebop!"                                   | 1030      │ spacing tightens to 0.3, back to quads                        
│ 3069      │ spacing loosens to 0.9, word becomes "Bebop..!!"               | 4300-4352 │ display halts, radius shrinks to 10-50, switches to triangles 
│ 4801      │ display resumes with tiny tight triangles, gradually growing  │ 5300-5600 │ radius expands back to 50-100 then 100-200

Scaling across frame levels

Each frame level i gets maxRadius and minRadius multiplied by \(1/(\phi)^i\). So frame 0 might have particles with radius 100-200, while frame 10 has radius ~1-3. The particle speeds and spacing also scale down. The effect is self-similar texture at every level of the spiral — large bold marks in the outer square, fine grain in the inner squares.

Score

A Score file (e.g. op3n1 — see below) defines parts separated by - -. There are four parts, soprano, alto, tenor and bass. Each part has:

  • Cues: a binary array (28 entries in the example). 1 means the voice plays this section, 0 means silence. The score cycles: localPhase = phase % numberOfSections.
  • Generator parameters: pitch range, interval arrays, rhythm, and articulation settings.

Interval arrays

  From Pitch.pde:64-75:

float intervals2[ ] = { hs, ws }; // halfstep, wholestep float intervals3[ ] = { m3, M3 }; // minor 3rd, major 3rd

float [][] ia23 = { intervals2, intervals3 };

So ia23 is a 2×2 array of interval ratios:

┌─────┬──────────────────────┬────────────────┐ │ Row │ Intervals │ Ratios │ ├─────┼──────────────────────┼────────────────┤ │ 0 │ halfstep, wholestep │ 1.0595, 1.1225 │ ├─────┼──────────────────────┼────────────────┤ │ 1 │ minor 3rd, major 3rd │ 1.1892, 1.2599 │ └─────┴──────────────────────┴────────────────┘

The generator picks a row (with probability newIntervalArrayProbability = 0.2), then picks a column within it (with probability newIntervalProbability = 0.5). So a soprano voice using ia23 tends to move in stepwise motion (row 0) with occasional jumps to thirds (row 1).

The other available keys for comparison:

  • ia2 — steps only: {[hs, ws]}
  • ia3 — thirds only: {[m3, M3]}
  • ia23b — biased toward steps: {[hs,ws], [hs,ws], [hs,ws], [m3,M3]} (3:1 ratio)
  • ia23456 — wide range: {[hs, ws, m3, M3, p4, p5, m6, M6]}
  • ia2345B — used by tenor/bass: {[hs, ws, ws, m3, M3, p4, p5]}

Melody generation (Generator.pde:83-197)

When a voice is cued to play, generator.play(beatsPerPhrase) generates a full phrase of notes. The number of notes is notesPerBeat * beatsPerPhrase. For each note:

. Interval selection — The generator has a 2D intervalArray (e.g. ia23 = {[halfstep, wholestep], [minor3rd, major3rd]}). Two levels of randomness:

. With probability newIntervalArrayProbability (0.2), pick a new row (interval group) - With probability newIntervalProbability (0.5), pick a new column (specific interval) from the current row

This gives the melody a character — it tends to move in one interval family for stretches, then shifts.

  • Direction — With probability newPitchDirectionProbability (0.1-0.15), reverse direction. Direction is +1 (ascending: pitch *= interval) or -1 (descending: pitch /= interval). Low probability means long ascending or descending runs — a true Brownian walk through pitch space.

  • Pitch clamping — If pitch drops below minPitch, it’s multiplied by 1.5 or 2 (up an octave/fifth). If above maxPitch, divided by 4/3 or 2. Each voice has its own range (soprano: 405-1012 Hz, bass: 30-120 Hz).

  • Unison avoidance — If currentPitch == lastPitch, multiply by wholestep to force movement.

  • Rhythm and articulation:

  • With probability restProbability (~0.16), the note is silenced (volume = 0) – With probability 0.02, the note becomes a triplet (3 notes in the time of 1)

    • With probability doubleNoteProbability (~0.05), it becomes a double (2 notes)
    • With probability 0.02, it becomes a pickup (single note on the back half)
    • Otherwise, a single note at full duration
    • Notes on the downbeat (i % meter == 0) get 1.33x volume, creating metric accent
  • Scheduling — Notes are scheduled via Minim’s out.playNote(startTime, duration, instrument). The start time is delay + i * noteSpacing, where noteSpacing = (60/bpm) / notesPerBeat. The whole phrase is queued at once between pauseNotes() and resumeNotes().

Synthesis (ToneInstrument.pde)

Each note is a triangle-wave oscillator (Waves.TRIANGLE) through an ADSR envelope (attack 0.01s, decay 0.05s, sustain 0.5, release 0.5s). The frequency is the raw Hz value from the generator, and amplitude is the computed volume (per-voice volume * global volume meter accent rest mask).

Dynamic evolution

The Interpreter script changes musical parameters over time: volume ramps from 0.5 to 1.1, BPM increases from 144 to 170, creating an accelerando and crescendo. At frame 4300, volume drops to 0 (silence), then gradually rebuilds from 0.1 — a structural pause and restart.

The score

The score for Bebop in C is four parts: soprano, alto, tenor, and bass.

  <title: Op 3 no. 1>
  --
  <part: soprano>
  <cues: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 0 1 1 0 1 1 1 1>
  <volume: 0.26>
  <maxPitch: 1012.5>
  <minPitch: 405>
  <firstPitch: 540>
  <intervalArrayKey: ia23>
  <newPitchDirectionProbability: 0.1>
  <meter: 5>
  <notesPerBeat: 4>
  <relativeNoteDuration: 0.15>
  <restProbability: 0.16>
  <doubleNoteProbability: 0.05>
  <beatsOfPhraseOverlap: 5>
  --
  <part: alto>
  <cues: 0 0 0 0 0 0 1 0 1 1 0 1 1 0 0 0 1 1 0 0 0 0 1 1 0 0 1 1>
  <volume: 0.36>
  <maxPitch: 675>
  <minPitch: 279>
  <firstPitch: 270>
  <intervalArrayKey: ia23b>
  <newPitchDirectionProbability: 0.1>
  <meter: 3>
  <notesPerBeat: 2>
  <relativeNoteDuration: 0.15>
  <restProbability: 0.16>
  <doubleNoteProbability: 0.05>
  <beatsOfPhraseOverlap: 10>
  --
  <part: tenor>
  <cues: 0 0 1 1 1 0 0 0 0 1 1 0 0 0 1 1 1 0 1 1 1 1 0 1 1 1 1 1>
  <volume: 0.46>
  <maxPitch: 360>
  <minPitch: 90>
  <firstPitch: 180>
  <intervalArrayKey: ia2345B>
  <newPitchDirectionProbability: 0.1>
  <meter: 4>
  <notesPerBeat: 2>
  <relativeNoteDuration: 0.25>
  <restProbability: 0.16>
  <doubleNoteProbability: 0.01>
  <beatsOfPhraseOverlap: 10>
  --
  <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>

Scratch work

One can maximize variety by flipping a coin to generate a sequence of unconnected random notes. This strategy maximizes variety. It quickly becomes unsatisfying. Another strategy is to repeatedly play one note. Also unsatisfying. The way out is a suitable balance between repetition, or some other deterministic way of making choices, and random choice.

Video is just a sequence of images which for this program we manufacture algorithmically. Again, one has the extreme choices: the same image in every frame (boring) or random images, each frame unrelated to the previous one (chaotic, irritating, and unphysical, with no sense of movement.)

Generating a sequence of images

Golden ratio
Golden ratio

Bebop in C begins with a blank rectangle divided into squares as you see in Figure 1 on the left. The program paints colored triangles into each square and also changes the overall color of the square. It will do this in a way that has some randomness to it, but not too much, so that we adhere to our “variety within unity” principle.

Let’s talk about painting the triangles first. To paint a triangle we need to know where the vertices are. Each vertex has an \(x\) and a \(y\) coordinate, so is given by a pair \((x,y)\). Since there are three vertices, a triangle is specifed by a 6-tuple of numbers \(T = (x1, y1, x2, y2, x3, y3)\). Choose these numbers somehow and choose a color, say “blue”. Next, choose small random numbers \(dx1, dy1, dx2, dy2, dx3\). We add these small numbers to the numbers of \(T\) to get a new triangle,

\[ T' = (x1 + dx1, y1 + dy1, x2 + dx2, y2 + dy2, x3 + dx3, y3 + dy3) \]

In this way we can generate a whole series of triangles \(T_0, T_1, T_2, \ldots\), each painted blue. If that color of blue is semi-transparent this process builds a bunch of overlapping tryiangles that form an interesting pattern, like this:

public
public

Each square is painted with a color that slow evolves over time.

The color is determined by a triple of numbers \((r,g,b)\) which determine how much red, green, and blue light the screen emits for points in that square. That color evolves over time by adding small positive or negative changes to the red, blue and green values. These lie in the range from 0 to 255, so if they go out of bounds we reset them. If the red value is bigger than 255, we set it to zero. If the red value is less that 0, we set it to 255.

An important detail: we lay down the new color in a square after we have paint any shapes in . We also lay it down in a kind of semitransparent paint, sothat the lines and shapes already there are slowly obliterated. This adds visual intereste.

Generating the music

public

. We achieve variety using sequences of random numbers. The most basic way, say for music, is to choose pitches and durations at random and string them together one after the other. This does not produce what anyone recognizes as music: it is entirely without structure or direction, though it is an interesting parlour trick.