something?

This commit is contained in:
mcneb10 2024-08-14 18:29:07 -05:00
commit 4578f50eed
9 changed files with 6073 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.vscode

5299
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "patchwire"
version = "0.1.0"
edition = "2021"
#build = "build.rs"
[dependencies]
anyhow = "1.0.86"
env_logger = "0.11.5"
log = "0.4.22"
rodio = "0.19.0"
slint = "1.7.1"
#[dependencies.steel-core]
#git="https://github.com/mattwparas/steel.git"
#branch = "master"
# TODO: figure out if these should be used
#features = [ "anyhow", "jit", "modules" ]
[build-dependencies]
slint-build = "1.7.1"

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# patchwire
DSP/Music creation stack based on node architecture using slint and custom written bytecode vm for dsp code.
TODO: make this make sense lmao

3
build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
slint_build::compile("ui/window.slint").unwrap();
}

223
src/lisp/mod.rs Normal file
View file

@ -0,0 +1,223 @@
use std::time::Instant;
use anyhow::{bail, Context, Error};
pub mod templates;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LispVMData {
Sample(u32),
// TODO: MIDI Packet, lighting packet, etc
}
impl LispVMData {
pub fn try_get_sample(self) -> Result<u32, Error> {
match self {
LispVMData::Sample(x) => Ok(x),
_ => bail!(
"Lisp VM attempted to convert {:?} to Sample data, it is not",
self
),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpCode {
Add,
AddConst(u32),
Sub,
SubConst(u32),
Mul,
Div,
Jump(usize),
JumpIfZero(usize),
JumpIfEq(usize, LispVMData),
JumpIfStackSizeEq(usize, usize),
Input(LispVMData),
Output(LispVMData),
Pop,
Dup,
Brk,
MicrosElapsedModN(u32),
}
impl OpCode {
pub fn try_get_data(self) -> Result<LispVMData, Error> {
match self {
OpCode::Input(x) => Ok(x),
OpCode::Output(x) => Ok(x),
_ => bail!("Failed to coerce opcode {:?} to data", self),
}
}
pub fn try_get_sample(self) -> Result<u32, Error> {
self.try_get_data()
.context(format!("Failed to coerce opcode {:?} as sample data", self))?
.try_get_sample()
}
}
pub struct Script {
bytecode: Vec<OpCode>,
source: Option<String>,
}
impl Script {
pub fn from_lisp(text: String) -> Self {
let opcodes: Vec<OpCode> = vec![];
// TODO: actual parse that shit
Self {
bytecode: opcodes,
source: Some(text),
}
}
pub fn from_raw_bytecode(bytecode: Vec<OpCode>) -> Self {
Self {
bytecode,
source: None,
}
}
pub fn process(self, input: Vec<OpCode>, time: Instant) -> Result<Vec<OpCode>, Error> {
let mut stack: Vec<OpCode> = input;
let mut pc = 0;
let mut dont_increment_pc = false;
while pc < self.bytecode.len() {
match self.bytecode.get(pc).unwrap() {
OpCode::Add => {
let a = stack
.pop()
.context(format!("Stack underflow on add at {}", pc))?
.try_get_sample()?;
let b = stack
.pop()
.context(format!("Stack underflow on add at {}", pc))?
.try_get_sample()?;
stack.push(OpCode::Output(LispVMData::Sample(a + b)));
}
OpCode::AddConst(x) => {
let a = stack
.pop()
.context(format!("Stack underflow on add at {}", pc))?
.try_get_sample()?;
stack.push(OpCode::Output(LispVMData::Sample(a + x)));
}
OpCode::Sub => {
let b = stack
.pop()
.context(format!("Stack underflow on sub at {}", pc))?
.try_get_sample()?;
let a = stack
.pop()
.context(format!("Stack underflow on sub at {}", pc))?
.try_get_sample()?;
stack.push(OpCode::Output(LispVMData::Sample(a - b)));
}
OpCode::SubConst(x) => {
let a = stack
.pop()
.context(format!("Stack underflow on sub at {}", pc))?
.try_get_sample()?;
stack.push(OpCode::Output(LispVMData::Sample(a - x)));
}
OpCode::Mul => todo!(),
OpCode::Div => todo!(),
OpCode::Jump(x) => {
if *x >= self.bytecode.len() {
bail!(
"Jump to {} is out of bounds. Code is {} opcodes long",
*x,
self.bytecode.len()
);
} else {
pc = *x;
dont_increment_pc = true;
}
}
OpCode::JumpIfZero(x) => {
if stack
.last()
.context(format!("Stack underflow on jz at {}", pc))?
.try_get_sample()?
== 0
{
if *x >= self.bytecode.len() {
bail!(
"Jump to {} is out of bounds. Code is {} opcodes long",
*x,
self.bytecode.len()
);
} else {
pc = *x;
dont_increment_pc = true;
}
}
}
OpCode::JumpIfEq(x, y) => {
if stack
.last()
.context(format!("Stack underflow on jq at {}", pc))?
.try_get_data()?
== *y
{
if *x >= self.bytecode.len() {
bail!(
"Jump to {} is out of bounds. Code is {} bytes long",
*x,
self.bytecode.len()
);
} else {
pc = *x;
dont_increment_pc = true;
}
}
}
OpCode::JumpIfStackSizeEq(x, y) => {
if stack.len() == *y {
if *x >= self.bytecode.len() {
bail!(
"Jump to {} is out of bounds. Code is {} bytes long",
*x,
self.bytecode.len()
);
} else {
pc = *x;
dont_increment_pc = true;
}
}
}
OpCode::Input(x) => {
stack.push(OpCode::Output(*x));
}
OpCode::Output(x) => {
stack.push(OpCode::Output(*x));
}
OpCode::Pop => {
stack.pop();
}
OpCode::Dup => {
stack.push(
*stack
.last()
.context(format!("Stack underflow on add at dup {}", pc))?,
);
}
OpCode::Brk => {
return Ok(stack);
}
OpCode::MicrosElapsedModN(x) => {
stack.push(OpCode::Output(LispVMData::Sample(u32::try_from(time.elapsed().as_micros() % (*x as u128)).unwrap())));
},
}
if dont_increment_pc {
dont_increment_pc = false;
} else {
pc += 1;
}
}
Ok(stack)
}
}

71
src/lisp/templates.rs Normal file
View file

@ -0,0 +1,71 @@
use std::{collections::HashMap};
use slint::{ModelRc, VecModel};
use super::{OpCode, Script};
use crate::{Node, Port};
pub struct NodeTemplate {
pub name: String,
pub script: Script, // TODO: figure out what to do with this?
pub input_connections: usize,
pub output_connections: usize,
}
impl NodeTemplate {
pub fn to_slint_node(self) -> Node {
let mut in_port_vec = vec![];
let mut out_port_vec = vec![];
for i in 0..self.input_connections {
in_port_vec.push(Port {
name: format!("i{i}").into(),
..Default::default()
});
}
for j in 0..self.output_connections {
out_port_vec.push(Port {
name: format!("o{j}").into(),
..Default::default()
});
}
Node {
text: self.name.into(),
x: 0.0, //?
y: 0.0, //?
in_ports: ModelRc::new(VecModel::from(in_port_vec)),
out_ports: ModelRc::new(VecModel::from(out_port_vec)),
..Default::default()
}
}
}
pub fn make_mixer(input_count: usize) -> NodeTemplate {
NodeTemplate {
name: "Mixer".to_owned(),
script: Script::from_raw_bytecode(vec![
OpCode::Add,
OpCode::JumpIfStackSizeEq(3, 1),
OpCode::Jump(0),
OpCode::Brk,
]),
input_connections: input_count,
output_connections: 1,
}
}
pub fn make_splitter(output_count: usize) -> NodeTemplate {
NodeTemplate {
name: "Splitter".to_owned(),
script: Script::from_raw_bytecode(vec![
OpCode::Dup,
OpCode::JumpIfStackSizeEq(3, output_count),
OpCode::Jump(0),
OpCode::Brk,
]),
input_connections: 1,
output_connections: output_count,
}
}

197
src/main.rs Normal file
View file

@ -0,0 +1,197 @@
use core::fmt;
use std::{process::exit, time::Instant};
//use steel::steel_vm::engine::Engine;
use anyhow::{Context, Error, Result};
use lisp::{LispVMData, OpCode, Script};
use log::{debug, error, info};
use rodio::{
cpal::{self, traits::HostTrait}, Device, DeviceTrait, OutputStream, OutputStreamHandle
};
use slint::{Brush, Color, Model, ModelRc, VecModel};
pub mod lisp;
slint::include_modules!();
trait UnwrapToLog<T, E> {
fn unwrap_to_log(self) -> T
where
E: fmt::Debug;
}
impl<T, E> UnwrapToLog<T, E> for Result<T, E> {
fn unwrap_to_log(self) -> T
where
E: fmt::Debug,
{
match self {
Ok(x) => x,
Err(e) => {
error!("{e:?}");
exit(1);
}
}
}
}
trait RustMainWindowFunctions {
fn connect(&self, node1: i32, port1: i32, node2: i32, port2: i32, brush: Brush, toggle: bool);
fn add_connection(&self, connection: Connection, toggle: bool);
fn add_node(&self, node: Node);
}
impl RustMainWindowFunctions for MainWindow {
fn connect(&self, node1: i32, port1: i32, node2: i32, port2: i32, brush: Brush, toggle: bool) {
self.add_connection(
Connection {
node1,
port1,
node2,
port2,
color: brush,
},
toggle,
);
}
fn add_connection(&self, connection: Connection, toggle: bool) {
let default = &VecModel::default();
let connections = self.get_connections();
let connections_ref = connections.as_any().downcast_ref().unwrap_or(default);
let mut existing_connections: Vec<Connection> = connections_ref.iter().collect();
let duplicate_index = existing_connections.iter().position(|r: &Connection| {
r.node1 == connection.node1
&& r.node2 == connection.node2
&& r.port1 == connection.port1
&& r.port2 == connection.port2
});
match duplicate_index {
Some(x) => {
if toggle {
existing_connections.remove(x);
}
}
None => {
existing_connections.push(connection);
}
}
info!("{existing_connections:?}");
self.set_connections(ModelRc::new(VecModel::from(existing_connections)));
}
fn add_node(&self, node: Node) {
let default = &VecModel::default();
let nodes = self.get_nodes();
let nodes_ref = nodes.as_any().downcast_ref().unwrap_or(default);
let mut existing_nodes: Vec<Node> = nodes_ref.iter().collect();
existing_nodes.push(node);
self.set_nodes(ModelRc::new(VecModel::from(existing_nodes)));
}
}
fn open_window_and_run_event_loop(device: Device, stream: OutputStreamHandle) -> Result<(), Error> {
let main_window = MainWindow::new().with_context(|| "failed to create main window")?;
main_window.set_window_title(format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")).into());
let patchwire_start_time = Instant::now();
main_window.add_node(Node {
text: "Sine Wave".into(),
// TODO: Node placement algorithm
x: 0.0,
y: 50.0,
out_ports: ModelRc::new(VecModel::from(vec![
Port {
name: "a".into(),
..Default::default()
},
])),
..Default::default()
});
main_window.add_node(Node {
text: format!("Audio device {}", device.name().unwrap_or("default".to_owned())).into(),
x: 200.0,
y: 50.0,
in_ports: ModelRc::new(VecModel::from(vec![
Port {
name: "L".into(),
..Default::default()
},
Port {
name: "R".into(),
..Default::default()
},
])),
..Default::default()
});
main_window.connect(0, 0, 1, 0, Brush::SolidColor(Color::from_rgb_u8(255, 255, 0)), true);
main_window.connect(0, 0, 1, 1, Brush::SolidColor(Color::from_rgb_u8(255, 255, 0)), true);
let mwr = main_window.as_weak();
main_window.on_add_connection_helper(move |c: Connection| mwr.unwrap().add_connection(c, true));
main_window
.run()
.with_context(|| "error in main window event loop")
}
// TODO: remove
fn script_test() {
let script = Script::from_raw_bytecode(vec![
OpCode::Input(LispVMData::Sample(1)),
OpCode::Sub,
//OpCode::JumpIfZero(4),
OpCode::JumpIfEq(4, LispVMData::Sample(0)),
OpCode::Jump(0),
OpCode::Brk,
]);
let input = vec![
OpCode::Input(LispVMData::Sample(69)),
];
let now = Instant::now();
//
let result = script.process(input, now);
//
let elapsed = now.elapsed();
info!("Result: {:?}, Time: {:?}", result.unwrap_to_log(), elapsed);
}
fn main() {
env_logger::init();
//let mut steel_engine = Engine::new();
// TODO: other config reading and setup goes here
// TODO: allow user to select device
debug!("Opening audio output stream");
let default_device = cpal::default_host()
.default_output_device()
.with_context(|| "failed to find default audio device")
.unwrap_to_log();
info!("Found audio device with name: \"{}\"", default_device.name().unwrap_or("default".to_string()));
let (_, stream) = OutputStream::try_from_device(&default_device)
.with_context(|| "failed to open default audio device")
.unwrap_to_log();
script_test();
debug!("Opening main window");
open_window_and_run_event_loop(default_device, stream).unwrap_to_log();
}

252
ui/window.slint Normal file
View file

@ -0,0 +1,252 @@
import { ScrollView, HorizontalBox, Button } from "std-widgets.slint";
struct Port {
x: length,
y: length,
name: string,
}
struct Node {
x: length,
y: length,
z: float,
ports-width: [length],
text: string,
in-ports: [Port],
out-ports: [Port],
}
component Node inherits Rectangle {
in-out property <[Node]> nodes;
in property <int> idx;
private property <Node> node: nodes[idx];
callback port_clicked(int, bool);
x: node.x;
y: node.y;
width: self.preferred-width;
height: self.preferred-height;
drop-shadow-blur: 5px;
drop-shadow-color: black;
border-radius: 8px;
VerticalLayout {
spacing: -heading.border-width;
heading := Rectangle {
border-radius: root.border-radius;
border-bottom-right-radius: self.border-bottom-left-radius;
border-bottom-left-radius: {
if node.in-ports.length == 0 && node.out-ports.length == 0 {
root.border-radius
} else {
0px
}
};
background: #383838;
border-width: 1px;
border-color: white.with-alpha(0.15);
HorizontalLayout {
padding: 5px;
spacing: 10px;
Text {
color: white;
text: node.text;
}
}
}
Rectangle {
background: #303030;
border-width: heading.border-width;
border-color: heading.border-color;
border-bottom-right-radius: root.border-radius;
border-bottom-left-radius: self.border-bottom-right-radius;
HorizontalLayout {
for ports[idx] in [node.in-ports, node.out-ports]: ports-layout := VerticalLayout {
private property <bool> is-input: idx == 0;
alignment: start;
padding-top: 5px;
padding-bottom: 5px;
spacing: 5px;
for idx in ports.length: port-layout := HorizontalLayout {
alignment: is-input ? start : end;
x: 15px / 2 * (is-input ? -1 : 1);
Rectangle {
area := TouchArea {
clicked => {
root.port_clicked(idx, is-input);
}
}
border-radius: 3px;
background: #f8e45c;
min-width: 15px;
height: 20px;
drop-shadow-blur: 5px;
drop-shadow-color: black;
HorizontalLayout {
padding: 2px;
Text {
color: black;
horizontal-alignment: center;
text: ports[idx].name;
}
}
init => {
ports[idx].y = //
heading.preferred-height//
+ ports-layout.padding-top//
+ port-layout.preferred-height / 2//
+ (ports-layout.spacing + port-layout.preferred-height) * idx;
}
}
}
}
}
}
}
for idx in node.out-ports.length: Rectangle {
init => {
node.out-ports[idx].x = root.preferred-width;
}
}
TouchArea {
moved => {
if self.pressed {
nodes[idx].x += self.mouse-x - self.pressed-x;
nodes[idx].y += self.mouse-y - self.pressed-y;
}
}
mouse-cursor: move;
}
}
struct Link { x1: length, x2: length, y1: length, y2: length, color: brush, }
component Link inherits Path {
in property <Link> link;
viewbox-width: self.width / 1px;
viewbox-height: self.height / 1px;
stroke: link.color;
stroke-width: 5px;
MoveTo {
x: link.x1 / 1px;
y: link.y1 / 1px;
}
// LineTo {
// x: link.x2 / 1px;
// y: link.y2 / 1px;
// }
private property <int> h: link.y2 / 1px - link.y1 / 1px;
private property <int> w: link.x2 / 1px - link.x1 / 1px;
private property <{ x: int, y: int}> c1: {
x: max(abs(h / 2), abs(w / 2)),
y: max(h / 4, -w / 4),
};
private property <{ x: int, y: int}> c2: {
x: max(abs(h / 2), abs(w / 2)),
y: min(h / 4, w / 4 + abs(h)),
};
CubicTo {
x: link.x2 / 1px;
y: link.y2 / 1px;
control-1-x: link.x1 / 1px + c1.x;
control-1-y: link.y1 / 1px + c1.y;
control-2-x: link.x2 / 1px - c2.x;
control-2-y: link.y2 / 1px - c2.y;
}
}
struct Connection { node1: int, port1: int, node2: int, port2: int, color: brush}
export component MainWindow inherits Window {
background: #242424;
min-width: 800px;
min-height: 400px;
in-out property window_title <=> self.title;
in-out property <[Node]> nodes;
in-out property <[Connection]> connections;
property <[Link]> links;
property <Connection> PendingConnection: { node1: -1, port1: -1, node2: -1, port2: -1, color: #ffa500 };
callback add_connection_helper(Connection);
preferred-height: 500px;
preferred-width: 500px;
public pure function create-link-object(start: { node: int, port: int}, end: { node: int, port: int}, color: brush) -> Link {
{
x1: nodes[start.node].x + nodes[start.node].out-ports[start.port].x,
y1: nodes[start.node].y + nodes[start.node].out-ports[start.port].y,
x2: nodes[end.node].x + nodes[end.node].in-ports[end.port].x,
y2: nodes[end.node].y + nodes[end.node].in-ports[end.port].y,
color: color,
}
}
VerticalLayout {
ScrollView {
for c in connections: Link {
link: root.create-link-object({ node: c.node1, port: c.port1 }, { node: c.node2, port: c.port2 }, c.color);
}
for node[idx] in nodes: Node {
nodes <=> nodes;
idx: idx;
port_clicked(port, is-input) => {
debug("Node", idx, "Port", port, "Input", is-input);
if (root.PendingConnection.node1 == -1) {
if (!is-input) {
root.PendingConnection.node1 = idx;
root.PendingConnection.port1 = port;
root.PendingConnection.color = #ffa500;
} else {
root.PendingConnection.node1 = -1;
}
} else if (root.PendingConnection.node1 != idx) {
if (is-input) {
root.PendingConnection.node2 = idx;
root.PendingConnection.port2 = port;
// Use rust code to add the connection because we can't do it in pure slint
// Ugh
root.add_connection_helper(root.PendingConnection);
}
root.PendingConnection.node1 = -1;
}
}
}
}
Rectangle {
height: 10%;
}
}
}