initial commit
This commit is contained in:
commit
c7a9c60ee6
12
Cargo.toml
Normal file
12
Cargo.toml
Normal 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
281
src/main.rs
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user