P1a: material import, sixteen faces, sample engine and quarry
This commit is contained in:
+237
-36
@@ -12,7 +12,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{BufferSize, FromSample, SampleFormat, SizedSample, Stream, StreamConfig};
|
||||
use rtrb::{Consumer, Producer, RingBuffer};
|
||||
use thiserror::Error;
|
||||
use zhila_core::Engine as EngineControl;
|
||||
use zhila_core::{Engine as EngineControl, MaterialSnapshot};
|
||||
use zhila_protocol::{
|
||||
AudioInfo, LOOP_STEPS, MAX_BPM, MAX_GAIN, MIN_BPM, MIN_GAIN, MeterFrame, Pattern,
|
||||
STEPS_PER_BAR, VEIN_COUNT, VeinId,
|
||||
@@ -20,13 +20,27 @@ use zhila_protocol::{
|
||||
|
||||
const VOICES: usize = 32;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// (vein index, velocity, optional (core index, face index)) for one step.
|
||||
type StepTrigger = (usize, u8, Option<(usize, u8)>);
|
||||
|
||||
enum AudioCommand {
|
||||
Play,
|
||||
Stop,
|
||||
Strike { vein: u8, velocity: f32 },
|
||||
Strike {
|
||||
vein: u8,
|
||||
velocity: f32,
|
||||
},
|
||||
StrikeFace {
|
||||
core: String,
|
||||
face: u8,
|
||||
velocity: f32,
|
||||
},
|
||||
SetTempo(f32),
|
||||
SetGain { vein: u8, gain: f32 },
|
||||
SetGain {
|
||||
vein: u8,
|
||||
gain: f32,
|
||||
},
|
||||
SetMaterial(Arc<MaterialSnapshot>),
|
||||
}
|
||||
|
||||
struct Levels {
|
||||
@@ -63,6 +77,14 @@ impl Levels {
|
||||
}
|
||||
}
|
||||
|
||||
struct SampleVoice {
|
||||
core_index: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
position: f64,
|
||||
rate: f64,
|
||||
}
|
||||
|
||||
struct Voice {
|
||||
active: bool,
|
||||
vein: usize,
|
||||
@@ -78,6 +100,7 @@ struct Voice {
|
||||
hp: f32,
|
||||
prev: f32,
|
||||
rng: u32,
|
||||
sample: Option<SampleVoice>,
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
@@ -97,6 +120,7 @@ impl Voice {
|
||||
hp: 0.0,
|
||||
prev: 0.0,
|
||||
rng: 0x9e37_79b9,
|
||||
sample: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,19 +134,52 @@ impl Voice {
|
||||
self.lp = 0.0;
|
||||
self.hp = 0.0;
|
||||
self.prev = 0.0;
|
||||
let (freq, attack, decay, pan) = match vein {
|
||||
VeinId::Bass => (55.0, 0.004, 3.2, 0.0),
|
||||
VeinId::Crack => (0.0, 0.001, 38.0, 0.35),
|
||||
VeinId::Voice => (220.0, 0.09, 1.1, -0.25),
|
||||
VeinId::Noise => (0.0, 0.004, 5.0, 0.55),
|
||||
VeinId::Silt => (1318.5, 0.002, 14.0, -0.45),
|
||||
self.sample = None;
|
||||
let (freq, attack, decay) = match vein {
|
||||
VeinId::Bass => (55.0, 0.004, 3.2),
|
||||
VeinId::Crack => (0.0, 0.001, 38.0),
|
||||
VeinId::Voice => (220.0, 0.09, 1.1),
|
||||
VeinId::Noise => (0.0, 0.004, 5.0),
|
||||
VeinId::Silt => (1318.5, 0.002, 14.0),
|
||||
};
|
||||
self.freq = freq;
|
||||
self.attack = attack;
|
||||
self.decay = decay;
|
||||
let angle = (pan + 1.0) * FRAC_PI_4;
|
||||
self.gain_l = angle.cos();
|
||||
self.gain_r = angle.sin();
|
||||
let (gain_l, gain_r) = pan_for(vein);
|
||||
self.gain_l = gain_l;
|
||||
self.gain_r = gain_r;
|
||||
}
|
||||
|
||||
fn trigger_sample(
|
||||
&mut self,
|
||||
vein: VeinId,
|
||||
velocity: f32,
|
||||
core_index: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
rate: f64,
|
||||
) {
|
||||
self.active = true;
|
||||
self.vein = vein.index();
|
||||
self.phase = 0.0;
|
||||
self.freq = 0.0;
|
||||
self.t = 0.0;
|
||||
self.velocity = velocity.clamp(0.0, 1.0);
|
||||
self.attack = 0.004;
|
||||
self.decay = 4.0;
|
||||
self.lp = 0.0;
|
||||
self.hp = 0.0;
|
||||
self.prev = 0.0;
|
||||
let (gain_l, gain_r) = pan_for(vein);
|
||||
self.gain_l = gain_l;
|
||||
self.gain_r = gain_r;
|
||||
self.sample = Some(SampleVoice {
|
||||
core_index,
|
||||
start,
|
||||
end,
|
||||
position: start as f64,
|
||||
rate,
|
||||
});
|
||||
}
|
||||
|
||||
fn noise(&mut self) -> f32 {
|
||||
@@ -130,8 +187,35 @@ impl Voice {
|
||||
(self.rng as f32 / u32::MAX as f32) * 2.0 - 1.0
|
||||
}
|
||||
|
||||
fn next(&mut self, sample_rate: f32) -> f32 {
|
||||
fn next(&mut self, sample_rate: f32, material: Option<&MaterialSnapshot>) -> f32 {
|
||||
self.t += 1.0 / sample_rate;
|
||||
if let Some(voice) = self.sample.as_mut() {
|
||||
let Some(core) = material.and_then(|material| material.cores.get(voice.core_index))
|
||||
else {
|
||||
self.active = false;
|
||||
return 0.0;
|
||||
};
|
||||
if voice.position >= voice.end as f64 {
|
||||
self.active = false;
|
||||
return 0.0;
|
||||
}
|
||||
let index = voice.position.floor() as usize;
|
||||
let fraction = (voice.position - index as f64) as f32;
|
||||
let first = core.samples.get(index).copied().unwrap_or(0.0);
|
||||
let second = core.samples.get(index + 1).copied().unwrap_or(first);
|
||||
let raw = first + (second - first) * fraction;
|
||||
let remaining =
|
||||
((voice.end as f64 - voice.position) / voice.rate / sample_rate as f64) as f32;
|
||||
let fade_in = (self.t / 0.004).min(1.0);
|
||||
let fade_out = (remaining / 0.004).min(1.0);
|
||||
voice.position += voice.rate;
|
||||
if voice.position >= voice.end as f64 {
|
||||
self.active = false;
|
||||
}
|
||||
let _ = voice.start;
|
||||
return raw * fade_in.min(fade_out) * self.velocity;
|
||||
}
|
||||
|
||||
let attack = (self.t / self.attack).min(1.0);
|
||||
let env = attack * (-self.t * self.decay).exp();
|
||||
if env < 0.0002 || self.t > 8.0 {
|
||||
@@ -173,6 +257,18 @@ impl Voice {
|
||||
}
|
||||
}
|
||||
|
||||
fn pan_for(vein: VeinId) -> (f32, f32) {
|
||||
let pan = match vein {
|
||||
VeinId::Bass => 0.0,
|
||||
VeinId::Crack => 0.35,
|
||||
VeinId::Voice => -0.25,
|
||||
VeinId::Noise => 0.55,
|
||||
VeinId::Silt => -0.45,
|
||||
};
|
||||
let angle = (pan + 1.0) * FRAC_PI_4;
|
||||
(angle.cos(), angle.sin())
|
||||
}
|
||||
|
||||
struct AudioState {
|
||||
rx: Consumer<AudioCommand>,
|
||||
sample_rate: f32,
|
||||
@@ -181,7 +277,8 @@ struct AudioState {
|
||||
playing: bool,
|
||||
step: usize,
|
||||
pending: f64,
|
||||
pattern: [[u8; LOOP_STEPS]; VEIN_COUNT],
|
||||
fallback: Pattern,
|
||||
material: Option<Arc<MaterialSnapshot>>,
|
||||
voices: [Voice; VOICES],
|
||||
gains: [f32; VEIN_COUNT],
|
||||
master_env: f32,
|
||||
@@ -192,13 +289,6 @@ struct AudioState {
|
||||
|
||||
impl AudioState {
|
||||
fn new(sample_rate: u32, channels: u16, pattern: &Pattern, rx: Consumer<AudioCommand>) -> Self {
|
||||
let mut grid = [[0u8; LOOP_STEPS]; VEIN_COUNT];
|
||||
for row in &pattern.rows {
|
||||
let index = row.vein.index();
|
||||
for (step, velocity) in row.velocities.iter().take(LOOP_STEPS).enumerate() {
|
||||
grid[index][step] = *velocity;
|
||||
}
|
||||
}
|
||||
Self {
|
||||
rx,
|
||||
sample_rate: sample_rate as f32,
|
||||
@@ -207,7 +297,8 @@ impl AudioState {
|
||||
playing: false,
|
||||
step: 0,
|
||||
pending: 0.0,
|
||||
pattern: grid,
|
||||
fallback: pattern.clone(),
|
||||
material: None,
|
||||
voices: std::array::from_fn(|_| Voice::inactive()),
|
||||
gains: zhila_core::INITIAL_GAINS,
|
||||
master_env: 0.0,
|
||||
@@ -246,6 +337,34 @@ impl AudioState {
|
||||
};
|
||||
self.trigger(vein, velocity);
|
||||
}
|
||||
AudioCommand::StrikeFace {
|
||||
core,
|
||||
face,
|
||||
velocity,
|
||||
} => {
|
||||
let Some(material) = self.material.as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(core_index) = material
|
||||
.cores
|
||||
.iter()
|
||||
.position(|candidate| candidate.id == core)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let vein_index = material
|
||||
.pattern
|
||||
.rows
|
||||
.iter()
|
||||
.find(|row| {
|
||||
row.face.as_ref().is_some_and(|reference| {
|
||||
reference.core == core && reference.face == face
|
||||
})
|
||||
})
|
||||
.map(|row| row.vein.index())
|
||||
.unwrap_or(0);
|
||||
self.trigger_sample(core_index, face, vein_index, velocity);
|
||||
}
|
||||
AudioCommand::SetTempo(bpm) => {
|
||||
self.bpm = bpm.clamp(MIN_BPM, MAX_BPM);
|
||||
}
|
||||
@@ -254,15 +373,14 @@ impl AudioState {
|
||||
self.gains[index.index()] = gain.clamp(MIN_GAIN, MAX_GAIN);
|
||||
}
|
||||
}
|
||||
AudioCommand::SetMaterial(material) => {
|
||||
self.material = Some(material);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger(&mut self, vein: VeinId, velocity: f32) {
|
||||
if velocity <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let index = self
|
||||
.voices
|
||||
fn free_voice(&self) -> usize {
|
||||
self.voices
|
||||
.iter()
|
||||
.position(|voice| !voice.active)
|
||||
.unwrap_or_else(|| {
|
||||
@@ -272,18 +390,86 @@ impl AudioState {
|
||||
.max_by(|a, b| a.1.t.total_cmp(&b.1.t))
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(0)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn trigger(&mut self, vein: VeinId, velocity: f32) {
|
||||
if velocity <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let index = self.free_voice();
|
||||
self.rng = self.rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
|
||||
self.voices[index].trigger(vein, velocity, self.rng);
|
||||
}
|
||||
|
||||
fn trigger_sample(
|
||||
&mut self,
|
||||
core_index: usize,
|
||||
face_index: u8,
|
||||
vein_index: usize,
|
||||
velocity: f32,
|
||||
) {
|
||||
if velocity <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let Some(material) = self.material.as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
let Some(core) = material.cores.get(core_index) else {
|
||||
return;
|
||||
};
|
||||
let Some(face) = core.faces.iter().find(|face| face.index == face_index) else {
|
||||
return;
|
||||
};
|
||||
let start = face.start as usize;
|
||||
let end = face.end as usize;
|
||||
if start >= end || end > core.samples.len() {
|
||||
return;
|
||||
}
|
||||
let Some(vein) = VeinId::from_index(vein_index) else {
|
||||
return;
|
||||
};
|
||||
let rate = core.sample_rate as f64 / self.sample_rate as f64;
|
||||
let index = self.free_voice();
|
||||
self.voices[index].trigger_sample(vein, velocity, core_index, start, end, rate);
|
||||
}
|
||||
|
||||
fn trigger_step(&mut self) {
|
||||
let step = self.step;
|
||||
for vein in 0..VEIN_COUNT {
|
||||
let velocity = self.pattern[vein][step];
|
||||
if velocity > 0
|
||||
&& let Some(vein) = VeinId::from_index(vein)
|
||||
{
|
||||
let (plan, count) = {
|
||||
let material = self.material.as_ref().cloned();
|
||||
let pattern = material
|
||||
.as_ref()
|
||||
.map(|material| &material.pattern)
|
||||
.unwrap_or(&self.fallback);
|
||||
let mut plan: [StepTrigger; 8] = [(0, 0, None); 8];
|
||||
let mut count = 0;
|
||||
for row in &pattern.rows {
|
||||
if count >= plan.len() {
|
||||
break;
|
||||
}
|
||||
let velocity = row.velocities.get(step).copied().unwrap_or(0);
|
||||
if velocity == 0 {
|
||||
continue;
|
||||
}
|
||||
let face = row.face.as_ref().and_then(|reference| {
|
||||
material.as_ref().and_then(|material| {
|
||||
material
|
||||
.cores
|
||||
.iter()
|
||||
.position(|core| core.id == reference.core)
|
||||
.map(|core_index| (core_index, reference.face))
|
||||
})
|
||||
});
|
||||
plan[count] = (row.vein.index(), velocity, face);
|
||||
count += 1;
|
||||
}
|
||||
(plan, count)
|
||||
};
|
||||
for (vein_index, velocity, face) in plan.into_iter().take(count) {
|
||||
if let Some((core_index, face_index)) = face {
|
||||
self.trigger_sample(core_index, face_index, vein_index, velocity as f32 / 127.0);
|
||||
} else if let Some(vein) = VeinId::from_index(vein_index) {
|
||||
self.trigger(vein, velocity as f32 / 127.0);
|
||||
}
|
||||
}
|
||||
@@ -299,6 +485,7 @@ impl AudioState {
|
||||
self.handle(command);
|
||||
}
|
||||
|
||||
let material = self.material.as_ref().cloned();
|
||||
for frame in data.chunks_mut(self.channels) {
|
||||
if self.playing {
|
||||
if self.pending <= 0.0 {
|
||||
@@ -315,7 +502,7 @@ impl AudioState {
|
||||
if !voice.active {
|
||||
continue;
|
||||
}
|
||||
let sample = voice.next(self.sample_rate);
|
||||
let sample = voice.next(self.sample_rate, material.as_deref());
|
||||
let index = voice.vein;
|
||||
let amplitude = sample * self.gains[index];
|
||||
left += amplitude * voice.gain_l;
|
||||
@@ -374,6 +561,14 @@ impl EngineControl for Control {
|
||||
});
|
||||
}
|
||||
|
||||
fn strike_face(&self, core: &str, face: u8, velocity: f32) {
|
||||
self.send(AudioCommand::StrikeFace {
|
||||
core: core.to_string(),
|
||||
face,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
|
||||
fn set_tempo(&self, bpm: f32) {
|
||||
self.send(AudioCommand::SetTempo(bpm));
|
||||
}
|
||||
@@ -385,6 +580,10 @@ impl EngineControl for Control {
|
||||
});
|
||||
}
|
||||
|
||||
fn set_material(&self, material: Arc<MaterialSnapshot>) {
|
||||
self.send(AudioCommand::SetMaterial(material));
|
||||
}
|
||||
|
||||
fn info(&self) -> AudioInfo {
|
||||
self.info.clone()
|
||||
}
|
||||
@@ -527,8 +726,10 @@ impl EngineControl for NullEngine {
|
||||
fn play(&self) {}
|
||||
fn stop(&self) {}
|
||||
fn strike(&self, _vein: VeinId, _velocity: f32) {}
|
||||
fn strike_face(&self, _core: &str, _face: u8, _velocity: f32) {}
|
||||
fn set_tempo(&self, _bpm: f32) {}
|
||||
fn set_gain(&self, _vein: VeinId, _gain: f32) {}
|
||||
fn set_material(&self, _material: Arc<MaterialSnapshot>) {}
|
||||
fn info(&self) -> AudioInfo {
|
||||
self.info.clone()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user