initial commit

This commit is contained in:
Marc Planard 2024-08-29 20:45:13 +02:00
commit c7a9c60ee6
2 changed files with 293 additions and 0 deletions

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "lj_dac_emulator"
version = "0.1.0"
edition = "2021"
[dependencies]
ether-dream-dac-emulator = "0.3.0"
futures = "0.3"
piper = "0.1"
smol = "0.1"
nannou = "0.14"

281
src/main.rs Normal file
View File

@ -0,0 +1,281 @@
//! A simple nannou app for visualising the output data produced by the Ether Dream DAC emulator.
//!
//! In this example we:
//!
//! 1. Create the default DAC emulator.
//! 2. Spawn the broadcaster on its own thread so that it sends UDP broadcasts once per second.
//! 3. Spawn the listener on its own thread so that it may listen for stream connection requests.
//! 4. Loop at 60 FPS (nannou's default app loop).
//! 5. On each loop, check whether or not a new stream has been established.
//! 6. If we have a stream, check for the latest frame points.
//! 7. In our `view` function, draw the laser frame to the bounds of the window.
use ether_dream_dac_emulator::ether_dream::protocol::DacPoint;
use ether_dream_dac_emulator::Description;
use futures::prelude::*;
use nannou::prelude::*;
use std::sync::mpsc;
use std::time::Duration;
use std::u16;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
active_stream: Option<std::net::SocketAddr>,
perceived_points: Vec<(std::time::Instant, DacPoint)>,
event_rx: mpsc::Receiver<Event>,
underflowing: bool,
buffer_status: BufferStatus,
}
#[derive(Debug)]
struct BufferStatus {
size: u16,
len: u16,
}
#[derive(Debug)]
enum Event {
/// The DAC connected to the given address.
Connected(std::net::SocketAddr),
/// Some points were emitted by the DAC.
Points(std::time::Instant, BufferStatus, Vec<DacPoint>),
/// The DAC's point buffer underflowed.
Underflowed,
/// The DAC disconnected from the stream.
Disconnected,
}
fn model(app: &App) -> Model {
app.new_window().view(view).build().unwrap();
// A descriptor of the DAC. Change this to change behaviour, MAC address, etc.
let dac_description = Default::default();
println!("Dac description: {dac_description:?}");
// Kick off the `smol` runtime with 4 threads.
for _ in 0..4 {
std::thread::spawn(|| smol::run(future::pending::<()>()));
}
// Run the DAC emulator on its own thread.
let (event_tx, event_rx) = mpsc::sync_channel(1_024);
std::thread::spawn(move || smol::block_on(run_dac(dac_description, event_tx)));
let active_stream = None;
let perceived_points = Vec::new();
let underflowing = false;
Model {
active_stream,
event_rx,
perceived_points,
underflowing,
buffer_status: BufferStatus {
size: u16::MAX,
len: 0,
},
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
/*
if app.elapsed_frames() % 60 != 0 {
std::thread::sleep(Duration::from_millis(10));
return;
}
*/
// Process all events.
for event in model.event_rx.try_iter() {
match event {
Event::Underflowed => model.underflowing = true,
Event::Connected(addr) => model.active_stream = Some(addr),
Event::Disconnected => model.active_stream = None,
Event::Points(when, buffer_status, pts) => {
model.underflowing = false;
let new_pts = pts.into_iter().map(|pt| (when, pt));
model.perceived_points.extend(new_pts);
model.buffer_status = buffer_status;
}
}
}
// Drain the old points.
let now = std::time::Instant::now();
let too_old = model
.perceived_points
.iter()
.position(|&(t, _)| match t < now {
true if now.duration_since(t) > PERSISTENCE_OF_VISION => false,
_ => true,
})
.unwrap_or(model.perceived_points.len());
model.perceived_points.drain(0..too_old);
}
// Draw the state of your `Model` into the given `Frame` here.
fn view(app: &App, model: &Model, frame: Frame) {
if app.elapsed_frames() % 10 != 0 {
std::thread::sleep(Duration::from_millis(1));
return;
}
// Begin drawing
let draw = app.draw();
let win = app.window_rect();
// Clear the background to blue.
draw.background().color(BLACK);
// Draw the status in the bottom left.
let (status_string, color) = match model.active_stream {
None => {
let s = format!("Awaiting connection...");
let color = WHITE;
(s, color)
}
Some(addr) => {
let (buffer, color) = match model.underflowing {
false => (format!(""), GREEN),
true => (format!(" (underflowing!)"), RED),
};
let s = format!("Connected{}: {}", buffer, addr);
(s, color)
}
};
let text_area = win.pad(20.0);
draw.text(&status_string)
.wh(text_area.wh())
.left_justify()
.align_text_bottom()
.font_size(18)
.color(color);
// Draw the path.
let t_x = |xi: i16| (xi as f32 / std::i16::MAX as f32) * win.w() * 0.5;
let t_y = |yi: i16| (yi as f32 / std::i16::MAX as f32) * win.h() * 0.5;
let t_color = |color: u16| color as f32 / std::u16::MAX as f32;
let convert_point = |pt: &DacPoint, lum: f32| -> (Point2, LinSrgb) {
let x = t_x(pt.x);
let y = t_y(pt.y);
let r = t_color(pt.r) * lum;
let g = t_color(pt.g) * lum;
let b = t_color(pt.b) * lum;
(pt2(x, y), lin_srgb(r, g, b))
};
let color_blend = wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::SrcAlpha,
//dst_factor: BlendFactor::OneMinusSrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
};
let alpha_blend = wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
};
let draw2 = draw.color_blend(color_blend).alpha_blend(alpha_blend);
//let draw2 = draw.color_blend(BLEND_ADD);
let now = std::time::Instant::now();
let mut last_pt: Option<(Point2, LinSrgb)> = None;
for &(inst, pt) in model.perceived_points.iter() {
let luminance = match inst < now {
true => persistence_of_vision(now.duration_since(inst)),
false => 1.0,
};
let a = convert_point(&pt, luminance * 0.25);
if let Some(b) = last_pt {
if !is_black(&a.1) || !is_black(&b.1) {
let weight = 5.0 * luminance;
if a.0 == b.0 {
let rgb = lightest(&a.1, &b.1);
draw2.ellipse().xy(a.0).color(rgb).radius(weight * 0.5);
} else {
let pts = [a, b];
draw2
.polyline()
.weight(weight)
.caps_round()
.points_colored(pts.iter().cloned());
}
}
}
last_pt = Some(a);
}
let w1 = 400.0;
let w2 = w1 * model.buffer_status.len as f32 / model.buffer_status.size as f32;
let draw = draw.x_y(-win.w() / 2.0 + w1 / 2.0 + 5.0, win.h() / 2.0 - 10.0);
draw.rect().color(BLUE).x((-w1 + w2) / 2.0).w(w2).h(10.0);
draw.rect()
.no_fill()
.stroke(BLUE)
.stroke_weight(1.0)
.w(400.0)
.h(10.0);
draw.to_frame(app, &frame).unwrap();
}
async fn run_dac(desc: Description, event_tx: mpsc::SyncSender<Event>) -> std::io::Result<()> {
let (mut broadcaster, mut listener) = ether_dream_dac_emulator::new(desc)?;
let msgs = ether_dream_dac_emulator::broadcaster::one_hz_send();
let broadcasting = broadcaster.run(msgs.boxed());
let listening = async move {
loop {
let (stream, addr) = match listener.accept().await {
Err(err) => break Err(err),
Ok(stream) => stream,
};
if event_tx.send(Event::Connected(addr)).is_err() {
break Ok(());
}
while let Ok(points_result) = stream.next_points().await {
let dac = stream.dac();
let buffer_status = BufferStatus {
size: dac.buffer_capacity,
len: dac.status.buffer_fullness,
};
let event = match points_result {
Err(_) => Event::Underflowed,
Ok(points) => Event::Points(std::time::Instant::now(), buffer_status, points),
};
event_tx.try_send(event).ok();
}
if event_tx.send(Event::Disconnected).is_err() {
break Ok(());
}
}
};
let (b_res, l_res) = future::join(broadcasting, listening).await;
b_res?;
l_res?;
Ok(())
}
const PERSISTENCE_OF_VISION: Duration = Duration::from_millis(250);
// Produce a colour that is the lightest channels of the two given colours.
fn lightest(a: &LinSrgb, b: &LinSrgb) -> LinSrgb {
let r = a.red.max(b.red);
let g = a.green.max(b.green);
let b = a.blue.max(b.blue);
lin_srgb(r, g, b)
}
fn is_black(c: &LinSrgb) -> bool {
c.red == 0.0 && c.green == 0.0 && c.blue == 0.0
}
// Given some duration since a point was emitted, produce a multiplier for its perceived
// luminosity.
fn persistence_of_vision(duration: Duration) -> f32 {
let secs = duration.as_secs_f32();
let max = PERSISTENCE_OF_VISION.as_secs_f32();
let pow = 4.0; // Found via tweaking.
(1.0 - (secs / max)).max(0.0).powf(pow)
}