Seash — Lab Bench Proof of Concept #
A minimal, readable bytebeat synthesizer. ~350 lines of clear code. Perfect for understanding the mechanism and experimenting.
What It Does #
Seash is the distilled core of Seashells:
- Generate sound using bytebeat (simple mathematical synthesis)
- Auto-spawn voices on a loop (hold sequence)
- Play interactively by touching the screen
- Press H to toggle automatic playback
No complex feedback loops. No dense visuals. Just the mechanism.
How It Works (Conceptual) #
Layer 1: Sound Generation (Bytebeat) #
The core idea: Audio from bit operations on integers.
// Two patterns that blend over time
pattern1 = (t ^ (t >> 8) ^ (t >> 9)) & 255 // XOR: crisp, digital
pattern2 = ((t * harmonic) & (t >> 5) | (t >> 4)) & 255 // Melodic: pitched
// Mix them based on time
finalPattern = pattern1 * (1 - blend) + pattern2 * blend
Why it works: Integer operations are deterministic. Same input → same output. But small changes in parameters create wildly different sounds.
Key insight: The two patterns never collide. When one fades out, the other fades in. This creates a continuous, evolving texture.
Layer 2: Spatial Control (Touch → Frequency) #
How position maps to sound:
// X-axis (left to right)
X = 0% → 55 Hz (very low)
X = 50% → 220 Hz (middle)
X = 100% → 880 Hz (high)
// Logarithmic scale (musically natural)
// Y-axis (top to bottom)
Y = 0% → 2.0x pitch multiplier (octave up)
Y = 50% → 1.0x pitch multiplier (normal)
Y = 100% → 0.5x pitch multiplier (octave down)
Result: Any (X, Y) position has a unique frequency. Move around = explore the frequency space.
Why logarithmic: Our ears perceive pitch logarithmically. A 2x frequency jump feels the same from 100Hz→200Hz as from 1000Hz→2000Hz.
Layer 3: Multi-Voice Management #
The system tracks multiple simultaneous voices:
const touchVoices = new Map() // { pointerIndex → { sound, frequency, x, y } }
When you touch:
- New voice spawns at that position
- Frequency derived from X/Y
- Voice added to map with unique pointer ID
When you move your finger:
- Voice position updates
- Frequency updates smoothly (with lerp to avoid jumps)
When you lift:
- Voice fades out (0.08 second fade)
- Removed from map
Volume balancing: If you have N fingers down, each voice volume = 0.5 / sqrt(N)
- 1 voice: 0.5 volume
- 4 voices: 0.25 each (stays reasonable)
- 16 voices: 0.125 each
Layer 4: Auto-Voice Generation (Hold Sequence) #
The magic mechanism:
if (holdSequence.enabled) {
// Every 3 seconds, spawn a new voice
spawnHoldVoice()
// Each voice:
// - Starts at orbital position
// - Drifts along orbit
// - Lasts 6±1.5 seconds
// - Fades out when duration expires
// Result: continuous audio, voices never sync
}
Why it works for 90 minutes:
- New voice spawns before old one dies
- Each has different orbit speed (unique drifting path)
- No two voices follow same trajectory
- Even though only 2 patterns, constant variation
Why voices don't sync:
- Orbit speeds differ by small amounts (0.0003–0.0005 rad/frame)
- After 30 seconds, they're all at different phases
- Mathematical: irrational multiples → infinite non-repetition
The Code (Line by Line) #
Sections #
Lines 1–20: Constants and initialization
touchVoices: Map of active voicesholdSequence: State for auto-generation
Lines 22–64: Bytebeat generator
- Takes frequency, time, and sample count
- Returns 512 audio samples
- Blends 2 patterns smoothly
Lines 66–80: Utility functions
clamp(): Constrain values to rangemapXToFrequency(): X pixel → Hz (logarithmic)mapYToPitch(): Y pixel → pitch factorderiveFrequency(): Combine X+Y
Lines 82–130: Voice lifecycle
createVoice(): Initialize synthesizerstartTouchVoice(): New voice from touchupdateTouchVoice(): Move existing voicestopTouchVoice(): Fade and removerebalanceVolumes(): Keep mix balanced
Lines 132–175: Hold sequence
spawnHoldVoice(): Create auto voice at orbital positionupdateHoldVoices(): Move orbits, spawn new, fade oldtoggleHoldSequence(): Start/stop auto mode
Lines 177–210: Rendering
paint(): Draw grid, labels, voice positions- Minimal UI: just frequency readouts and status
Lines 212–247: Input handling
act(): Touch and keyboard events- Support 8 simultaneous touches
- H key toggles hold sequence
Lines 249–253: Per-frame updates
sim(): Audio polling, hold sequence updates
How to Use It #
Interactive Mode #
Touch screen → voice spawns at that position
Move finger → frequency changes in real-time
Lift finger → voice fades out
Multi-touch: 8 fingers at once, each with own voice
Autoplay (Tape Mode) #
Press H → hold sequence starts
Voices spawn automatically every 3 seconds
Each holds for 6±1.5 seconds
Press H again → stops
Let run for 90 minutes → listen to emergence
Observe the Mechanism #
Watch voice numbers increase and decrease
Notice frequency labels updating as voices move
See hold sequence spawn new voice before old fades
Listen to how patterns blend smoothly
Why It's a Good Lab Bench #
1. Readable #
- ~350 lines (vs 800+ for seashells.mjs)
- No visual feedback loop
- No complex state decay
- Direct cause-effect
2. Modifiable #
Each section is independent:
Change sound: Edit generator.bytebeat() (line 22)
- Add pattern 3: just write another formula
- Integrate it into blending
Change spatial mapping: Edit mapXToFrequency() (line 69)
- Try linear instead of logarithmic
- Try quantized to specific notes
- Try polar coordinates
Change auto-generation: Edit spawnHoldVoice() (line 135)
- Different spawn positions (grid, random, bounded)
- Different orbit speeds
- Different durations
Change UI: Edit paint() (line 179)
- Remove grid, add oscilloscope
- Add different text labels
- Show orbit paths visually
3. Testable #
- Add
console.log()anywhere to debug - Change one variable, test immediately
- No side effects (each voice is independent)
4. Minimal Dependencies #
- Only uses AC's
sound.synth()API - No external libraries
- No complex state machines
Experiments to Try #
Experiment 1: Change the Patterns #
Goal: Make it sound more chaotic
// In generator.bytebeat(), change p1:
const p1 = (t * t) & (t >> 4) & 255; // Multiplicative instead of XOR
// Test: Press H, listen to how it differs
Experiment 2: Change Frequency Range #
Goal: Make it higher or lower pitched
// In mapXToFrequency():
const minHz = 110; // Was 55 (raise minimum)
const maxHz = 440; // Was 880 (lower maximum)
// Test: Press H, notice narrower frequency range
Experiment 3: Change Auto-Spawn Rate #
Goal: More or fewer voices
// In holdSequence:
spawnInterval: 1500, // Was 3000 (spawn every 1.5 sec instead of 3)
// Test: More voices, denser texture
Experiment 4: Change Voice Lifespan #
Goal: Longer or shorter holds
// In spawnHoldVoice():
const duration = 15000 + (Math.random() - 0.5) * 5000; // Was 6000 ± 1500
// Test: Slower, more meditative tape
Experiment 5: Add a Third Pattern #
Goal: More timbral variety
// In generator.bytebeat(), add:
const p3 = ((t >> 2) + (t >> 5)) & 255; // Additive pattern
// Modify blending to include it:
let mixPhase = (time * 0.1 + freqScale * 0.5) % 3; // Was % 2
if (mixPhase < 1) { /* p1 to p2 */ }
else if (mixPhase < 2) { /* p2 to p3 */ }
else { /* p3 to p1 */ }
// Test: Three-way blend creates richer texture
What's Removed (vs Seashells) #
Removed: #
- Feedback loop (pixels don't influence audio)
- 5 patterns (reduced to 2 for clarity)
- Complex UI (minimal text only)
- Grid visualization (just dots)
- State decay system (removed unnecessary complexity)
- Chaos injection (simplified synthesis)
- Harmonic Bell quantization (bare version uses full spectrum)
Kept: #
- Core bytebeat synthesis
- Spatial frequency mapping
- Multi-voice management
- Hold sequence (auto-generation)
- Smooth blending between patterns
Performance Notes #
CPU usage: Very light
- 2 patterns instead of 5
- No pixel sampling
- No feedback loop
- Simple rendering
Audio quality: 8-bit bytebeat character (lo-fi by design)
Latency: Minimal — direct synthesis, no heavy processing
Next Steps #
To Understand Deeper: #
- Run it and experiment with touches
- Press H and listen for 5 minutes
- Add
console.log()to understand timing - Modify one parameter at a time
To Build Variations: #
- Copy seash.mjs to seashell_variation.mjs
- Make ONE change
- Test immediately
- Document what changed and why
- Stack changes incrementally
To Connect to Seashells: #
Once you understand seashell:
- Read seashells_conceptual_model.md
- Understand how feedback loop works
- Add feedback sampling to seashell
- You've now built seashells from scratch!
Philosophy #
Seashell embodies these principles:
- Clarity over cleverness — Every line should make sense
- Mechanism over magic — You can trace cause-effect
- Minimal abstraction — One function per concept
- Modular independence — Change one layer, others stay stable
- Testability — Add print statements, change values, listen
Use it as a thinking tool, not just a sound generator.
Quick Reference #
| What | Where | How |
|---|---|---|
| Change sound | generator.bytebeat() line 22 |
Modify pattern formulas |
| Change frequency range | mapXToFrequency() line 69 |
Edit minHz/maxHz |
| Change pitch modulation | mapYToPitch() line 75 |
Edit exponent (2) |
| Change auto-spawn timing | holdSequence.spawnInterval line 24 |
Edit milliseconds |
| Change voice duration | spawnHoldVoice() line 149 |
Edit duration calculation |
| Change voice count limit | updateHoldVoices() line 161 |
Edit .length < 5 |
| Change rendering | paint() line 179 |
Modify drawing code |
| Change audio response | sim() line 245 |
Edit voice update logic |
Files to Compare #
- seash.mjs (this one) — bare bones, ~350 lines
- seashells.mjs — full version, ~900 lines, with feedback
- seashells_conceptual_model.md — 4-layer architecture explanation
You can learn by:
- Understanding seashell completely
- Reading conceptual_model.md
- Comparing against seashells.mjs to see what each layer adds
Good lab bench = good foundation for understanding the full piece.