closer and closer to testing it away

This commit is contained in:
stop screaming 2026-02-21 18:13:36 +02:00
parent 817d2a722c
commit 4aef21f60d
10 changed files with 1782 additions and 2028 deletions

View file

@ -1076,3 +1076,64 @@
//}
//};
//}
//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
//impl<T, U: Has<Option<T>>> MaybeHas<T> for U {
//fn get (&self) -> Option<&T> {
//Has::<Option<T>>::get(self).as_ref()
//}
//}
//
//// DRAW SAMPLE ADD:
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
//let cells_x = 8u16;
//let cells_y = 8u16;
//let cell_width = 10u16;
//let cell_height = 2u16;
//let width = cells_x * cell_width;
//let height = cells_y * cell_height;
//let cols = Map::east(
//cell_width,
//move||0..cells_x,
//move|x, _|Map::south(
//cell_height,
//move||0..cells_y,
//move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height)
//)
//);
//cols
//Thunk::new(|to: &mut TuiOut|{
//})

View file

@ -22,6 +22,10 @@ pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, n
pub extern crate tengri;
pub(crate) use tengri::{
*,
dizzle::{
self,
*
},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
@ -76,9 +80,10 @@ pub(crate) use JackState::*;
///
/// ```
/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let proj = Default::default();
/// let conf = Default::default();
/// let tek = tek::tek(&jack, proj, conf, "mode-doctest");
/// let proj = tek::Arrangement::default();
/// let mut conf = tek::Config::default();
/// conf.add("(mode hello)");
/// let tek = tek::tek(&jack, proj, conf, "hello");
/// ```
pub fn tek (
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
@ -124,55 +129,22 @@ fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
})
}
pub fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}
pub fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut mode = Mode::default();
body.each(|item|mode.add(item))?;
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
pub fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut map = Bind::new();
body.each(|item|if item.expr().head() == Ok(Some("see")) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
map.add(key, Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
})?;
binds.write().unwrap().insert(name.as_ref().into(), map);
pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?);
Ok(())
}
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ v0.4.0, 2026 winter (or is it) ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
fn collect_commands (
app: &App, input: &TuiIn
) -> Usually<Vec<AppCommand>> {
fn collect_commands (app: &App, input: &TuiIn) -> Usually<Vec<AppCommand>> {
let mut commands = vec![];
for id in app.mode.keys.iter() {
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
@ -335,42 +307,6 @@ pub fn tek_print_status (project: &Arrangement) {
// TODO dawvert integration
}
pub const DEFAULT_PPQ: f64 = 96.0;
/// FIXME: remove this and use PPQ from timebase everywhere:
pub const PPQ: usize = 96;
/// (pulses, name), assuming 96 PPQ
pub const NOTE_DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (2, "1/192"),
(3, "1/128"), (4, "1/96"),
(6, "1/64"), (8, "1/48"),
(12, "1/32"), (16, "1/24"),
(24, "1/16"), (32, "1/12"),
(48, "1/8"), (64, "1/6"),
(96, "1/4"), (128, "1/3"),
(192, "1/2"), (256, "2/3"),
(384, "1/1"), (512, "4/3"),
(576, "3/2"), (768, "2/1"),
(1152, "3/1"), (1536, "4/1"),
(2304, "6/1"), (3072, "8/1"),
(3456, "9/1"), (6144, "16/1"),
];
pub const NOTE_NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>)
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
@ -1261,15 +1197,49 @@ mod view {
}
}
pub(crate) fn sampler_jack_process (
state: &mut Sampler, _: &Client, scope: &ProcessScope
) -> Control {
if let Some(midi_in) = &state.midi_in {
for midi in midi_in.port().iter(scope) {
sampler_midi_in(&state.samples, &state.voices, midi)
}
}
state.process_audio_out(scope);
state.process_audio_in(scope);
Control::Continue
}
/// Create [Voice]s from [Sample]s in response to MIDI input.
pub(crate) fn sampler_midi_in (
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
) {
if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(sample) = samples.get(key.as_int() as usize) {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
},
MidiMessage::Controller { controller: _, value: _ } => {
// TODO
}
_ => {}
}
}
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
@ -1278,3 +1248,46 @@ mod view {
}
}
}
pub const DEFAULT_PPQ: f64 = 96.0;
/// FIXME: remove this and use PPQ from timebase everywhere:
pub const PPQ: usize = 96;
/// (pulses, name), assuming 96 PPQ
pub const NOTE_DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (2, "1/192"),
(3, "1/128"), (4, "1/96"),
(6, "1/64"), (8, "1/48"),
(12, "1/32"), (16, "1/24"),
(24, "1/16"), (32, "1/12"),
(48, "1/8"), (64, "1/6"),
(96, "1/4"), (128, "1/3"),
(192, "1/2"), (256, "2/3"),
(384, "1/1"), (512, "4/3"),
(576, "3/2"), (768, "2/1"),
(1152, "3/1"), (1536, "4/1"),
(2304, "6/1"), (3072, "8/1"),
(3456, "9/1"), (6144, "16/1"),
];
pub const NOTE_NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ v0.4.0, 2026 winter (or is it) ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;

File diff suppressed because it is too large Load diff

View file

@ -147,10 +147,13 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
pub modes: Modes,
}
/// An input binding.
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```
/// let bind = tek::Bind::<(), ()>::default();
/// let lang = "(@x (nop)) (@y (nop) (nop))";
/// let bind = tek::Bind::<tek::tengri::TuiEvent, std::sync::Arc<str>>::load(&lang).unwrap();
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
/// ```
#[derive(Debug)] pub struct Bind<E, C>(
/// Map of each event (e.g. key combination) to
@ -159,10 +162,12 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
pub BTreeMap<E, Vec<Binding<C>>>
);
/// An input binding.
/// A sequence of zero or more commands (e.g. [AppCommand]),
/// optionally filtered by [Condition] to form layers.
///
/// ```
/// let binding: tek::Binding<()> = Default::default();
/// //FIXME: Why does it overflow?
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
/// ```
#[derive(Debug, Clone)] pub struct Binding<C> {
pub commands: Arc<[C]>,
@ -302,7 +307,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
///
/// ```
///
/// let _ = tek::Timebase::default();
/// ```
#[derive(Debug, Clone)] pub struct Timebase {
/// Audio samples per second
@ -316,15 +321,28 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
/// Iterator that emits subsequent ticks within a range.
///
/// ```
/// let iter = tek::TicksIterator::default();
/// let iter = tek::Ticker::default();
/// ```
#[derive(Debug, Default)] pub struct TicksIterator {
#[derive(Debug, Default)] pub struct Ticker {
pub spp: f64,
pub sample: usize,
pub start: usize,
pub end: usize,
}
///
/// ```
/// let _ = tek::MidiCursor::default();
/// ```
#[derive(Debug, Clone)] pub struct MidiCursor {
/// Time coordinate of cursor
pub time_pos: Arc<AtomicUsize>,
/// Note coordinate of cursor
pub note_pos: Arc<AtomicUsize>,
/// Length of note that will be inserted, in pulses
pub note_len: Arc<AtomicUsize>,
}
///
/// ```
/// use tek::{TimeRange, NoteRange};
@ -341,19 +359,6 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
/// let _ = model.get_note_axis();
/// let _ = model.get_note_hi();
/// ```
#[derive(Debug, Clone)] pub struct MidiCursor {
/// Time coordinate of cursor
pub time_pos: Arc<AtomicUsize>,
/// Note coordinate of cursor
pub note_pos: Arc<AtomicUsize>,
/// Length of note that will be inserted, in pulses
pub note_len: Arc<AtomicUsize>,
}
///
/// ```
///
/// ```
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
pub time_len: Arc<AtomicUsize>,
/// Length of visible time axis
@ -373,7 +378,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
///
/// ```
///
/// let _ = tek::Moment::default();
/// ```
#[derive(Debug, Default, Clone)] pub struct Moment {
pub timebase: Arc<Timebase>,
@ -387,7 +392,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
///
/// ```
///
/// let _ = tek::Moment2::default();
/// ```
#[derive(Debug, Clone, Default)] pub enum Moment2 {
#[default] None,
@ -834,28 +839,18 @@ pub struct PoolView<'a>(pub &'a Pool);
pub name: Arc<str>,
/// Device color.
pub color: ItemTheme,
/// Audio input ports. Samples get recorded here.
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
/// Audio input meters.
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
/// Sample currently being recorded.
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
/// Recording buffer.
pub buffer: Vec<Vec<f32>>,
pub buffer: Vec<Vec<f32>>,
/// Samples mapped to MIDI notes.
pub samples: SampleKit<128>,
/// Samples that are not mapped to MIDI notes.
pub unmapped: Vec<Arc<RwLock<Sample>>>,
/// Sample currently being edited.
pub editing: Option<Arc<RwLock<Sample>>>,
/// MIDI input port. Triggers sample playback.
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
pub samples: SampleKit<128>,
/// Collection of currently playing instances of samples.
pub voices: Arc<RwLock<Vec<Voice>>>,
/// Audio output ports. Voices get played here.
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
/// Audio output meters.
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
/// Samples that are not mapped to MIDI notes.
pub unmapped: Vec<Arc<RwLock<Sample>>>,
/// Sample currently being edited.
pub editing: Option<Arc<RwLock<Sample>>>,
/// How to mix the voices.
pub mixing_mode: MixingMode,
/// How to meter the inputs and outputs.
@ -872,6 +867,16 @@ pub struct PoolView<'a>(pub &'a Pool);
pub note_pt: AtomicUsize,
/// Selected note as row/col.
pub cursor: (AtomicUsize, AtomicUsize),
/// Audio input meters.
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
/// Audio input ports. Samples are recorded from here.
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
/// MIDI input port. Sampler are triggered from here.
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
/// Audio output ports. Voices are played into here.
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
/// Audio output meters.
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
}
/// Collection of samples, one per slot, fixed number of slots.
@ -909,7 +914,7 @@ pub struct PoolView<'a>(pub &'a Pool);
pub velocity: f32,
}
pub struct AddSampleModal {
#[derive(Default, Debug)] pub struct SampleAdd {
pub exited: bool,
pub dir: PathBuf,
pub subdirs: Vec<OsString>,

View file

@ -318,9 +318,14 @@ pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
/// ```
/// use tek::{MidiEditor, HasEditor, tengri::Has};
/// struct TestEditorHost(Option<MidiEditor>);
/// tek::tengri::has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
///
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
/// struct TestEditorHost(Option<MidiEditor>);
/// impl Has<Option<MidiEditor>> for TestEditorHost {
/// fn get (&self) -> &Option<MidiEditor> { &self.0 }
/// fn get_mut (&mut self) -> &mut Option<MidiEditor> { &mut self.0 }
/// }
///
/// let _ = host.editor();
/// let _ = host.editor_mut();
/// let _ = host.is_editing();