/* * HW1: create a chuck/chugl program that makes sound AND has real-time animated graphics * petal-spiral.ck * 2026/SEP/29 * Felipe Rocha Campos * * visualize a tilting spiral of petals that change color based on midi notes * * requires assets/petal.obj (petal model from visualizer I worked on this summer) and assets/onsets.txt (exported from ableton) */ // load petal obj GModel model(me.dir() + "assets/petal.obj"); // fail if no asset if (model.geometries.size() == 0) { <<< "missing petal.obj" >>>; me.exit(); } @(0.5, 0.01, 0.05) => vec3 velvetRed; // starting color // configure n petals in an xy grid layout 12 => int nPetals; 2.5 => float petalSpacing; Math.ceil(Math.sqrt(nPetals)) $ int => int side; vec3 petalGrid[nPetals]; for (int i; i < nPetals; i++) { i % side => int x; i / side => int y; @((x - (side - 1) / 2.0) * petalSpacing, (y - (side - 1) / 2.0) * petalSpacing, 0) => petalGrid[i]; } // one material per petal so we can color independently GMesh petals[nPetals]; PBRMaterial velvet[nPetals]; vec3 noteColor[nPetals]; // color each petal is heading toward vec3 shownColor[nPetals]; // color each petal shows right now for (int i; i < petals.size(); i++) { velvet[i].roughness(0.88); velvet[i].cullMode(Material.Cull_None); // seems to help rendering of thin petals vs Cull_Back but unclear velvetRed => noteColor[i] => shownColor[i]; velvet[i].color(velvetRed); petals[i].mesh(model.geometries[0], velvet[i]); petals[i].sca(1.6); // scale up a bit petals[i].pos(petalGrid[i]); petals[i] --> GG.scene(); } // camera settings GWindow.windowed(640, 640); GG.camera().perspective(); GG.camera().pos(@(0, 0, 15)); GG.camera().lookAt(@(0, 0, 0)); // GG.camera().orthographic(); // GG.camera().viewSize(8.0); GG.scene().light().intensity(2.0); // 3. exported clip onsets from ableton proj FileIO file; if (!file.open(me.dir() + "assets/onsets.txt", FileIO.READ)) me.exit(); file.readInt(IO.INT32) => int count; file.readFloat() => float loopBeats; float onset[count]; int pitch[count]; for (int i; i < count; i++) { file.readFloat() => onset[i]; file.readInt(IO.INT32) => pitch[i]; } file.close(); 0 => int withTrack; 0.0 => float runSeconds; if (me.args()) { if (me.arg(0) == "track") 1 => withTrack; else Std.atof(me.arg(0)) => runSeconds; } if (me.args() > 2 && me.arg(2) == "track") 1 => withTrack; SndBuf2 track => dac; if (withTrack) { track.read(me.dir() + "assets/ableton-bounce.wav"); if (!track.ready()) { <<< "no assets/ableton-bounce.wav" >>>; me.exit(); } track.rate(0); track.gain(0.5); if (runSeconds <= 0) track.length() / second => runSeconds; } // 120 bpm 0.5::second => dur beat; SinOsc tone => ADSR envelope => dac; // play 2 speaker yay 0.10 => tone.gain; envelope.set(8::ms, 70::ms, 0.45, 40::ms); // hsv -> rgb mirroring python's colorsys.hsv_to_rgb // https://github.com/python/cpython/blob/main/Lib/colorsys.py fun vec3 hsv(float h, float s, float v) { (h - Math.floor(h)) * 6.0 => float hh; Math.floor(hh) $ int => int sector; hh - sector => float f; v * (1 - s) => float p; v * (1 - s * f) => float q; v * (1 - s * (1 - f)) => float r; if (sector == 0) return @(v, r, p); if (sector == 1) return @(q, v, p); if (sector == 2) return @(p, v, r); if (sector == 3) return @(p, q, v); if (sector == 4) return @(r, p, v); return @(v, p, q); } // midi note -> color via circle of fifths - octave sets brightness note sets color fun vec3 midiColor(int midi) { ((midi % 12) * 7) % 12 => int fifths; // https://en.wikipedia.org/wiki/Multiplication_(music) "When the chromatic scale is multiplied by 7 (mod 12), the result is a cycle of fifths." trick haha midi / 12 - 1 => int octave; Math.max(0.2, Math.min(0.75, 0.25 + 0.06 * octave)) => float bright; return hsv(fifths / 12.0, 0.95, bright); } // trigger sound and set colors based on current midi fun void play() { while (true) { now => time start; for (int i; i < count; i++) { start + onset[i] * beat => now; Std.mtof(pitch[i]) => tone.freq; envelope.keyOn(); // newest note colors the inner petal w/ older notes rippling outward - not quite right atm but will come back to this for (petals.size() - 1 => int j; j > 0; j--) noteColor[j-1] => noteColor[j]; midiColor(pitch[i]) => noteColor[0]; loopBeats => float nextBeat; if (i+1 < count) onset[i+1] => nextBeat; start + nextBeat * beat - 40::ms => now; envelope.keyOff(); } start + loopBeats * beat => now; } } // motion spiral pattern using polar coords repeat (5) GG.nextFrame() => now; // wait a sec cuz my mac laggy af now => time started => time previous; track.pos(0); track.rate(1); spork ~ play(); 0.0 => float spin; -0.9 => float tilt; 1 => int tiltDir; while (true) { GG.nextFrame() => now; (now - previous) / second => float dt; now => previous; (now - started) / second => float t; 2 * dt +=> spin; if (Std.fabs(tilt) > 1.0) { -1 * tiltDir => tiltDir; // oscillate between -1 and 1 } 0.03 * tiltDir +=> tilt; // <<< "tilt: " + tilt >>>; // <<< "tiltDir: " + tiltDir >>>; 0.5 * (1 - Math.cos(2*pi*t / 8.0)) => float blend; for (int i; i < petals.size(); i++) { 0.8 + 0.5 * i => float radius; // move each petal a little farther out 1.5 * i + spin => float angle; // rotate around // set w/ polar coords radius * Math.cos(angle) => float x; radius * Math.sin(angle) => float y; radius * (Math.sin(angle) + tilt * Math.cos(angle)) => float z; // add some tilt to the spiral @(x, y, z) => vec3 spiral; petals[i].pos(petalGrid[i] * (1 - blend) + spiral * blend); // blend between petalGrid and spiral petals[i].rotZ(angle); // ease toward the note color, flash brighter on each onset shownColor[i] + (noteColor[i] - shownColor[i]) * Math.min(1.0, 8.0 * dt) => shownColor[i]; velvet[i].color(shownColor[i]); } if (runSeconds > 0 && t >= runSeconds) break; }