Audio Flow

Using audio input to control particles in a flow field.

Audio Flow
0:00
/

Using audio input to control particles in a flow field. Loosely based on this nannou example.

use nannou::noise::*;
use std::sync::mpsc;
use nannou::prelude::*;
use nannou_audio as audio;
use nannou_audio::Buffer;

fn main() {
    nannou::app(model)
        .update(update)
        .run();
}

struct InputModel {
    tx: mpsc::Sender<f32>,
}

struct Particle {
    pos: Vec2,
    vel: Vec2,
}

impl Particle {
    fn new(x: f32, y: f32) -> Particle {
        Particle {
            pos: vec2(x, y),
            vel: vec2(0.0, 0.0),
        }
    }

    fn update(&mut self, dir: Vec2, max: f32) {
        self.pos += self.vel;
        self.vel += dir / 50.0 * (max * 100.0);
        self.vel *= 0.95;
    }
}

struct Model {
    scale: u32,
    cols: u32,
    rows: u32,
    noise: Perlin,
    particles: Vec<Particle>,
    vectors: Vec<Vec2>,
    max: f32,
    rx: mpsc::Receiver<f32>,
    in_stream: audio::Stream<InputModel>,
}

fn model(app: &App) -> Model {
    // Create a new window! Store the ID so we can refer to it later.
    let width = 200;
    let height = 200;
    let scale = 10;
    app.new_window()
        .size(width, height)
        .view(view)
        .build()
        .unwrap();
    let win_r = app.main_window().rect();
    let cols = width / scale;
    let rows = height / scale;
    let mut noise = Perlin::new();
    noise = noise.set_seed(1);
    let mut vectors = vec![];
    for _i in 0..(rows * cols) {
        vectors.push(vec2(0.1, 0.1));
    }
    let mut particles = vec![];
    for _i in 0..500 {
        let x = map_range(random(), 0.0, 1.0, win_r.left(), win_r.right());
        let y = map_range(random(), 0.0, 1.0, win_r.bottom(), win_r.top());
        particles.push(Particle::new(x, y));
    }

    // Initialise the audio host so we can spawn an audio stream.
    let audio_host = audio::Host::new();

    let (tx, rx) = mpsc::channel();

    // Create input model and input stream using that model
    let in_model = InputModel { tx };
    let in_stream = audio_host
        .new_input_stream(in_model)
        .capture(pass_in)
        .build()
        .unwrap();

    in_stream.play().unwrap();

    Model {
        scale,
        cols,
        rows,
        noise,
        particles,
        vectors,
        max: 0.0,
        rx,
        in_stream,
    }
}

fn pass_in(model: &mut InputModel, buffer: &Buffer) {
    for frame in buffer.frames() {
        for sample in frame {
            if *sample > 0.0 {
                model.tx.send(*sample).unwrap();
            }
        }
    }
}

fn update(app: &App, model: &mut Model, _update:Update) {
    let win = app.main_window();
    let win_r = win.rect();
    let width = win_r.right() - win_r.left();
    let height = win_r.top() - win_r.bottom();
    let mut yoff = 0.0;
    let zoff = app.time / 5.0;

    model.max *= 0.9;

    for x in model.rx.try_iter() {
        if x > model.max {
            model.max = x;
        }
    }

    for col in 0..model.cols {
        let mut xoff = 0.0;
        for row in 0..model.rows {
            let random = model.noise.get([xoff as f64, yoff as f64, zoff as f64]);
            let angle = map_range(random, 0.0, 1.0, 0.0, 360.0).to_radians();
            model.vectors[(row * model.cols + col) as usize] = vec2(0.0, 1.0).rotate(angle as f32);
            xoff += 0.1;
        }
        yoff += 0.1;
    }

    for particle in &mut model.particles {
        if particle.pos[0] <= win_r.left() {
            particle.pos[0] = win_r.right() - 1.0;
        } else if particle.pos[0] >= win_r.right() {
            particle.pos[0] = win_r.left();
        }
        if particle.pos[1] <= win_r.bottom() {
            particle.pos[1] = win_r.top() - 1.0;
        } else if particle.pos[1] >= win_r.top() {
            particle.pos[1] = win_r.bottom();
        }

        let mapped_x = map_range(particle.pos[0], win_r.left(), win_r.right(), 0.0, width);
        let mapped_y = map_range(particle.pos[1], win_r.bottom(), win_r.top(), 0.0, height);
        let row = (mapped_y / model.scale as f32).floor();
        let col = (mapped_x / model.scale as f32).floor();
        let vec = model.vectors[(row * model.cols as f32 + col) as usize];
        particle.update(vec, model.max);
    }
}

// Draw the state of your `Model` into the given `Frame` here.
fn view(app: &App, model: &Model, frame: Frame) {
    let win = app.main_window();
    let win_r = win.rect();
    let draw = app.draw();
    let width = win_r.right() - win_r.left();
    let height = win_r.top() - win_r.bottom();
    let line_length = width / model.cols as f32;

    if frame.nth() == 0 {
        draw.background().color(BLACK);
    }
    
    draw.rect()
        .w_h(width, height)
        .color(rgba(0.0, 0.0, 0.0, 0.05));

    for col in 0..model.cols {
        for row in 0..model.rows {
            let vec = model.vectors[(row * model.cols + col) as usize];
            let start_x = map_range(col as f32, 0.0, model.cols as f32, win_r.left(), win_r.right());
            let start_y = map_range(row as f32, 0.0, model.rows as f32, win_r.bottom(), win_r.top());
            let start = pt2(start_x, start_y);
            let end = start + pt2(line_length as f32 * vec.angle().cos(), line_length as f32 * vec.angle().sin());
            draw.line()
                .start(start)
                .end(end)
                .weight(1.0)
                .color(get_color(model.max));
        }
    }

    for particle in &model.particles {
        draw.ellipse()
            .xy(particle.pos)
            .w_h(3.0, 3.0)
            .color(get_color(model.max * 10.0));
    }

    draw.to_frame(app, &frame).unwrap();
    
    if frame.nth() < 300 {
        let file_path = captured_frame_path(app, &frame);
        app.main_window().capture_frame(file_path);
    }
}

fn get_color(n: f32) -> Rgba {
    let colors = [
        rgba(0.28, 0.20, 0.12, 1.0),
        rgba(0.42, 0.42, 0.05, 1.0),
        rgba(0.68, 0.24, 0.22, 1.0),
        rgba(0.89, 0.59, 0.37, 1.0),
        rgba(0.79, 0.83, 0.65, 1.0),
        rgba(0.87, 0.85, 0.81, 1.0),
    ];
    let idx = map_range(n, 0.0, 1.0, 0.0, colors.len() as f32).floor() as usize;
    let clamped_idx = clamp(idx, 0, colors.len() - 1);
    return colors[clamped_idx];
}

fn captured_frame_path(app: &App, frame: &Frame) -> std::path::PathBuf {
    app.project_path()
        .expect("failed to locate `project_path`")
        .join("frames")
        .join(format!("{:04}", frame.nth()))
        .with_extension("png")
}