Audio Flow
Using audio input to control particles in a flow field.
![Audio Flow](/content/images/size/w2000/2024/02/Screen-Shot-2024-02-04-at-12.36.48-PM.png)
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")
}