Include my code
This commit is contained in:
parent
b81bf4cb13
commit
acd0fd1b25
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
example.wav
|
||||
.vscode/
|
25
Cargo.lock
generated
Normal file
25
Cargo.lock
generated
Normal file
@ -0,0 +1,25 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "lansky"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wav",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "riff"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1"
|
||||
|
||||
[[package]]
|
||||
name = "wav"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a65e199c799848b4f997072aa4d673c034f80f40191f97fe2f0a23f410be1609"
|
||||
dependencies = [
|
||||
"riff",
|
||||
]
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "lansky"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
wav = "1.0.0"
|
77
asm_notes.txt
Normal file
77
asm_notes.txt
Normal file
@ -0,0 +1,77 @@
|
||||
NOTES: Slightly out of date -- I haven't added the Imm mov mode or the ix register yet
|
||||
|
||||
Registers:
|
||||
$a0..9: Corresponds to input 0 through 9
|
||||
$l0..9: "Locals" 0..9
|
||||
$z0..9: Corresponds to output 0 through 9
|
||||
$r0..255: Auxiliary uint64_ts
|
||||
|
||||
$ix: Index base
|
||||
|
||||
Channels:
|
||||
A channel is a vector of samples with a known "formal range" --
|
||||
this formal range includes its start (relative to the whole song)
|
||||
and its basic length.
|
||||
|
||||
You can use outputs that aren't exposed as part of your device.
|
||||
This can be a good place to hide wavetables and stuff.
|
||||
They just won't be returned.
|
||||
|
||||
Memory model:
|
||||
$a0..9, $l0..9, and $z0..9 correspond to arrays
|
||||
When you read $a0, it reads channel a0 at $ix+a0.formal_start+a0.loop_start
|
||||
When you write $z0, it writes channel z0 at $ix+z0.formal_start+z0.loop_start
|
||||
|
||||
Unknown positions are 0.
|
||||
|
||||
A channel has an informal_start and an informal_length which initially match
|
||||
formal_start and formal_length.
|
||||
|
||||
Writing to a position outside of informal_start..informal_start+informal_length
|
||||
extends the bounds.
|
||||
|
||||
In looped mode, then the ultimate indices are wrapped to
|
||||
(formal_start+loop_start..formal_start+loop_start+loop_length)
|
||||
and all other behavior remains the same as above.
|
||||
|
||||
Instructions:
|
||||
mov|add|xor ($l0..9, $z0..9, $r0..255), ($a0..9, $l0..9, $z0..9, $r0..255)
|
||||
Copy value, add value, or xor value.
|
||||
|
||||
range ($a0..9, $l0..9, $z0..9), loop_start, loop_length
|
||||
Start and length are immediate values.
|
||||
If length is nonzero, the channel is in "looped" mode.
|
||||
|
||||
inp ($a0..9, $l0..9, $z0..9)
|
||||
Push a copy of the channel to the stack for "invoke".
|
||||
The channel will be sliced:
|
||||
- if loop_start is zero and loop_length is zero:
|
||||
The formal range runs from formal_start to formal_start+formal_length.
|
||||
The informal range runs from informal_start to informal_start+informal_length.
|
||||
- if loop_start is nonzero and loop_length is zero:
|
||||
The formal range runs from formal_start+loop_start to formal_start+formal_length
|
||||
The informal range runs from formal_start+loop_start to informal_start+informal_length
|
||||
- if loop_length is nonzero:
|
||||
The formal range runs from formal_start+loop_start to formal_start+loop_start+loop_length
|
||||
The informal range is the same as the formal range.
|
||||
|
||||
dup
|
||||
Duplicate an item from the invoke stack.
|
||||
|
||||
oup (mov|add|xor) ($l0..9, $z0..9)
|
||||
Pop an output from the invoke stack and write it to an output channel or local.
|
||||
|
||||
All indexes from informal_start to informal_start+informal_length are written.
|
||||
|
||||
The instruction used to write can be mov, add, or xor
|
||||
|
||||
invoke device, n_inputs, n_outputs
|
||||
Invoke the device by name (an immediate string)
|
||||
Asserts that the device takes n_inputs inputs (an immediate integer) and n_outputs outputs (an immediate integer)
|
||||
Asserts that there are actually n_inputs inputs on the stack.
|
||||
After invoke, there will be n_outputs outputs on the stack.
|
||||
They will be ordered such that the first call to oup produces the first output, the second produces the second,
|
||||
and so on
|
||||
|
||||
exit
|
||||
Exit the current procedure.
|
131
src/audio.rs
Normal file
131
src/audio.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use std::ops::{Range, Index, IndexMut};
|
||||
|
||||
// Fixed point: 0xFFFF_FFFF.FFFF_FFFF
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Sample(pub i64);
|
||||
|
||||
impl Sample {
|
||||
pub fn to_i16_sample(&self) -> i16 {
|
||||
// interpret self as fixed-point
|
||||
if self.0 > i32::MAX as i64 { return i16::MAX }
|
||||
if self.0 < i32::MIN as i64 { return i16::MIN }
|
||||
let low_part = self.0 as i32;
|
||||
return (low_part >> 16) as i16
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Sample {
|
||||
fn default() -> Self { BLANK_SAMPLE }
|
||||
}
|
||||
|
||||
const BLANK_SAMPLE: Sample = Sample(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Channel {
|
||||
formal: Range<i32>,
|
||||
|
||||
minus: Vec<Sample>,
|
||||
plus: Vec<Sample>,
|
||||
}
|
||||
|
||||
pub struct Bounds {
|
||||
pub formal: Range<i32>,
|
||||
pub informal: Range<i32>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(formal: Range<i32>) -> Self {
|
||||
assert!(formal.end >= formal.start);
|
||||
Channel {
|
||||
formal: formal,
|
||||
minus: vec![],
|
||||
plus: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formal(&self) -> Range<i32> {
|
||||
self.formal.clone()
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Bounds {
|
||||
let formal = self.formal.clone();
|
||||
let informal_start = formal.start - self.minus.len() as i32;
|
||||
let informal_len_0 = self.minus.len() + self.plus.len();
|
||||
let informal_len_1 = (formal.end - informal_start) as usize;
|
||||
let informal_len = informal_len_0.max(informal_len_1);
|
||||
|
||||
return Bounds {
|
||||
formal,
|
||||
informal: informal_start..(informal_start + informal_len as i32)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn blank() -> Channel {
|
||||
return Channel::new(0..0);
|
||||
}
|
||||
|
||||
pub fn slice(&self, start: i32, length: i32) -> Channel {
|
||||
if start == 0 && length == 0 {
|
||||
return self.clone()
|
||||
}
|
||||
if start != 0 && length == 0 {
|
||||
let formal_start = self.formal.start + start;
|
||||
let mut formal_end = self.formal.start + self.formal.end;
|
||||
if formal_end < formal_start { formal_end = formal_start; }
|
||||
|
||||
let mut chan = Channel::new(formal_start..formal_end);
|
||||
for i in 0..self.plus.len() {
|
||||
chan[self.formal.start + i as i32] = self[self.formal.start + i as i32]
|
||||
}
|
||||
return chan
|
||||
}
|
||||
|
||||
let formal_start = self.formal.start + start;
|
||||
let formal_end = self.formal.start + start + length;
|
||||
let mut chan = Channel::new(formal_start..formal_end);
|
||||
for i in formal_start..formal_end {
|
||||
chan[i] = self[i];
|
||||
}
|
||||
return chan
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<i32> for Channel {
|
||||
type Output = Sample;
|
||||
|
||||
fn index(&self, index: i32) -> &Self::Output {
|
||||
let adj_idx = index - self.formal.start;
|
||||
if adj_idx < 0 {
|
||||
let neg_adj_idx = (-adj_idx) as usize - 1;
|
||||
self.minus.get(neg_adj_idx).unwrap_or(&BLANK_SAMPLE)
|
||||
} else {
|
||||
let pos_adj_idx = adj_idx as usize;
|
||||
self.plus.get(pos_adj_idx).unwrap_or(&BLANK_SAMPLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<i32> for Channel {
|
||||
fn index_mut(&mut self, index: i32) -> &mut Self::Output {
|
||||
let adj_idx = index - self.formal.start;
|
||||
if adj_idx < 0 {
|
||||
let neg_adj_idx = (-adj_idx) as usize - 1;
|
||||
if self.minus.len() < neg_adj_idx + 1 {
|
||||
self.minus.reserve(neg_adj_idx + 1);
|
||||
while self.minus.len() < neg_adj_idx + 1 {
|
||||
self.minus.push(BLANK_SAMPLE);
|
||||
}
|
||||
}
|
||||
return &mut self.minus[neg_adj_idx]
|
||||
} else {
|
||||
let pos_adj_idx = adj_idx as usize;
|
||||
if self.plus.len() < pos_adj_idx + 1{
|
||||
self.plus.reserve(pos_adj_idx + 1);
|
||||
while self.plus.len() < pos_adj_idx + 1 {
|
||||
self.plus.push(BLANK_SAMPLE);
|
||||
}
|
||||
}
|
||||
return &mut self.plus[pos_adj_idx]
|
||||
}
|
||||
}
|
||||
}
|
122
src/device.rs
Normal file
122
src/device.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use std::{collections::HashMap, cell::Cell};
|
||||
|
||||
use crate::{audio::Channel, error::{Lansky, LanskyError}, vm::{Procedure, Block}, rack::Rack};
|
||||
|
||||
pub struct Device {
|
||||
proto: Proto,
|
||||
implementation: Implementation,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Proto {
|
||||
pub name: String,
|
||||
pub inputs: Vec<String>,
|
||||
pub outputs: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
enum Implementation {
|
||||
Native(fn(Vec<Channel>) -> Lansky<Vec<Channel>>),
|
||||
Hosted(Procedure),
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn native(
|
||||
name: String,
|
||||
inputs: Vec<String>,
|
||||
outputs: Vec<String>,
|
||||
native: fn(Vec<Channel>) -> Lansky<Vec<Channel>>
|
||||
) -> Lansky<Self> {
|
||||
let proto = Proto { name, inputs, outputs };
|
||||
Self::new(proto, Implementation::Native(native))
|
||||
}
|
||||
|
||||
pub fn hosted(
|
||||
name: String,
|
||||
inputs: Vec<String>,
|
||||
outputs: Vec<String>,
|
||||
blocks: Vec<Block>,
|
||||
) -> Lansky<Self> {
|
||||
let proto = Proto { name, inputs, outputs };
|
||||
Self::new(proto.clone(), Implementation::Hosted(Procedure::new(proto, blocks)))
|
||||
}
|
||||
|
||||
fn new(proto: Proto, implementation: Implementation) -> Lansky<Self> {
|
||||
for i0 in 0..proto.inputs.len() {
|
||||
for i1 in i0+1..proto.inputs.len() {
|
||||
if proto.inputs[i0] == proto.inputs[i1] {
|
||||
return Err(LanskyError::DuplicateInput { name: proto.inputs[i0].clone(), i0, i1 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i0 in 0..proto.outputs.len() {
|
||||
for i1 in i0+1..proto.outputs.len() {
|
||||
if proto.outputs[i0] == proto.outputs[i1] {
|
||||
return Err(LanskyError::DuplicateOutput { name: proto.outputs[i0].clone(), i0, i1 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Validate the code in some way?
|
||||
// Not needed for native devices
|
||||
|
||||
Ok(Device { proto, implementation })
|
||||
}
|
||||
|
||||
pub(crate) fn analyze(&self, rack: &Rack) -> Lansky<()> {
|
||||
match &self.implementation {
|
||||
Implementation::Native(_) => {}
|
||||
Implementation::Hosted(h) => {
|
||||
h.analyze(rack)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_proto(&self) -> &Proto {
|
||||
&self.proto
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_named(&self, rack: &Rack, mut inputs: HashMap<String, Channel>) -> Lansky<HashMap<String, Channel>> {
|
||||
let expected = self.proto.inputs.len();
|
||||
let got = inputs.len();
|
||||
if expected != got { return Err(LanskyError::WrongNInputs { expected, got }) }
|
||||
|
||||
let mut inputs_2 = Vec::with_capacity(self.proto.inputs.len());
|
||||
for i in self.proto.inputs.iter() {
|
||||
let Some(channel_in) = inputs.remove(i) else {
|
||||
return Err(LanskyError::MissingInput { name: i.clone() })
|
||||
};
|
||||
inputs_2.push(channel_in)
|
||||
}
|
||||
for i in inputs.keys() {
|
||||
if !self.proto.inputs.contains(i) {
|
||||
return Err(LanskyError::UnknownInput { name: i.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
let outputs = self.invoke_positional(rack, inputs_2)?;
|
||||
|
||||
let mut outputs_2 = HashMap::new();
|
||||
for (k, v) in self.proto.outputs.iter().zip(outputs.into_iter()) {
|
||||
outputs_2.insert(k.clone(), v);
|
||||
}
|
||||
Ok(outputs_2)
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_positional(&self, rack: &Rack, inputs: Vec<Channel>) -> Lansky<Vec<Channel>> {
|
||||
let expected = self.proto.inputs.len();
|
||||
let got = inputs.len();
|
||||
if expected != got { return Err(LanskyError::WrongNInputs { expected, got }) }
|
||||
let outputs = match &self.implementation {
|
||||
Implementation::Native(f) => f(inputs)?,
|
||||
Implementation::Hosted(procedure) => procedure.invoke(rack, inputs)?,
|
||||
};
|
||||
|
||||
let expected = self.proto.outputs.len();
|
||||
let got = outputs.len();
|
||||
if expected != got { return Err(LanskyError::WrongNOutputs { expected, got }) }
|
||||
Ok(outputs)
|
||||
}
|
||||
}
|
38
src/error.rs
Normal file
38
src/error.rs
Normal file
@ -0,0 +1,38 @@
|
||||
pub type Lansky<T> = Result<T, LanskyError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LanskyError {
|
||||
DuplicateDevice { name: String },
|
||||
UnknownDevice { name: String },
|
||||
|
||||
DuplicateInput { name: String, i0: usize, i1: usize },
|
||||
DuplicateOutput { name: String, i0: usize, i1: usize },
|
||||
|
||||
MissingInput { name: String },
|
||||
UnknownInput { name: String },
|
||||
|
||||
WrongNInputs { expected: usize, got: usize },
|
||||
WrongNOutputs { expected: usize, got: usize },
|
||||
|
||||
NoFirstBlock,
|
||||
|
||||
WrongNStack { why: LanskyStackReason, expected: usize, got: usize },
|
||||
|
||||
MissingRegA { i: usize },
|
||||
MissingRegZ { i: usize },
|
||||
|
||||
WrongNChannels { expected: usize, got: usize },
|
||||
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LanskyStackReason {
|
||||
CannotDupEmptyStack,
|
||||
CannotOupEmptyStack,
|
||||
CannotPopEmptyStack,
|
||||
|
||||
BlockMustEndWithEmptyStack,
|
||||
|
||||
InvokeNInputs
|
||||
}
|
98
src/main.rs
Normal file
98
src/main.rs
Normal file
@ -0,0 +1,98 @@
|
||||
// TODOs:
|
||||
// Generate at 88200Hz, downsample to 44100Hz
|
||||
// Parser for assembly language
|
||||
// More context in error messages
|
||||
// Replace the sample type with something more Pico 8 fixedpoint-like (right now we are always falling down on the i64 interface)
|
||||
// Use the _top_ part when coercing the sample type to an i32
|
||||
// jmp and similar instructions
|
||||
// Use primarily the named interface and allow defaulting
|
||||
// Channel-like objects that produce a constant value
|
||||
|
||||
use std::{path::Path, fs::File};
|
||||
|
||||
use audio::{Sample, Channel};
|
||||
use device::Device;
|
||||
use error::{Lansky, LanskyError};
|
||||
use rack::Rack;
|
||||
use vm::{Block, BodyInstruction, ExitInstruction, MovSrc, OupDst, RegZ};
|
||||
|
||||
mod audio;
|
||||
mod error;
|
||||
mod device;
|
||||
mod standard_devices;
|
||||
mod rack;
|
||||
mod vm;
|
||||
|
||||
fn main() {
|
||||
main_err().expect("crash:");
|
||||
}
|
||||
|
||||
|
||||
fn main_err() -> Lansky<()> {
|
||||
let mut rack = Rack::new();
|
||||
|
||||
rack.add_device(Device::hosted(
|
||||
"main".to_string(),
|
||||
vec![],
|
||||
vec!["audio".to_string()],
|
||||
vec![
|
||||
Block {
|
||||
name: "entry".to_string(),
|
||||
body: vec![
|
||||
BodyInstruction::New(MovSrc::Imm(Sample(0)), MovSrc::Imm(Sample(100000)), MovSrc::Imm(Sample(440))),
|
||||
BodyInstruction::Invoke("gensin".to_string(), 1, 1),
|
||||
BodyInstruction::Pop(OupDst::Z(RegZ(0))),
|
||||
],
|
||||
exit: ExitInstruction::Exit,
|
||||
}
|
||||
],
|
||||
)?)?;
|
||||
|
||||
rack.analyze_all()?;
|
||||
|
||||
let result =
|
||||
rack.invoke_positional("main", vec![])?;
|
||||
|
||||
export(Path::new("example.wav"), result)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export(path: &Path, audio: Vec<Channel>) -> Lansky<()> {
|
||||
match &audio[..] {
|
||||
[mono] => {
|
||||
let header = wav::Header::new(
|
||||
wav::header::WAV_FORMAT_PCM,
|
||||
1,
|
||||
44100,
|
||||
16
|
||||
);
|
||||
|
||||
let mut out_file = File::create(path).map_err(|e| LanskyError::IO(e))?;
|
||||
let data = channel_to_data(&mono);
|
||||
wav::write(header, &data, &mut out_file).map_err(|e| LanskyError::IO(e))?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
_ => {
|
||||
return Err(LanskyError::WrongNChannels { expected: 1, got: audio.len() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_to_data(channel: &Channel) -> wav::BitDepth {
|
||||
let mut v: Vec<i16> = vec![];
|
||||
for i in channel.bounds().informal {
|
||||
v.push(channel[i].to_i16_sample())
|
||||
}
|
||||
wav::BitDepth::Sixteen(v)
|
||||
}
|
||||
|
||||
/*
|
||||
fn main_err() -> std::io::Result<()> {
|
||||
let mut out_file = File::create(Path::new("output.wav"))?;
|
||||
wav::write(header, &data, &mut out_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
*/
|
56
src/rack.rs
Normal file
56
src/rack.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{device::{Device, Proto}, error::{Lansky, LanskyError}, standard_devices, audio::Channel};
|
||||
|
||||
pub struct Rack {
|
||||
devices: HashMap<String, Device>
|
||||
}
|
||||
|
||||
impl Rack {
|
||||
pub fn new() -> Self {
|
||||
let mut r = Rack {
|
||||
devices: HashMap::new()
|
||||
};
|
||||
standard_devices::augment(&mut r);
|
||||
r
|
||||
}
|
||||
|
||||
pub fn analyze_all(&self) -> Lansky<()> {
|
||||
for d in self.devices.values() {
|
||||
d.analyze(self)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_device(&mut self, device: Device) -> Lansky<()> {
|
||||
if self.devices.contains_key(&device.get_proto().name) {
|
||||
return Err(LanskyError::DuplicateDevice { name: device.get_proto().name.clone() })
|
||||
}
|
||||
|
||||
let name = device.get_proto().name.clone();
|
||||
self.devices.insert(name, device);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_device(&self, name: &str) -> Lansky<&Device> {
|
||||
let Some(device) = self.devices.get(name) else {
|
||||
return Err(LanskyError::UnknownDevice { name: name.to_string() })
|
||||
};
|
||||
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
pub fn invoke_named(&self, name: &str, inputs: HashMap<String, Channel>) -> Lansky<HashMap<String, Channel>> {
|
||||
self.get_device(name)?.invoke_named(self, inputs)
|
||||
}
|
||||
|
||||
pub fn invoke_positional(&self, name: &str, inputs: Vec<Channel>) -> Lansky<Vec<Channel>> {
|
||||
self.get_device(name)?.invoke_positional(self, inputs)
|
||||
}
|
||||
|
||||
pub fn get_proto(&self, name: &str) -> Lansky<&Proto> {
|
||||
Ok(self.get_device(name)?.get_proto())
|
||||
}
|
||||
|
||||
}
|
33
src/standard_devices/mod.rs
Normal file
33
src/standard_devices/mod.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use crate::{rack::Rack, device::Device, audio::{Channel, Sample}, error::Lansky};
|
||||
|
||||
pub fn augment(r: &mut Rack) -> Lansky<()> {
|
||||
r.add_device(Device::native(
|
||||
"gensin".to_string(),
|
||||
vec!["frequency".to_string()],
|
||||
vec!["signal".to_string()],
|
||||
gensin
|
||||
)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gensin(mut chans: Vec<Channel>) -> Lansky<Vec<Channel>> {
|
||||
assert!(chans.len() == 1);
|
||||
let inp = chans.pop().expect("must be one channel");
|
||||
|
||||
let mut pos = 0.0;
|
||||
let sr = 44100.0;
|
||||
|
||||
let mut oup = Channel::new(inp.formal());
|
||||
|
||||
for i in inp.bounds().informal {
|
||||
let freq = inp[i].0 as f32;
|
||||
pos += freq/sr;
|
||||
pos %= 1.0;
|
||||
oup[i] = Sample(((pos * PI * 2.0).sin() * i32::MAX as f32) as i64);
|
||||
}
|
||||
|
||||
Ok(vec![oup])
|
||||
}
|
519
src/vm.rs
Normal file
519
src/vm.rs
Normal file
@ -0,0 +1,519 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::{audio::{Channel, Sample}, error::{Lansky, LanskyError, LanskyStackReason}, device::Proto, rack::Rack};
|
||||
|
||||
pub(crate) struct Procedure {
|
||||
proto: Proto,
|
||||
blocks: Vec<Block>,
|
||||
analysis: Cell<Option<Analysis>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Analysis {
|
||||
stack_depth: usize,
|
||||
|
||||
proto_n_inputs: usize,
|
||||
proto_n_outputs: usize,
|
||||
|
||||
n_reg_ls: usize,
|
||||
}
|
||||
|
||||
pub struct Block {
|
||||
pub name: String,
|
||||
pub body: Vec<BodyInstruction>,
|
||||
pub exit: ExitInstruction
|
||||
}
|
||||
|
||||
pub enum BodyInstruction {
|
||||
Mov(MovType, MovDst, MovSrc),
|
||||
Range(RangeDst, i32, i32),
|
||||
Inp(InpSrc),
|
||||
Dup(),
|
||||
Oup(MovType, OupDst),
|
||||
Pop(OupDst),
|
||||
New(MovSrc, MovSrc, MovSrc),
|
||||
Invoke(String, usize, usize),
|
||||
}
|
||||
|
||||
pub enum ExitInstruction {
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub enum MovType { Mov, Add, Xor }
|
||||
|
||||
#[derive(Clone, Copy)] pub enum MovDst { L(RegL), R(RegR), Z(RegZ), Ix }
|
||||
#[derive(Clone, Copy)] pub enum MovSrc { A(RegA), L(RegL), R(RegR), Z(RegZ), Ix, Imm(Sample) }
|
||||
#[derive(Clone, Copy)] pub enum RangeDst { A(RegA), L(RegL), Z(RegZ) }
|
||||
#[derive(Clone, Copy)] pub enum InpSrc { A(RegA), L(RegL), Z(RegZ) }
|
||||
#[derive(Clone, Copy)] pub enum OupDst { L(RegL), Z(RegZ) }
|
||||
|
||||
impl OupDst {
|
||||
fn to_mov_dst(self) -> MovDst {
|
||||
match self {
|
||||
Self::L(l) => MovDst::L(l),
|
||||
Self::Z(z) => MovDst::Z(z),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)] pub struct RegA(pub u8);
|
||||
#[derive(Clone, Copy)] pub struct RegL(pub u8);
|
||||
#[derive(Clone, Copy)] pub struct RegR(pub u8);
|
||||
#[derive(Clone, Copy)] pub struct RegZ(pub u8);
|
||||
|
||||
struct Scope {
|
||||
ix: Sample,
|
||||
|
||||
reg_as: Vec<Channel>,
|
||||
reg_ls: Vec<Channel>,
|
||||
reg_rs: [Sample; 256],
|
||||
reg_zs: Vec<Channel>,
|
||||
|
||||
loop_as: [LoopRange; 256],
|
||||
loop_ls: [LoopRange; 256],
|
||||
loop_zs: [LoopRange; 256],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct LoopRange {
|
||||
start: i32,
|
||||
length: i32,
|
||||
}
|
||||
|
||||
impl Default for LoopRange {
|
||||
fn default() -> Self {
|
||||
Self { start: 0, length: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
fn new(analysis: Analysis, inputs: Vec<Channel>) -> Scope {
|
||||
assert_eq!(inputs.len(), analysis.proto_n_inputs); // analysis is done elsewhere
|
||||
|
||||
let reg_as: Vec<Channel> = inputs;
|
||||
let mut reg_ls: Vec<Channel> = vec![];
|
||||
let reg_rs: [Sample; 256] = [Sample::default(); 256];
|
||||
let mut reg_zs: Vec<Channel> = vec![];
|
||||
|
||||
for _ in 0..analysis.n_reg_ls { reg_ls.push(Channel::blank()); }
|
||||
for _ in 0..analysis.proto_n_outputs { reg_zs.push(Channel::blank()); }
|
||||
|
||||
Scope {
|
||||
ix: Sample(0),
|
||||
|
||||
reg_as,
|
||||
reg_ls,
|
||||
reg_rs,
|
||||
reg_zs,
|
||||
|
||||
loop_as: [LoopRange::default(); 256],
|
||||
loop_ls: [LoopRange::default(); 256],
|
||||
loop_zs: [LoopRange::default(); 256],
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(self) -> Vec<Channel> {
|
||||
self.reg_zs
|
||||
}
|
||||
|
||||
fn read(&self, src: MovSrc) -> Sample {
|
||||
let flat_ix = self.ix.0 as i32;
|
||||
let (chan, loop_) = match src {
|
||||
MovSrc::A(RegA(a)) => {
|
||||
let a = a as usize;
|
||||
(&self.reg_as[a], &self.loop_as[a])
|
||||
}
|
||||
MovSrc::L(RegL(l)) => {
|
||||
let l = l as usize;
|
||||
(&self.reg_ls[l], &self.loop_ls[l])
|
||||
}
|
||||
MovSrc::R(RegR(r)) => {
|
||||
let r = r as usize;
|
||||
return self.reg_rs[r]
|
||||
}
|
||||
MovSrc::Z(RegZ(z)) => {
|
||||
let z = z as usize;
|
||||
(&self.reg_zs[z], &self.loop_zs[z])
|
||||
}
|
||||
MovSrc::Ix => return self.ix,
|
||||
MovSrc::Imm(i) => return i,
|
||||
};
|
||||
|
||||
let mut ix = flat_ix;
|
||||
if loop_.length != 0 { ix %= loop_.length; }
|
||||
ix += chan.formal().start + loop_.start;
|
||||
|
||||
chan[ix]
|
||||
}
|
||||
|
||||
fn write(&mut self, ty: &MovType, dst: MovDst, value: Sample, offset: i32) {
|
||||
let flat_ix = self.ix.0 as i32;
|
||||
let (chan, loop_) = match dst {
|
||||
MovDst::L(RegL(l)) => {
|
||||
let l = l as usize;
|
||||
(&mut self.reg_ls[l], &self.loop_ls[l])
|
||||
}
|
||||
MovDst::R(RegR(r)) => {
|
||||
let r = r as usize;
|
||||
self.reg_rs[r] = value; return
|
||||
}
|
||||
MovDst::Z(RegZ(z)) => {
|
||||
let z = z as usize;
|
||||
(&mut self.reg_zs[z], &self.loop_zs[z])
|
||||
}
|
||||
MovDst::Ix => { self.ix = value; return }
|
||||
};
|
||||
|
||||
let mut ix = flat_ix + offset;
|
||||
if loop_.length != 0 { ix %= loop_.length; }
|
||||
ix += chan.formal().start + loop_.start;
|
||||
|
||||
chan[ix] = value
|
||||
}
|
||||
|
||||
fn replace(&mut self, dst: OupDst, value: Channel) {
|
||||
match dst {
|
||||
OupDst::L(RegL(l)) => {
|
||||
self.reg_ls[l as usize] = value;
|
||||
self.loop_ls[l as usize] = LoopRange::default()
|
||||
}
|
||||
OupDst::Z(RegZ(z)) => {
|
||||
self.reg_zs[z as usize] = value;
|
||||
self.loop_zs[z as usize] = LoopRange::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_range(&mut self, dst: RangeDst, start: i32, length: i32) {
|
||||
let lr = LoopRange{start, length};
|
||||
match dst {
|
||||
RangeDst::A(RegA(a)) => {
|
||||
self.loop_as[a as usize] = lr
|
||||
}
|
||||
RangeDst::L(RegL(l)) => {
|
||||
self.loop_ls[l as usize] = lr
|
||||
}
|
||||
RangeDst::Z(RegZ(z)) => {
|
||||
self.loop_zs[z as usize] = lr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inp(&self, src: InpSrc) -> Channel {
|
||||
let (chan, loop_) = match src {
|
||||
InpSrc::A(RegA(a)) => {
|
||||
let a = a as usize;
|
||||
(&self.reg_as[a], &self.loop_as[a])
|
||||
}
|
||||
InpSrc::L(RegL(l)) => {
|
||||
let l = l as usize;
|
||||
(&self.reg_ls[l], &self.loop_ls[l])
|
||||
}
|
||||
InpSrc::Z(RegZ(z)) => {
|
||||
let z = z as usize;
|
||||
(&self.reg_zs[z], &self.loop_zs[z])
|
||||
}
|
||||
};
|
||||
chan.slice(loop_.start, loop_.length)
|
||||
}
|
||||
}
|
||||
|
||||
impl Procedure {
|
||||
pub(crate) fn new(proto: Proto, blocks: Vec<Block>) -> Procedure {
|
||||
Procedure {
|
||||
proto,
|
||||
blocks,
|
||||
analysis: Cell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invoke(&self, rack: &Rack, inputs: Vec<Channel>) -> Lansky<Vec<Channel>> {
|
||||
let analysis = self.internal_analyze(rack)?;
|
||||
|
||||
let mut scope = Scope::new(analysis, inputs);
|
||||
let mut stack: Vec<Channel> = vec![];
|
||||
|
||||
let block_ix = 0;
|
||||
loop {
|
||||
let block = &self.blocks[block_ix];
|
||||
for instruction in block.body.iter() {
|
||||
match instruction {
|
||||
BodyInstruction::Mov(ty, dst, src) => {
|
||||
scope.write(ty, *dst, scope.read(*src), 0)
|
||||
}
|
||||
BodyInstruction::Range(dst, start, len) => {
|
||||
scope.apply_range(*dst, *start, *len)
|
||||
}
|
||||
BodyInstruction::Inp(src) => {
|
||||
let slice = scope.inp(*src);
|
||||
stack.push(slice)
|
||||
}
|
||||
BodyInstruction::Dup() => {
|
||||
let top = stack.pop().expect("stack can't be empty");
|
||||
stack.push(top.clone())
|
||||
}
|
||||
BodyInstruction::Oup(ty, dst) => {
|
||||
let top = stack.pop().expect("stack can't be empty");
|
||||
let bounds = top.bounds();
|
||||
for ix in bounds.informal {
|
||||
let offset = ix - bounds.formal.start;
|
||||
scope.write(ty, dst.to_mov_dst(), top[ix], offset)
|
||||
}
|
||||
}
|
||||
BodyInstruction::Pop(dst) => {
|
||||
let top = stack.pop().expect("stack can't be empty");
|
||||
scope.replace(*dst, top)
|
||||
}
|
||||
BodyInstruction::New(formal_start, formal_len, fill) => {
|
||||
let start = scope.read(*formal_start);
|
||||
let len = scope.read(*formal_len);
|
||||
let fill = scope.read(*fill);
|
||||
let mut chan = Channel::new((start.0 as i32)..((start.0+len.0) as i32));
|
||||
for i in chan.formal() {
|
||||
chan[i] = fill
|
||||
}
|
||||
stack.push(chan)
|
||||
}
|
||||
BodyInstruction::Invoke(device, n_inputs, n_outputs) => {
|
||||
let mut inputs: Vec<Channel> = vec![];
|
||||
std::mem::swap(&mut stack, &mut inputs);
|
||||
assert_eq!(inputs.len(), *n_inputs);
|
||||
let outputs = rack.invoke_positional(device, inputs)?;
|
||||
assert_eq!(outputs.len(), *n_outputs);
|
||||
stack.extend(outputs);
|
||||
stack.reverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match block.exit {
|
||||
ExitInstruction::Exit => return Ok(scope.exit())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn analyze(&self, rack: &Rack) -> Lansky<()> {
|
||||
self.internal_analyze(rack)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn internal_analyze(&self, rack: &Rack) -> Lansky<Analysis> {
|
||||
if let Some(x) = self.analysis.get() { return Ok(x) }
|
||||
|
||||
let mut analysis = Analysis {
|
||||
stack_depth: 0,
|
||||
|
||||
proto_n_inputs: self.proto.inputs.len(),
|
||||
proto_n_outputs: self.proto.outputs.len(),
|
||||
|
||||
n_reg_ls: 0
|
||||
};
|
||||
|
||||
if self.blocks.len() == 0 {
|
||||
return Err(LanskyError::NoFirstBlock);
|
||||
}
|
||||
|
||||
for block in self.blocks.iter() {
|
||||
block.touch_analysis(
|
||||
&mut analysis, &|d| rack.get_proto(d).cloned()
|
||||
)?;
|
||||
}
|
||||
|
||||
self.analysis.set(Some(analysis));
|
||||
|
||||
return Ok(analysis)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Block {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis, get_proto: &impl Fn(&str) -> Lansky<Proto>) -> Lansky<()> {
|
||||
for instruction in self.body.iter() {
|
||||
instruction.touch_analysis(analysis, get_proto)?;
|
||||
}
|
||||
|
||||
if analysis.stack_depth != 0 {
|
||||
return Err(LanskyError::WrongNStack {
|
||||
why: LanskyStackReason::BlockMustEndWithEmptyStack,
|
||||
got: analysis.stack_depth, expected: 0
|
||||
})
|
||||
}
|
||||
self.exit.touch_analysis(analysis)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BodyInstruction {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis, get_proto: &impl Fn(&str) -> Lansky<Proto>) -> Lansky<()> {
|
||||
match self {
|
||||
BodyInstruction::Mov(_, dst, src) => {
|
||||
dst.touch_analysis(analysis)?;
|
||||
src.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::Range(dst, _, _) => {
|
||||
dst.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::Inp(src) => {
|
||||
analysis.stack_depth += 1;
|
||||
src.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::Dup() => {
|
||||
if analysis.stack_depth == 0 {
|
||||
return Err(LanskyError::WrongNStack {
|
||||
why: LanskyStackReason::CannotDupEmptyStack,
|
||||
expected: 1, got: 0
|
||||
})
|
||||
}
|
||||
analysis.stack_depth += 1;
|
||||
}
|
||||
BodyInstruction::Oup(_, dst) => {
|
||||
if analysis.stack_depth == 0 {
|
||||
return Err(LanskyError::WrongNStack {
|
||||
why: LanskyStackReason::CannotOupEmptyStack,
|
||||
expected: 1, got: 0
|
||||
})
|
||||
}
|
||||
analysis.stack_depth -= 1;
|
||||
|
||||
dst.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::Pop(dst) => {
|
||||
if analysis.stack_depth == 0 {
|
||||
return Err(LanskyError::WrongNStack {
|
||||
why: LanskyStackReason::CannotPopEmptyStack,
|
||||
expected: 1, got: 0
|
||||
})
|
||||
}
|
||||
analysis.stack_depth -= 1;
|
||||
|
||||
dst.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::New(formal_start, formal_len, fill) => {
|
||||
analysis.stack_depth += 1;
|
||||
|
||||
formal_start.touch_analysis(analysis)?;
|
||||
formal_len.touch_analysis(analysis)?;
|
||||
fill.touch_analysis(analysis)?;
|
||||
}
|
||||
BodyInstruction::Invoke(device, n_inputs, n_outputs) => {
|
||||
let proto = get_proto(device)?;
|
||||
|
||||
let actual_n_inputs = proto.inputs.len();
|
||||
let actual_n_outputs = proto.outputs.len();
|
||||
|
||||
if *n_inputs != actual_n_inputs {
|
||||
return Err(LanskyError::WrongNInputs { expected: actual_n_inputs, got: *n_inputs })
|
||||
}
|
||||
|
||||
if *n_outputs != actual_n_outputs {
|
||||
return Err(LanskyError::WrongNInputs { expected: actual_n_inputs, got: *n_outputs })
|
||||
}
|
||||
|
||||
if analysis.stack_depth != *n_inputs {
|
||||
return Err(LanskyError::WrongNStack {
|
||||
why: LanskyStackReason::InvokeNInputs,
|
||||
expected: *n_inputs,
|
||||
got: analysis.stack_depth,
|
||||
})
|
||||
}
|
||||
analysis.stack_depth = *n_outputs;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ExitInstruction {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
ExitInstruction::Exit => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MovDst {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
Self::L(l) => l.touch_analysis(analysis),
|
||||
Self::R(r) => r.touch_analysis(analysis),
|
||||
Self::Z(z) => z.touch_analysis(analysis),
|
||||
Self::Ix => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MovSrc {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
Self::A(a) => a.touch_analysis(analysis),
|
||||
Self::L(l) => l.touch_analysis(analysis),
|
||||
Self::R(r) => r.touch_analysis(analysis),
|
||||
Self::Z(z) => z.touch_analysis(analysis),
|
||||
Self::Ix => Ok(()),
|
||||
Self::Imm(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RangeDst {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
Self::A(a) => a.touch_analysis(analysis),
|
||||
Self::L(l) => l.touch_analysis(analysis),
|
||||
Self::Z(z) => z.touch_analysis(analysis),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InpSrc {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
Self::A(a) => a.touch_analysis(analysis),
|
||||
Self::L(l) => l.touch_analysis(analysis),
|
||||
Self::Z(z) => z.touch_analysis(analysis),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OupDst {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
match self {
|
||||
Self::L(l) => l.touch_analysis(analysis),
|
||||
Self::Z(z) => z.touch_analysis(analysis),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl RegA {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
if self.0 as usize >= analysis.proto_n_inputs {
|
||||
return Err(LanskyError::MissingRegA { i: self.0 as usize });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegL {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
analysis.n_reg_ls = analysis.n_reg_ls.max(self.0 as usize);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegR {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegZ {
|
||||
fn touch_analysis(&self, analysis: &mut Analysis) -> Lansky<()> {
|
||||
if self.0 as usize >= analysis.proto_n_outputs {
|
||||
return Err(LanskyError::MissingRegZ { i: self.0 as usize });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user