sim_lib_midi_core/
frame_digest.rs1use std::sync::Arc;
11
12use sim_cookbook::frame_digest;
13use sim_kernel::{
14 AbiVersion, Args, Callable, ClassRef, Cx, Error, Export, Expr, Lib, LibManifest, LibTarget,
15 Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
16};
17
18use crate::wire::encode_channel;
19use crate::{Channel, ChannelMessage, U7};
20
21pub fn manifest_name() -> Symbol {
23 Symbol::qualified("midi", "digest")
24}
25
26fn chord_digest_symbol() -> Symbol {
28 Symbol::qualified("midi", "chord-digest")
29}
30
31fn chord_digest(root: u8) -> String {
36 let mut bytes = Vec::new();
37 for key in [root, root.wrapping_add(4), root.wrapping_add(7)] {
38 let (status, data) = encode_channel(&ChannelMessage::NoteOn {
39 ch: Channel(0),
40 key: U7(key & 0x7f),
41 vel: U7(100),
42 });
43 bytes.push(status);
44 bytes.extend(data);
45 }
46 frame_digest(&bytes)
47}
48
49fn root_arg(cx: &mut Cx, value: &Value) -> Result<u8> {
51 let text = match value.object().as_expr(cx)? {
52 Expr::String(text) => text,
53 _ => {
54 return Err(Error::Eval(
55 "midi/chord-digest expects a string root".to_owned(),
56 ));
57 }
58 };
59 text.trim().parse::<u8>().map_err(|_| {
60 Error::Eval(format!(
61 "midi/chord-digest root must be 0-255, got {text:?}"
62 ))
63 })
64}
65
66struct ChordDigestOp;
68
69impl Object for ChordDigestOp {
70 fn display(&self, _cx: &mut Cx) -> Result<String> {
71 Ok("#<function midi/chord-digest>".to_owned())
72 }
73
74 fn as_any(&self) -> &dyn std::any::Any {
75 self
76 }
77}
78
79impl ObjectCompat for ChordDigestOp {
80 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
81 cx.resolve_class(&Symbol::qualified("core", "Function"))
82 }
83
84 fn as_callable(&self) -> Option<&dyn Callable> {
85 Some(self)
86 }
87}
88
89impl Callable for ChordDigestOp {
90 fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
91 let args = args.into_vec();
92 let [root] = args.as_slice() else {
93 return Err(Error::Eval(format!(
94 "midi/chord-digest expects one root-note argument, got {}",
95 args.len()
96 )));
97 };
98 let root = root_arg(cx, root)?;
99 cx.factory().string(chord_digest(root))
100 }
101}
102
103pub struct MidiDigestLib;
105
106impl Lib for MidiDigestLib {
107 fn manifest(&self) -> LibManifest {
108 LibManifest {
109 id: manifest_name(),
110 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
111 abi: AbiVersion { major: 0, minor: 1 },
112 target: LibTarget::HostRegistered,
113 requires: Vec::new(),
114 capabilities: Vec::new(),
115 exports: vec![Export::Function {
116 symbol: chord_digest_symbol(),
117 function_id: None,
118 }],
119 }
120 }
121
122 fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
123 linker.function_value(
124 chord_digest_symbol(),
125 cx.factory().opaque(Arc::new(ChordDigestOp))?,
126 )?;
127 Ok(())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn chord_digest_is_a_deterministic_frame() {
137 let digest = chord_digest(60);
139 assert!(digest.starts_with("(frame (bytes 9) (hash "));
140 assert_eq!(digest, chord_digest(60));
141 }
142}