use rill_core::{
buffer::{ResourceRegistry, TapeReader},
math::Transcendental,
traits::{Node, NodeCategory, NodeMetadata, NodeState, Source},
ClockTick, NodeId, ParamValue, ParameterId, Port, ProcessError, ProcessResult, RenderContext,
};
pub struct ReadHead<T: Transcendental, const BUF_SIZE: usize> {
id: NodeId,
metadata: NodeMetadata,
outputs: Vec<Port<T, BUF_SIZE>>,
state: NodeState<T, BUF_SIZE>,
tape: Option<TapeReader<T>>,
resource_name: String,
delay: f32,
sample_rate: f32,
current_delay_samples: f64,
delay_smoothing: f64,
}
const DELAY_SMOOTH_SECONDS: f64 = 0.008;
fn delay_smoothing_coeff(sample_rate: f64) -> f64 {
1.0 - (-1.0 / (DELAY_SMOOTH_SECONDS * sample_rate)).exp()
}
#[allow(unsafe_code)]
unsafe impl<T: Transcendental, const B: usize> Send for ReadHead<T, B> {}
#[allow(unsafe_code)]
unsafe impl<T: Transcendental, const B: usize> Sync for ReadHead<T, B> {}
impl<T: Transcendental, const BUF_SIZE: usize> Default for ReadHead<T, BUF_SIZE> {
fn default() -> Self {
Self::new()
}
}
impl<T: Transcendental, const BUF_SIZE: usize> ReadHead<T, BUF_SIZE> {
pub fn new() -> Self {
Self::with_resource("tape_0")
}
pub fn with_resource(resource_name: &str) -> Self {
let mut metadata = NodeMetadata::new("ReadHead", NodeCategory::Source);
metadata.parameters = vec![rill_core::ParamMetadata::new(
"delay",
rill_core::ParamType::Float,
ParamValue::Float(0.5),
)
.with_range(0.01, 2.0, 0.01)];
let outputs = vec![Port::output(NodeId(0), 0, "out")];
Self {
id: NodeId(0),
metadata,
outputs,
state: NodeState::new(44100.0),
tape: None,
resource_name: resource_name.to_string(),
delay: 0.5,
sample_rate: 44100.0,
current_delay_samples: 0.5 * 44100.0,
delay_smoothing: delay_smoothing_coeff(44100.0),
}
}
pub fn set_reader(&mut self, reader: TapeReader<T>) {
self.tape = Some(reader);
}
}
impl<T: Transcendental, const BUF_SIZE: usize> Source<T, BUF_SIZE> for ReadHead<T, BUF_SIZE> {
#[allow(clippy::needless_range_loop)]
fn generate(
&mut self,
_ctx: &RenderContext,
_control_inputs: &[T],
_clock_inputs: &[RenderContext],
_tick: &ClockTick,
) -> ProcessResult<()> {
let Some(tape) = self.tape.as_ref() else {
debug_assert!(false, "ReadHead: tape not set");
return Ok(());
};
let target = (self.delay as f64) * (self.sample_rate as f64);
let glide = self.delay_smoothing;
let mut current = self.current_delay_samples;
let out = self.outputs[0].write();
let n = BUF_SIZE;
for i in 0..n {
let d = current + (n - 1 - i) as f64;
out[i] = tape.read_interpolated(d.max(0.0));
current += (target - current) * glide;
}
self.current_delay_samples = current;
self.state.advance();
Ok(())
}
fn num_signal_outputs(&self) -> usize {
1
}
}
impl<T: Transcendental, const BUF_SIZE: usize> Node<T, BUF_SIZE> for ReadHead<T, BUF_SIZE> {
fn node_type_id(&self) -> rill_core::NodeTypeId
where
Self: 'static + Sized,
{
rill_core::NodeTypeId::of::<Self>()
}
fn id(&self) -> NodeId {
self.id
}
fn set_id(&mut self, id: NodeId) {
self.id = id;
}
fn metadata(&self) -> NodeMetadata {
self.metadata.clone()
}
fn init(&mut self, sr: f32) {
self.sample_rate = sr;
self.state.sample_rate = sr;
self.current_delay_samples = (self.delay as f64) * (sr as f64);
self.delay_smoothing = delay_smoothing_coeff(sr as f64);
}
fn reset(&mut self) {
self.state.sample_pos = 0;
self.state.blocks_processed = 0;
self.current_delay_samples = (self.delay as f64) * (self.sample_rate as f64);
}
fn resolve_resources(&mut self, resources: &mut ResourceRegistry<T>) {
if self.tape.is_some() {
return;
}
self.tape = resources.reader(&self.resource_name);
}
fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue> {
match id.as_str() {
"delay" => Some(ParamValue::Float(self.delay)),
_ => None,
}
}
fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()> {
let name = id.as_str();
if let Some(v) = value.as_f32() {
match name {
"delay" => {
self.delay = v.clamp(0.01, 2.0);
Ok(())
}
_ => Err(ProcessError::parameter(format!(
"Unknown parameter: {}",
name
))),
}
} else {
Err(ProcessError::parameter("Expected float value"))
}
}
fn input_port(&self, _: usize) -> Option<&Port<T, BUF_SIZE>> {
None
}
fn input_port_mut(&mut self, _: usize) -> Option<&mut Port<T, BUF_SIZE>> {
None
}
fn output_port(&self, i: usize) -> Option<&Port<T, BUF_SIZE>> {
self.outputs.get(i)
}
fn output_port_mut(&mut self, i: usize) -> Option<&mut Port<T, BUF_SIZE>> {
self.outputs.get_mut(i)
}
fn control_port(&self, _: usize) -> Option<&Port<T, BUF_SIZE>> {
None
}
fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, BUF_SIZE>> {
None
}
fn num_signal_inputs(&self) -> usize {
0
}
fn num_signal_outputs(&self) -> usize {
1
}
fn state(&self) -> &NodeState<T, BUF_SIZE> {
&self.state
}
fn state_mut(&mut self) -> &mut NodeState<T, BUF_SIZE> {
&mut self.state
}
}
#[cfg(test)]
mod tests {
use super::*;
use rill_core::buffer::{tape_handles, TapeLoop};
fn ramp_tape(n: usize) -> TapeLoop<f32> {
let mut tape = TapeLoop::<f32>::new(1024).unwrap();
for i in 0..n {
tape.write(i as f32);
}
tape
}
fn delay_param(v: f32) -> (ParameterId, ParamValue) {
(ParameterId::new("delay").unwrap(), ParamValue::Float(v))
}
#[test]
fn test_read_head_creation() {
let rh = ReadHead::<f32, 64>::new();
assert!((rh.delay - 0.5).abs() < 1e-6);
assert_eq!(rh.outputs.len(), 1);
}
#[test]
fn read_head_integer_delay_reads_exact_samples() {
let tape = ramp_tape(40);
let mut rh = ReadHead::<f32, 4>::new();
let (id, v) = delay_param(0.1);
rh.set_parameter(&id, v).unwrap();
rh.init(100.0);
let (_writer, reader) = tape_handles(tape);
rh.set_reader(reader);
let ctx = RenderContext::new(0, 4, 100.0);
let tick = ClockTick::new(0, 4, 100.0, String::new());
rh.generate(&ctx, &[], &[], &tick).unwrap();
let out = rh.outputs[0].read();
assert_eq!(out[0], 26.0);
assert_eq!(out[3], 29.0);
}
#[test]
fn read_head_fractional_delay_interpolates() {
let tape = ramp_tape(40);
let mut rh = ReadHead::<f32, 4>::new();
let (id, v) = delay_param(0.105);
rh.set_parameter(&id, v).unwrap();
rh.init(100.0);
let (_writer, reader) = tape_handles(tape);
rh.set_reader(reader);
let ctx = RenderContext::new(0, 4, 100.0);
let tick = ClockTick::new(0, 4, 100.0, String::new());
rh.generate(&ctx, &[], &[], &tick).unwrap();
let out = rh.outputs[0].read();
assert!(
out[0] > 25.0 && out[0] < 26.0,
"expected interpolated ~25.5, got {}",
out[0]
);
assert!((out[0] - 25.5).abs() < 0.01, "got {}", out[0]);
}
#[test]
fn read_head_delay_change_glides_not_jumps() {
let tape = ramp_tape(40);
let mut rh = ReadHead::<f32, 4>::new();
let (id, v) = delay_param(0.1);
rh.set_parameter(&id, v).unwrap();
rh.init(100.0);
let (_writer, reader) = tape_handles(tape);
rh.set_reader(reader);
let (id2, v2) = delay_param(0.3);
rh.set_parameter(&id2, v2).unwrap();
let ctx = RenderContext::new(0, 4, 100.0);
let tick = ClockTick::new(0, 4, 100.0, String::new());
rh.generate(&ctx, &[], &[], &tick).unwrap();
let out = rh.outputs[0].read();
assert!(
out[0] > 20.0,
"delay jumped instead of gliding: out[0]={}",
out[0]
);
}
}