//----------------------------------------------------------------------------- // name: floral_waves.ck // desc: CS476A HW2, a polar function flower real time audio visualizer. // // author: Elisse Chow // date: Fall 2023 //----------------------------------------------------------------------------- /*----------------------------------------------------------------------------- CONSTANTS -----------------------------------------------------------------------------*/ 0.0 => float THETA_MIN; 8 * Math.PI => float THETA_MAX; 1024 => int MAX_ITERS; // Number of sampled points on the polar curve 1024 => int WINDOW_SIZE; // Window size 256 => int WATERFALL_DEPTH; // Waterfall depth 32 => int NUM_LEAVES; // Number of time domain wave "leaves" 0 => int GRAPH_CHOICE; // Polar flower graph choice 0 => int IN_NARRATIVE; // Boolean for if narrative is playing /*----------------------------------------------------------------------------- CAMERA/WINDOW -----------------------------------------------------------------------------*/ GG.fullscreen(); GG.camera() --> GGen dolly --> GG.scene(); GG.camera().posZ( 8 ); // Arbitrarily set to this distance. GG.camera().lookAt( @(0, 0, 0) ); // Look at center. GG.scene().backgroundColor( @(0, 0, 0) ); // Black background. /*----------------------------------------------------------------------------- NARRATIVE SET UP Reference: rhodey.ck + flute.ck -----------------------------------------------------------------------------*/ Leaves leaves --> GG.scene(); Waterfall waterfall --> GG.scene(); Rhodey r[5]; NRev rev[2] => dac; 0.05 => rev[0].mix; 0.05 => rev[1].mix; Rhodey solo => rev; Pan2 p[2]; p[0] => rev[0]; p[1] => rev[1]; Delay d[2]; rev[0] => d[0] => d[1] => rev[1]; 0.7 => d[0].gain => d[1].gain; second => d[0].max => d[0].delay => d[1].max => d[1].delay; SinOsc panner => blackhole; 1 => panner.freq; r[0] => p[0]; r[1] => p[0]; r[2] => p[1]; r[3] => p[1]; r[4] => p[0]; r[4] => p[1]; for (int i; i < r.cap(); i++) { 1 => r[i].lfoSpeed; 0.0 => r[i].lfoDepth; r[i].opAM(0,0.4); r[i].opAM(2,0.4); r[i].opADSR(0, 0.001, 3.50, 0.0, 0.04); r[i].opADSR(2, 0.001, 3.00, 0.0, 0.04); } // spork panner on its own shred spork ~ doPan(); 0 => int STEP_MIN; 270 => int STEP_MAX; 5::second => dur BUFFER_TIME; 1.5::second => dur FADE_TIME; 150::ms => dur NOTE_DUR; // Some chords. We don't use all here [41, 56, 60, 63, 67] @=> int Fmi9[]; [36, 55, 58, 62, 65] @=> int BfMajC[]; [34, 56, 60, 63, 67] @=> int AfMaj7Bf[]; [43, 53, 58, 62, 65] @=> int Gmi7[]; [62, 66, 69, 74] @=> int DMaj[]; [59, 62, 66, 68] @=> int Bmi6[]; [55, 59, 62, 66] @=> int GMaj7[]; [59, 62, 66, 71] @=> int Bmi[]; [62, 66, 69, 73] @=> int D7[]; [62, 66, 69, 74] @=> int DFs[]; [64, 69] @=> int UH[]; [36, 41, 45, 48, 69] @=> int BOTTOM[]; second/2 => dur Q; Q/2 => dur E; E/2 => dur S; // Accumulate samples from mic. adc => Flip accum => blackhole; // Take the FFT adc => PoleZero dcbloke => FFT fft => blackhole; // Set DC blocker .95 => dcbloke.blockZero; fun void narrative() { 1 => IN_NARRATIVE; // Stop taking in mic input. adc =< accum; // Do not take the mic FFt. adc =< dcbloke; // Accumulate samples from narrative. dac => accum; // Take the FFT dac => dcbloke; .95 => dcbloke.blockZero; 2 => GRAPH_CHOICE; // waterfall.set_colors(@(0, 0, 0.8), @(0, 0, 0)); spork ~ rising_steps(); // Wait for intro to complete BUFFER_TIME + NOTE_DUR * (STEP_MAX - STEP_MIN) + FADE_TIME + 8::second => now; 0 => GRAPH_CHOICE; // Play song play_song(); BUFFER_TIME => now; // Accumulate samples from mic. adc => accum; // Take the FFT adc => dcbloke; .95 => dcbloke.blockZero; 0 => IN_NARRATIVE; } fun void play_song() { spork ~ rollChord(BOTTOM, 1.0 ); 8 * second => now; Std.mtof(69) => solo.freq; 0.6 => solo.noteOn; 2 * Q => now; Std.mtof(64) => solo.freq; 0.6 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.6 => solo.noteOn; Q => now; Std.mtof(64) => solo.freq; 0.6 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.6 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.8 => solo.noteOn; Q => now; Q => now; Std.mtof(74) => solo.freq; 0.8 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.8 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.6 => solo.noteOn; 2 * Q => now; Std.mtof(67) => solo.freq; 0.4 => solo.noteOn; 2 * Q => now; Std.mtof(69) => solo.freq; 0.3 => solo.noteOn; 2 * Q => now; 1 => GRAPH_CHOICE; spork ~ rollChord(DFs, 0.8); 4*second => now; Std.mtof(76) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(74) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(72) => solo.freq; 0.8 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.8 => solo.noteOn; Q => now; spork ~ chord(UH, 0.8); 2*second => now; Std.mtof(67) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(71) => solo.freq; 0.8 => solo.noteOn; 3 * Q => now; spork ~ chord(UH, 0.8); 2*second => now; 2 => GRAPH_CHOICE; spork ~ chord([55, 64], 0.6); 2 * Q => now; spork ~ chord([57, 64, 69], 0.7); 2 * Q => now; spork ~ chord([59, 64], 0.8); 2 * Q => now; spork ~ chord([60, 69], 0.9); 3 * Q => now; 2 * Q => now; spork ~ chord([67, 74], 0.8); Q => now; Q => now; Std.mtof(72) => solo.freq; 0.75 => solo.noteOn; 2 * Q => now; Std.mtof(71) => solo.freq; 0.7 => solo.noteOn; 2 * Q => now; Q => now; Std.mtof(67) => solo.freq; 0.7 => solo.noteOn; 2 * Q => now; Q => now; Std.mtof(69) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(76) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.5 => solo.noteOn; Q => now; 3 * Q => now; 3 => GRAPH_CHOICE; Std.mtof(76) => solo.freq; 0.6 => solo.noteOn; 2 * Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([69, 74], 0.6); Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([67, 72], 0.6); Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([72, 77], 0.6); Q => now; Std.mtof(79) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(74) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(72) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(74) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; 2 * Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([62, 67], 0.6); Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([60, 65], 0.6); Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ chord([65, 70], 0.6); Q => now; Std.mtof(67) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(67) => solo.freq; 0.7 => solo.noteOn; Q => now; Std.mtof(69) => solo.freq; 0.7 => solo.noteOn; Q => now; spork ~ rollChord(Bmi, 0.4); 2 * second => now; 0 => GRAPH_CHOICE; Std.mtof(69) => solo.freq; 0.6 => solo.noteOn; 3 * E => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(72) => solo.freq; 0.7 => solo.noteOn; 3 * E => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(76) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(81) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(76) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; 3 * E => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(72) => solo.freq; 0.6 => solo.noteOn; 3 * E => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(71) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(67) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; 3 => GRAPH_CHOICE; Std.mtof(69) => solo.freq; 0.6 => solo.noteOn; 3 * E => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(72) => solo.freq; 0.7 => solo.noteOn; 3 * E => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(76) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(81) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(76) => solo.freq; 0.8 => solo.noteOn; 2 * Q => now; Std.mtof(76) => solo.freq; 0.7 => solo.noteOn; 3 * E => now; Std.mtof(74) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(72) => solo.freq; 0.6 => solo.noteOn; 3 * E => now; Std.mtof(71) => solo.freq; 0.5 => solo.noteOn; 3 * E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(71) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(67) => solo.freq; 0.4 => solo.noteOn; E => now; Std.mtof(69) => solo.freq; 0.4 => solo.noteOn; E => now; 4 * second => now; } fun void rising_steps() { Flute flute => PoleZero f => rev; 0.1 => flute.jetDelay; 0.6 => flute.jetReflection; 0.530745 => flute.endReflection; 0.6 => flute.noiseGain; 7.725672 => flute.vibratoFreq; 0.176077 => flute.vibratoGain; 0.05 => flute.pressure; Rhodey steps => rev; BUFFER_TIME => now; 0.0 => float saturation; for (STEP_MIN => int i; i < STEP_MAX; i++) { Std.mtof(i) => flute.freq; (STEP_MAX - STEP_MIN - i + 1) / (3 * (STEP_MAX - STEP_MIN)$float) => flute.noteOn; Std.mtof(i) => steps.freq; (i + 1) / (2 * STEP_MAX - STEP_MIN)$float => steps.noteOn; NOTE_DUR => now; // Desmos Calcs: https://www.desmos.com/calculator/btrndqdmkh 38.295 * (i$float / (STEP_MAX - STEP_MIN)$float) => float temp_theta; -4 * (Math.sin(temp_theta) + 3 * Math.sin(temp_theta/(2 * Math.PI))) => float displacement; GG.camera().posZ( 8 + displacement ); // Arbitrarily set to this distance. GG.camera().lookAt( @(0, 0, 0) ); // Look at center. 2 * i / (STEP_MAX - STEP_MIN)$float => saturation; if (saturation > 0.7) { 0.7 => saturation; } waterfall.set_colors(@(0, saturation, 1.0), @(360, saturation, 0.5)); } GG.camera().posZ( 8 ); // Arbitrarily set to this distance. GG.camera().lookAt( @(0, 0, 0) ); // Look at center. waterfall.reset_colors(); FADE_TIME => now; } // @(Math.random2(0, 360), Math.random2f(0, 1), Math.random2f(0, 1)) => vec3 start_color; // @(Math.random2(0, 360), Math.random2f(0, 1), Math.random2f(0, 1)) => vec3 end_color; // waterfall.set_colors(start_color, end_color); fun void doPan() { while( true ) { 1.0 - panner.last() => float temp; temp => p[0].pan; 1.0 - temp => p[1].pan; ms => now; } } fun void chord(int chord[], float vel) { for (int i; i < chord.cap(); i++) { Std.mtof(chord[i]) => r[i].freq; vel => r[i].noteOn; } } fun void rollChord(int chord[], float vel) { for (int i; i < chord.cap(); i++) { Std.mtof(chord[i]) => r[i].freq; vel => r[i].noteOn; Math.random2f(0.01,0.08)::second => now; } } /*----------------------------------------------------------------------------- AUDIO PROCESSING Reference: sndpeek.ck -----------------------------------------------------------------------------*/ float samples[WINDOW_SIZE]; complex response[WINDOW_SIZE]; vec3 time_positions[WINDOW_SIZE]; // SEE NARRATIVE SECTION FOR fft & accum initialization. // Set size of flip WINDOW_SIZE => accum.size; // Set window type and size Windowing.hann(WINDOW_SIZE) => fft.window; // Set FFT size (will automatically zero pad) WINDOW_SIZE*2 => fft.size; // Get a reference for our window for visual tapering of the waveform Windowing.hann(WINDOW_SIZE) @=> float window[]; fun void do_audio() { while (true) { // Upchuck to process accum accum.upchuck(); accum.output( samples ); fft.upchuck(); fft.spectrum ( response ); WINDOW_SIZE::samp/2 => now; } } spork ~ do_audio(); /*----------------------------------------------------------------------------- LEAVES GGEN Used to show the time domain waveform in a radial manner. Frames the spectral representation in the polar flower form. -----------------------------------------------------------------------------*/ class Leaves extends GGen { GLines leaves[NUM_LEAVES]; @(0.4, 0.4, 0.4) => vec3 color; // Position radially 0 => int i; for ( GLines leaf : leaves ) { leaf --> this; leaf.mat().color( color ); 2 * Math.PI * (i$float / NUM_LEAVES$float) => float angle; leaf.rotZ( -angle ); // Rotation function is a bit wonky. leaf.posX( 2.75*Math.cos(angle) ); leaf.posY (2.75 * Math.sin(angle) ); i++; } fun void latest( vec3 positions[] ) { for ( GLines leaf : leaves ) { leaf.geo().positions(positions); } } } fun void time_waveform( float in[], vec3 out[] ) { /* Map audio samples into time domain waveform representation. */ if( in.size() != out.size() ) { <<< "size mismatch in flatten()", "" >>>; return; } // mapping to xyz coordinate int i; 0.5 => float width; for( auto s : in ) { // map -width/2 + width/WINDOW_SIZE*i => out[i].x; s * 2 * window[i] => out[i].y; i * 0.0005 => out[i].z; // increment i++; } } /*----------------------------------------------------------------------------- WATERFALL GGEN Used to show the spectral domain waveform in polar graphs with history. -----------------------------------------------------------------------------*/ class Waterfall extends GGen { 0 => int playhead; // Waterfall playhead. GPoints wfl[WATERFALL_DEPTH]; // Storing history. vec3 positions[MAX_ITERS]; // Temp storage. for ( GPoints w : wfl ) { w --> this; w.mat().color( @(1.0, 1.0, 1.0) ); } fun void latest(complex response[], int graph_choice, float r_scale, float c) { /* Graph polar function given spectrum. Args: samples: FFT samples. out: Array to store positions r_scale: Scale factor for the radius; used to show history c: Coefficient driving the animation */ for (int i; i < MAX_ITERS; i++) { i * (THETA_MAX - THETA_MIN) / MAX_ITERS => float theta; polar_flower(graph_choice, theta, r_scale, c) => float r; r * Math.sin(theta) => float x; r * Math.cos(theta) => float y; // Get wrapped angle so theta is in range [-PI. PI]. Math.atan2(y, x) => float wrapped_theta; if (show_point(wrapped_theta, response, 0.1)) { x => positions[i].x; y => positions[i].y; 0 => positions[i].z; } else { 0 => positions[i].x; 0 => positions[i].y; 0 => positions[i].z; } } wfl[playhead].geo().positions(positions); playhead++; WATERFALL_DEPTH %=> playhead; } @(0, 0.7, 1.0) => vec3 start; @(300, 0.7, 0.5) => vec3 end; fun void reset_colors() { // By default, rainbow spectrum. @(0, 0.7, 1.0) => start; @(300, 0.7, 0.5) => end; } fun void set_colors(vec3 s, vec3 e) { s => start; e => end; } fun void update(float dt) { playhead => int pos; for ( int i; i < wfl.size(); i++ ) { // Start w/ playhead - 1 and go backwards pos--; if ( pos < 0 ) WATERFALL_DEPTH - 1 => pos; // Offset Z (update: float division! it's a lot more responsive and smooth ) wfl[pos].posZ( -i/5.0 ); // Color based on position in history. color_lerp(start, end, i/wfl.size()$float) => vec3 color; wfl[pos].mat().color(hsv_to_rgb(color.x, color.y, color.z)); } } } fun float polar_flower(int graph_choice, float theta, float r_scale, float c) { /* Polar function for different flowers (3 choices). Args: theta: Calculate the radius for the given theta r_scale: Scale factor for the radius; used to show history c: Coefficient to drive animation Returns: Calculated radius */ [ // Default flower, animated! r_scale * Math.cos(c * theta), // Another flower, also animated! r_scale * Math.cos(c * theta) + Math.sin(Math.PI * theta / 4.0), // And another flower, (animated) r_scale / 22.5 * Math.sin(c * theta) * theta, // Discrete Flower, (animated, but hard to see) r_scale / 4.0 * (Math.sin(3 * theta / 4.0 + ((1 + c) * Math.PI/6)) - Math.acos(Math.cos(6 * theta))), // Circle (no animation) r_scale ] @=> float options[]; return options[graph_choice]; } 5 => int MAX_CHOICES; fun int show_point(float theta, complex response[], float threshold) { // Determine whether to show the point at theta given the response bucket result. Math.floor(WINDOW_SIZE * theta / (2 * Math.PI))$int => int bucket; return 5 * Math.sqrt((response[bucket]$polar).mag * 10) > threshold; } 1 => int DIRECTION; 7.0 => float MAX_C; 1.5 => float MIN_C; fun float update_coefficient(float c, float amplitude) { /* Update animation coefficient by incrementing or decrementing based on constants defined above. Args: c: Current value of c Returns: Updated c value */ if (c >= MAX_C) { -1 => DIRECTION; } if (c <= MIN_C) { 1 => DIRECTION; } return c + DIRECTION * amplitude; } /*----------------------------------------------------------------------------- UTILITY FUNCTIONS -----------------------------------------------------------------------------*/ fun vec3 hsv_to_rgb(float h, float s, float v) { /* HSV to RGB converter. Thanks @tae kyu for the link to the calcs! https://www.rapidtables.com/convert/color/hsv-to-rgb.html Args: h: Hue [0, 360] s: Saturation [0, 1] v: Value [0, 1] Returns: vec3 for RGB color */ v * s => float c; (h / 60) % 2 - 1 => float temp; if (temp < 0) { -temp => temp; } c * (1 - temp) => float x; v - c => float m; vec3 rgb_prime; if (0 <= h && h < 60) { @(c, x, 0.0) => rgb_prime; } else if (60 <= h && h < 120) { @(x, c, 0.0) => rgb_prime; } else if (120 <= h && h < 180) { @(0.0, c, x) => rgb_prime; } else if (180 <= h && h < 240) { @(0.0, x, c) => rgb_prime; } else if (240 <= h && h < 300) { @(x, 0.0, c) => rgb_prime; } else { @(c, 0.0, x) => rgb_prime; } vec3 rgb; (rgb_prime.x + m) => rgb.x; (rgb_prime.y + m) => rgb.y; (rgb_prime.z + m) => rgb.z; return rgb; } fun vec3 color_lerp(vec3 c0, vec3 c1, float percentage) { return (1.0 - percentage) * c0 + (percentage) * c1; } fun float max_value(float arr[]) { -10000.0 => float max; for ( auto s : arr ) { Math.max(max, s) => max; } return max; } /*----------------------------------------------------------------------------- UI! -----------------------------------------------------------------------------*/ UI_Window w; UI_Button button; button.text("Start narrative..."); UI_SliderInt graph_slider; graph_slider.text("Change the flower!"); graph_slider.range(1, MAX_CHOICES); UI_Dropdown dropdown; dropdown.text("Select color mode:"); dropdown.options(["Rainbow", "Black+White", "Random"]); w.add(button); w.add(graph_slider); w.add(dropdown); fun void ButtonListener(UI_Button @ button) { while (true) { button => now; if (IN_NARRATIVE == 0) { <<< "Narrative playing" >>>; narrative(); } } } spork ~ ButtonListener(button); fun void ISliderListener(UI_SliderInt @ islider) { while (true) { islider => now; <<< "Showing graph #" + islider.val() >>>; islider.val() - 1 => GRAPH_CHOICE; } } spork ~ ISliderListener(graph_slider); fun void DropdownListener(UI_Dropdown @ dropdown) { while (true) { dropdown => now; if (IN_NARRATIVE == 0) { dropdown.val() => int val; if (val == 0) { <<< "Rainbow color palette!" >>>; waterfall.reset_colors(); } else if (val == 1) { <<< "Black + White color palette!" >>>; waterfall.set_colors(@(0, 0, 0.8), @(0, 0, 0)); } else if (val == 2) { <<< "Random color palette!" >>>; @(Math.random2(0, 360), Math.random2f(0, 1), Math.random2f(0, 1)) => vec3 start_color; @(Math.random2(0, 360), Math.random2f(0, 1), Math.random2f(0, 1)) => vec3 end_color; <<< start_color, end_color >>>; waterfall.set_colors(start_color, end_color); } } } } spork ~ DropdownListener(dropdown); /*----------------------------------------------------------------------------- VISUALIZER, GRAPHICS LOOP -----------------------------------------------------------------------------*/ 2.5 => float r_scale; MIN_C => float c; while (true) { time_waveform(samples, time_positions); leaves.latest(time_positions); max_value( samples ) / 50.0 => float amplitude; waterfall.latest(response, GRAPH_CHOICE, r_scale, c); update_coefficient(c, amplitude) => c; GG.nextFrame() => now; } /*----------------------------------------------------------------------------- MOUSE INPUT Doesn't work with trackpad input, so I've removed this. Keeping it in comments for the sake of future work. -----------------------------------------------------------------------------*/ // Mouse mouse; // // open mouse device 0 // spork ~ mouse.start(0); // for ( int i : [0, 1, 2]) { // spork ~ wait_on_button(i); // } // fun void wait_on_button(int which) { // if( which < 0 || which > mouse.mouseDownEvents.size() ) // { // <<< "cannot wait on button with index", which >>>; // return; // } // // loop // while( true ) // { // // wait on a particular mouse button is hit // mouse.mouseDownEvents[which] => now; // // print // if (which == 0) { // Iterate through graph choices. // (GRAPH_CHOICE + 1) % MAX_CHOICES => GRAPH_CHOICE; // <<< "Showing graph", GRAPH_CHOICE >>>; // } else if (which == 1) { // if (IN_NARRATIVE == 0) { // <<< "Narrative playing" >>>; // narrative(); // } // } // } // }