use std::sync::Arc;
use sim_cookbook::frame_digest;
use sim_kernel::{
AbiVersion, Args, Callable, ClassRef, Cx, Error, Export, Expr, Lib, LibManifest, LibTarget,
Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
};
use crate::wire::encode_channel;
use crate::{Channel, ChannelMessage, U7};
pub fn manifest_name() -> Symbol {
Symbol::qualified("midi", "digest")
}
fn chord_digest_symbol() -> Symbol {
Symbol::qualified("midi", "chord-digest")
}
fn chord_digest(root: u8) -> String {
let mut bytes = Vec::new();
for key in [root, root.wrapping_add(4), root.wrapping_add(7)] {
let (status, data) = encode_channel(&ChannelMessage::NoteOn {
ch: Channel(0),
key: U7(key & 0x7f),
vel: U7(100),
});
bytes.push(status);
bytes.extend(data);
}
frame_digest(&bytes)
}
fn root_arg(cx: &mut Cx, value: &Value) -> Result<u8> {
let text = match value.object().as_expr(cx)? {
Expr::String(text) => text,
_ => {
return Err(Error::Eval(
"midi/chord-digest expects a string root".to_owned(),
));
}
};
text.trim().parse::<u8>().map_err(|_| {
Error::Eval(format!(
"midi/chord-digest root must be 0-255, got {text:?}"
))
})
}
struct ChordDigestOp;
impl Object for ChordDigestOp {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok("#<function midi/chord-digest>".to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for ChordDigestOp {
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
cx.resolve_class(&Symbol::qualified("core", "Function"))
}
fn as_callable(&self) -> Option<&dyn Callable> {
Some(self)
}
}
impl Callable for ChordDigestOp {
fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
let args = args.into_vec();
let [root] = args.as_slice() else {
return Err(Error::Eval(format!(
"midi/chord-digest expects one root-note argument, got {}",
args.len()
)));
};
let root = root_arg(cx, root)?;
cx.factory().string(chord_digest(root))
}
}
pub struct MidiDigestLib;
impl Lib for MidiDigestLib {
fn manifest(&self) -> LibManifest {
LibManifest {
id: manifest_name(),
version: Version(env!("CARGO_PKG_VERSION").to_owned()),
abi: AbiVersion { major: 0, minor: 1 },
target: LibTarget::HostRegistered,
requires: Vec::new(),
capabilities: Vec::new(),
exports: vec![Export::Function {
symbol: chord_digest_symbol(),
function_id: None,
}],
}
}
fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
linker.function_value(
chord_digest_symbol(),
cx.factory().opaque(Arc::new(ChordDigestOp))?,
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chord_digest_is_a_deterministic_frame() {
let digest = chord_digest(60);
assert!(digest.starts_with("(frame (bytes 9) (hash "));
assert_eq!(digest, chord_digest(60));
}
}