Per the instructions, here is my primary project link (submitted in Canvas). For your convenience, I’ve also attached the raw ChucK source file for review and grading.
Tip: If Canvas only allows a URL, this HTML page contains the downloadable code and a readable copy below.
// =================== SIMPLE MODAL JAM ===================
// Lead: random E Dorian (2 octaves) + reverb
// Drone: single E3 with slow gain oscillation
// Rhythm: kick every beat
// ---------- GRAPHICS FLAG ----------
global int kickFlash; // 0/1: set to 1 when kick triggers
global float droneVis; // 0..1 mirror of the drone's current gain
// lead ? visuals bridge
global int leadTrig; // set to 1 on each note-on
global float leadFreq; // Hz of current note
global float prevFreq; // Hz of previous note
global float leadVel; // 0..1 (how strong the note is; use a constant if no envelope)
global float leadDurSec; // duration (in seconds) of the current lead note
// ---------- TEMPO ----------
120 => float BPM; // tweak tempo
(60.0 / BPM)::second => dur beat; // 1 beat
// ---------- LEAD (random notes + reverb) ----------
SawOsc lead => Gain lg => JCRev rv => dac;
0.25 => lg.gain;
0.20 => rv.mix; // tweak reverb amount (0..1)
// E Dorian (E F# G A B C# D) across two octaves: E3..D5
[52,54,55,57,59,61,62, 64,66,67,69,71,73,74] @=> int E_DORIAN[];
fun void leadRandom() {
// start aligned to an 8th-note grid
(beat/2) - (now % (beat/2)) => now;
// base lengths to choose from: 8ths, quarters, halves
[ beat/2, beat, 2::beat ] @=> dur baseChoices[];
0.06 => float jitterFrac; // +-6% timing jitter
while (true) {
// --- pick pitch ---
Math.random2(0, E_DORIAN.size()-1) => int i;
// compute the frequency once
Std.mtof(E_DORIAN[i]) => float f;
// finally set the oscillator
f => lead.freq;
// --- timing (quantized with light jitter) ---
baseChoices[Math.random2(0, baseChoices.size()-1)] => dur base;
(base / samp) => float baseSamps;
Math.random2f(-jitterFrac, jitterFrac) * baseSamps => float jitterSamps;
Math.max((5::ms / samp), baseSamps + jitterSamps) $ int => int totalSamps;
// >>> expose exact duration in seconds for visuals <<<
(totalSamps :: samp / second) $ float => leadDurSec;
// --- update globals for the visual (orbits) ---
leadFreq => prevFreq; // remember previous note
f => leadFreq; // current note
0.8 => leadVel; // or Math.random2f(0.6,0.9)
// trigger *after* we?ve set leadFreq and leadDurSec
1 => leadTrig; // spawn an orbit this note
// wait for this note's actual (jittered) duration
(totalSamps :: samp) => now;
}
}
spork ~ leadRandom();
// ---------- DRONE (one note with gently shifting volume) ----------
SinOsc drone => Gain dG => dac;
Std.mtof(52) => drone.freq; // E3
0.0 => dG.gain; // start silent
SinOsc lfo => blackhole; // slow LFO for gain
0.20 => lfo.freq; // ~5s cycle
0.10 => float DRONE_MIN;
0.45 => float DRONE_MAX;
fun void droneSwell() {
while (true) {
DRONE_MIN + ((lfo.last() + 1.0) * 0.5) * (DRONE_MAX - DRONE_MIN) => dG.gain;
dG.gain() => droneVis; // expose to visuals
10::ms => now;
}
}
spork ~ droneSwell();
// ---------- RHYTHM: KICK (808-style synth) ----------
fun void kick808() {
// Body: sine -> amp env -> mix
SinOsc body => ADSR amp => Gain mix => dac;
// Click: noise -> highpass -> short env -> mix
Noise n => HPF hp => ADSR click => mix;
// Levels
0.6 => mix.gain; // overall kick loudness (raise/lower to taste)
0.9 => body.gain; // body dominates
0.25 => n.gain; // click is subtle
// Envelopes
amp.set(1::ms, 120::ms, 0.0, 25::ms); // fast attack, short decay
click.set(1::ms, 15::ms, 0.0, 5::ms); // tiny transient
1800 => hp.freq; // brighten click
// Timing helpers
70::ms => dur sweep; // pitch sweep length (feel of the "thump")
20 => int steps; // smoothness of sweep
// Sync to beat grid
beat - (now % beat) => now;
while(true) {
// slight natural variation each hit
Math.random2f(110.0, 140.0) => float fStart; // Hz
Math.random2f(40.0, 55.0) => float fEnd; // Hz
// trigger envelopes
amp.keyOn();
click.keyOn();
1 => kickFlash; // <-- set flag for the light pulse
// exponential pitch sweep: f(t) = fStart * (fEnd/fStart)^t
for (0 => int i; i < steps; i++) {
(i $ float) / (steps - 1) => float t;
fStart * Math.pow(fEnd / fStart, t) => body.freq;
(sweep / steps) => now;
}
// release envelopes
click.keyOff();
amp.keyOff();
// wait the rest of the beat
(beat - sweep) => now;
}
}
spork ~ kick808();
//visuals
// ===== ChuGL: side stage lights (pastel blue) that flash on kick =====
fun void visualsKickSideBars()
{
// set window title (optional size is handled automatically)
GG.windowTitle( "Kick Side Lights" ); // API style in examples
// ----- geometry: two tall, thin planes added to the default scene -----
// connect geometry into the scene (example shows: GPlane --> GG.scene())
GPlane leftGeo --> GG.scene();
GPlane rightGeo --> GG.scene();
// scale & position (normalized-ish scene coordinates)
// width ~0.05 looks like a ~10px sliver on ~800?900px window;
// height 2 fills the view vertically with default camera.
@(0.55, 5.0, 1) => leftGeo.sca;
@(-.99, 0, 0) => leftGeo.pos;
@(0.55, 5.0, 1) => rightGeo.sca;
@( 0.99, 0, 0) => rightGeo.pos;
// make flat-color materials (pastel blue) and assign
FlatMaterial leftMat ( Color.WHITE );
FlatMaterial rightMat( Color.WHITE );
leftGeo.mat(leftMat);
rightGeo.mat(rightMat);
// base pastel blue
@(0.0, 0.3, 1.0) => vec3 baseCol; // strong saturated blue
// flash accumulator (0..1), exponential-ish decay per frame
0.0 => float flash;
0.90 => float decay; // closer to 1.0 = longer glow
// render loop (step one graphics frame at a time)
while (true)
{
// bump when kick fires, otherwise decay
if (kickFlash == 1) { 1.0 => flash; 0 => kickFlash; }
else { flash * decay => flash; }
// brighten toward white on flash; lerp base -> white
// c = base*(1 - a) + white*a
1.0 => float a;
(a * flash) => a;
@( baseCol.x * (1 - a) + 1.0 * a,
baseCol.y * (1 - a) + 1.0 * a,
baseCol.z * (1 - a) + 1.0 * a ) => vec3 c;
leftMat.color(c);
rightMat.color(c);
// draw next frame (per docs: first call also ensures window is created)
GG.nextFrame() => now; // sync render & advance time
}
}
spork ~ visualsKickSideBars();
// ===== ChuGL: gradient sky tied to drone gain (Cosmic Wash) =====
fun void visualsDroneGradient()
{
GWindow.windowed(900, 500);
GWindow.title("Drone Gradient Sky");
// background plane in the default scene
GPlane bg --> GG.scene();
@(2.6, 5.0, 1.0) => bg.sca; // size (X,Y,Z)
@(0.0, 0.0, -0.1) => bg.pos; // slightly behind
// colors: quiet = deep navy, loud = cyan/teal
@(0.05, 0.05, 0.25) => vec3 navy;
@(0.35, 0.85, 1.00) => vec3 teal;
(1::second/60) => dur frame; // ~60 FPS
while (true)
{
// clamp droneVis (0..1)
Math.min(1.0, Math.max(0.0, droneVis)) => float dv;
// interpolate: navy*(1-dv) + teal*dv
(navy.x * (1.0 - dv) + teal.x * dv) => float r;
(navy.y * (1.0 - dv) + teal.y * dv) => float g;
(navy.z * (1.0 - dv) + teal.z * dv) => float b;
@(r, g, b) => vec3 rgb;
bg.color(rgb);
GG.nextFrame() => now;
frame => now;
}
}
// start the gradient visual
spork ~ visualsDroneGradient();
// ---------- SAFETY & COLOR HELPERS ----------
fun vec3 pcColor(float hz) {
if (hz <= 0.0 || Math.isinf(hz) || Math.isnan(hz)) return @(0.85, 0.90, 1.00);
Std.ftom(hz) => float midi;
(Math.floor(midi + 0.5) $ int) % 12 => int pc;
if(pc == 0 ) return @(1.00, 0.60, 0.60); // C
if(pc == 1 ) return @(1.00, 0.75, 0.55); // C#
if(pc == 2 ) return @(1.00, 0.95, 0.60); // D
if(pc == 3 ) return @(0.85, 1.00, 0.65); // D#
if(pc == 4 ) return @(0.65, 1.00, 0.75); // E
if(pc == 5 ) return @(0.60, 1.00, 0.95); // F
if(pc == 6 ) return @(0.60, 0.85, 1.00); // F#
if(pc == 7 ) return @(0.65, 0.70, 1.00); // G
if(pc == 8 ) return @(0.80, 0.65, 1.00); // G#
if(pc == 9 ) return @(0.95, 0.65, 1.00); // A
if(pc == 10) return @(1.00, 0.65, 0.85); // A#
return @(1.00, 0.60, 0.70); // B
}
// bright impact color (white flash)
fun vec3 impactColor(vec3 _) { return @(1.0, 1.0, 1.0); }
// linear blend (0..1)
fun vec3 mix(vec3 a, vec3 b, float t) {
t => float u; if(u < 0.0) 0.0 => u; if(u > 1.0) 1.0 => u;
return @(
a.x*(1.0-u) + b.x*u,
a.y*(1.0-u) + b.y*u,
a.z*(1.0-u) + b.z*u
);
}
// ----- prevent multiple window inits if you re-run the file -----
global int _visuals_inited;
// ===== ChuGL: Starfield Orbits for the lead (safe + synced + size variety) =====
fun void visualsLeadOrbits()
{
if(_visuals_inited == 0) {
// window + title (init once)
GWindow.windowed(900, 500);
GWindow.title("Lead Orbits");
1 => _visuals_inited;
}
// center nucleus
GPlane center --> GG.scene();
@(0.06, 0.06, 1.0) => center.sca;
@(0.0, 0.0, 0.0) => center.pos;
center.color(@(0.90, 0.95, 1.00));
// pool of dots (planes)
24 => int MAX;
GPlane dots[MAX];
for (0 => int i; i < MAX; i++) {
GPlane p --> GG.scene();
// avoid exact zero scale on X/Y
@(0.0001, 0.0001, 1.0) => p.sca; // hidden until used
@(0.0, 0.0, 0.0) => p.pos;
p @=> dots[i];
}
// per-dot state
float ang[MAX]; // angle (rad)
float rad[MAX]; // base radius for X
float omg[MAX]; // angular speed (rad/sec)
float life[MAX]; // remaining life (sec)
float lifeMax[MAX]; // max life (sec)
float size0[MAX]; // starting dot size (with jitter)
vec3 baseC[MAX]; // pitch color
vec3 hitC[MAX]; // impact flash color
float sinceSpawn[MAX]; // seconds since spawn
int alive[MAX]; // 0/1
// parameters (unchanged feel)
0.03 => float R_PER_SEMITONE;
0.06 => float R_MIN;
0.60 => float R_MAX; // width cap unchanged (keep > 0)
0.05 => float SIZE_MIN;
0.11 => float SIZE_MAX;
0.20 => float OMG_MULT; // spin multiplier (lower = slower)
// NEW: size variety + optional pulse
0.35 => float SIZE_JITTER; // +-35% random size variance at spawn
0.10 => float PULSE_AMT; // 0..~0.2 nice; set 0.0 to disable
2.0 => float PULSE_HZ; // pulses per second
// vertical stretch to +-5.0 (guard divisor)
5.0 => float Y_MAX;
(Y_MAX / Math.max(R_MAX, 0.0001)) => float Y_SCALE;
// impact flash duration
0.10 => float IMPACT_DUR; // 100 ms flash
// frame timing (guard dt)
(1::second/60) => dur frame;
Math.max(1.0/600.0, (frame / second) $ float) => float dt;
while (true)
{
// spawn EVERY time we get a note-on (even if same pitch)
if (leadTrig == 1) {
0 => leadTrig; // consume trigger immediately
// find free slot (or overwrite oldest index 0 if none)
-1 => int s;
for (0 => int i; i < MAX; i++) if (!alive[i]) { i => s; break; }
if (s == -1) 0 => s;
// snapshot current/prev freq (guard values)
(leadFreq > 0.0 && !Math.isnan(leadFreq)) ? leadFreq : 220.0 => float curr;
(prevFreq > 0.0 && !Math.isnan(prevFreq)) ? prevFreq : curr => float prev;
// width radius from INTERVAL (in semitones), clamped
Math.fabs(Std.ftom(curr) - Std.ftom(prev)) => float semis;
(semis * R_PER_SEMITONE) => float rTmp;
Math.max(R_MIN, Math.min(R_MAX, rTmp)) => rad[s];
// pitch-locked spin; clamp to sane range
(2.0 * Math.PI * curr * OMG_MULT) => float omgTmp;
if(Math.isnan(omgTmp) || Math.isinf(omgTmp)) 0.0 => omgTmp;
omgTmp => omg[s];
// lifetime = exact note duration, with floor/ceiling
Math.max(0.01, Math.min(10.0, Math.isnan(leadDurSec) ? 0.25 : leadDurSec)) => lifeMax[s];
lifeMax[s] => life[s];
// size from velocity, then add jitter (NEW)
(Math.isnan(leadVel) ? 0.8 : Math.min(1.0, Math.max(0.0, leadVel))) => float v;
SIZE_MIN + (SIZE_MAX - SIZE_MIN) * v => float baseSz;
(1.0 + Math.random2f(-SIZE_JITTER, SIZE_JITTER)) * baseSz => size0[s];
// clamp to safe bounds
if (size0[s] < 0.02) 0.02 => size0[s];
if (size0[s] > 0.25) 0.25 => size0[s];
// colors: flash then settle to pitch color
pcColor(curr) => baseC[s];
impactColor(baseC[s]) => hitC[s];
0.0 => sinceSpawn[s];
// start angle
Math.random2f(0.0, 2.0*Math.PI) => ang[s];
1 => alive[s];
}
// updates
for (0 => int i; i < MAX; i++) {
if (!alive[i]) continue;
life[i] - dt => life[i];
sinceSpawn[i] + dt => sinceSpawn[i];
if (life[i] <= 0.0) {
0 => alive[i];
// hide safely (avoid exact zero scale on both X/Y)
@(0.0001, 0.0001, 1.0) => dots[i].sca;
continue;
}
// motion: width same, Y stretched
ang[i] + omg[i]*dt => ang[i];
Math.cos(ang[i]) * rad[i] => float x;
Math.sin(ang[i]) * rad[i] * Y_SCALE => float y;
// guard NaNs
if(Math.isnan(x) || Math.isinf(x)) 0.0 => x;
if(Math.isnan(y) || Math.isinf(y)) 0.0 => y;
// impact flash -> base color blend
Math.max(0.0, 1.0 - (sinceSpawn[i] / Math.max(IMPACT_DUR, 0.0001))) => float flashT; // 1..0
mix(baseC[i], hitC[i], flashT) => vec3 currC;
// fade brightness with age
(life[i] / Math.max(lifeMax[i], 0.0001)) => float a; // 1..0
if(Math.isnan(a) || Math.isinf(a)) 0.0 => a;
@( currC.x * a, currC.y * a, currC.z * a ) => vec3 outC;
// NEW: optional gentle pulse over lifetime
float pulse;
if (PULSE_AMT > 0.0) {
((lifeMax[i]-life[i]) * 2.0 * Math.PI * PULSE_HZ) => float tphase;
(1.0 + PULSE_AMT * Math.sin(tphase)) => pulse;
} else { 1.0 => pulse; }
// size with floor
(size0[i] * (0.4 + 0.6*a) * pulse) => float sz;
if(sz < 0.0002) 0.0002 => sz; // avoid fully zero scale
@(sz, sz, 1.0) => dots[i].sca;
@(x, y, 0.0) => dots[i].pos;
dots[i].color(outC);
}
GG.nextFrame() => now; // per-frame step (required)
frame => now;
}
}
// run visuals (safe to call multiple times; init guarded)
spork ~ visualsLeadOrbits();
// ---------- keep VM alive ----------
while (true) 1::second => now;