mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-09-18 12:56:42 +02:00
Compare commits
2 commits
07f2290017
...
ba8ff1ae69
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba8ff1ae69 | ||
|
|
b4424fcb69 |
29 changed files with 2503 additions and 2425 deletions
398
src/app.rs
398
src/app.rs
|
|
@ -1,398 +0,0 @@
|
|||
use crate::*;
|
||||
pub mod audio; #[allow(unused)] pub use self::audio::*;
|
||||
pub mod bind; pub use self::bind::*;
|
||||
pub mod config; pub use self::config::*;
|
||||
pub mod draw; pub use self::draw::*;
|
||||
pub mod size; pub use self::size::*;
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Pool: |self: App|self.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
|
||||
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
|
||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
||||
impl_has_clips!( |self: App|self.pool.clips);
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scene_add(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
#[namespace(u8)]
|
||||
#[namespace(isize)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(Arc<str> App::get_arc_str)]
|
||||
#[namespace(u16 App::get_u16)]
|
||||
#[namespace(usize App::get_usize)]
|
||||
#[namespace(bool App::get_bool)]
|
||||
#[namespace(Selection App::get_selection)]
|
||||
#[namespace(Color App::get_color)]
|
||||
#[namespace(Option<u7> App::get_opt_u7)]
|
||||
#[namespace(Option<u16> App::get_opt_u16)]
|
||||
#[namespace(Option<usize> App::get_opt_usize)]
|
||||
#[namespace(Option<Arc<RwLock<MidiClip>>> App::get_clip)]
|
||||
pub struct App {
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Config,
|
||||
/// Currently selected mode
|
||||
pub mode: Arc<Mode<Arc<str>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Arrangement,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>
|
||||
}
|
||||
impl App {
|
||||
/// Create a new application instance from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let proj = tek::Arrangement::default();
|
||||
/// let mut conf = tek::Config::default();
|
||||
/// conf.add("(mode hello)");
|
||||
/// let tek = tek::App::new(&jack, proj, conf, "hello");
|
||||
/// ```
|
||||
pub fn new (
|
||||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||||
) -> Self {
|
||||
let mode: &str = mode.as_ref();
|
||||
App {
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
jack: jack.clone(),
|
||||
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// Update memoized render of clock values.
|
||||
/// ```
|
||||
/// tek::App::default().update_clock();
|
||||
/// ```
|
||||
pub fn update_clock (&self) {
|
||||
ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
||||
}
|
||||
|
||||
/// Set modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
dialog
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Set picked device in device pick dialog.
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().device_pick(0);
|
||||
/// ```
|
||||
pub fn device_pick (&mut self, index: usize) {
|
||||
self.dialog = Dialog::Device(index);
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Add device to current track.
|
||||
pub fn add_device (&mut self, index: usize) -> Usually<()> {
|
||||
match index {
|
||||
0 => {
|
||||
let name = self.jack.with_client(|c|c.name().to_string());
|
||||
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
|
||||
let track = self.track().expect("no active track");
|
||||
let port = format!("{}/Sampler", &track.name);
|
||||
let connect = Connect::exact(format!("{name}:{midi}"));
|
||||
let sampler = if let Ok(sampler) = Sampler::new(
|
||||
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
|
||||
) {
|
||||
self.dialog = Dialog::None;
|
||||
Device::Sampler(sampler)
|
||||
} else {
|
||||
self.dialog = Dialog::Message("Failed to add device.".into());
|
||||
return Err("failed to add device".into())
|
||||
};
|
||||
let track = self.track_mut().expect("no active track");
|
||||
track.devices.push(sampler);
|
||||
Ok(())
|
||||
},
|
||||
1 => {
|
||||
todo!();
|
||||
//Ok(())
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return reference to content browser if open.
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(tek::App::default().browser(), None);
|
||||
/// ```
|
||||
pub fn browser (&self) -> Option<&Browse> {
|
||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||
}
|
||||
|
||||
/// Is a MIDI editor currently focused?
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().editor_focused();
|
||||
/// ```
|
||||
pub fn editor_focused (&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Toggle MIDI editor.
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().toggle_editor(None);
|
||||
/// ```
|
||||
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
||||
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
||||
let value = value.unwrap_or_else(||!self.editor().is_some());
|
||||
if value {
|
||||
// Create new clip in pool when entering empty cell
|
||||
if let Selection::TrackClip { track, scene } = *self.selection()
|
||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||
&& let Some(slot) = scene.clips.get_mut(track)
|
||||
&& slot.is_none()
|
||||
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||
{
|
||||
let (_index, clip) = self.pool.add_new_clip();
|
||||
// autocolor: new clip colors from scene and track color
|
||||
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||
if let Some(editor) = &mut self.project.editor {
|
||||
editor.set_clip(Some(&clip));
|
||||
}
|
||||
*slot = Some(clip.clone());
|
||||
//Some(clip)
|
||||
} else {
|
||||
//None
|
||||
}
|
||||
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||
&& let Some(slot) = scene.clips.get_mut(track)
|
||||
&& let Some(clip) = slot.as_mut()
|
||||
{
|
||||
// Remove clip from arrangement when exiting empty clip editor
|
||||
let mut swapped = None;
|
||||
if clip.read().unwrap().count_midi_messages() == 0 {
|
||||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
self.pool.delete_clip(&clip.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_arc_str (&self, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
}
|
||||
fn get_u16 (&self, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
fn get_usize (&self, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => self.scenes().len(),
|
||||
Some(":track-count") => self.tracks().len(),
|
||||
Some(":device-kind") => self.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
fn get_bool (&self, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => self.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(self.dialog, Dialog::None),
|
||||
":focused/message" => matches!(self.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(self.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => self.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix),
|
||||
_ => return Err(format!("not bool: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_selection (&self, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => self.selection().select_scene(self.tracks().len()),
|
||||
":select/scene/next" => self.selection().select_scene_next(self.scenes().len()),
|
||||
":select/scene/prev" => self.selection().select_scene_prev(),
|
||||
":select/track" => self.selection().select_track(self.tracks().len()),
|
||||
":select/track/next" => self.selection().select_track_next(self.tracks().len()),
|
||||
":select/track/prev" => self.selection().select_track_prev(),
|
||||
_ => return Err(format!("not selection: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_color (&self, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(expr.tail().map_err(Into::into))?
|
||||
.ok_or(LanguageError::Domain("not red"))?;
|
||||
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::Domain("not green"))?;
|
||||
let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::Domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
|
||||
(None, _) => return Err(format!("not a color expression: {expr}").into()),
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return Err(format!("not a color: {sym}").into())
|
||||
})
|
||||
} else {
|
||||
return Err(format!("not a color: {:?}", src.src()?).into())
|
||||
}
|
||||
}
|
||||
fn get_opt_u7 (&self, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":editor/pitch" => Some((
|
||||
self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
_ => return Err(format!("unknown midi note: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_opt_u16 (&self, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
}
|
||||
fn get_opt_usize (&self, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":selected/scene" => self.selection().scene(),
|
||||
":selected/track" => self.selection().track(),
|
||||
_ => return Err(format!("unknown opt<usize>: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_clip (&self, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() =>
|
||||
self.scenes()[*scene].clips[*track].clone(),
|
||||
_ => return Err(format!("not a clip: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
||||
Ok(match &self.dialog {
|
||||
Dialog::Menu(index, items) => {
|
||||
let callback = items.0[*index].1.clone();
|
||||
callback(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
) -> Perhaps<U> {
|
||||
if *target == *value {
|
||||
Ok(None)
|
||||
} else {
|
||||
let mut value = value.clone();
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(value)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_bool <U> (
|
||||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||
) -> Perhaps<U> {
|
||||
let mut value = value.unwrap_or(!*target);
|
||||
if value == *target {
|
||||
Ok(None)
|
||||
} else {
|
||||
std::mem::swap(target, &mut value);
|
||||
Ok(Some(returned(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||
let entry = entry.expect("failed to read drectory entry");
|
||||
let meta = entry.metadata().expect("failed to read entry metadata");
|
||||
if meta.is_file() {
|
||||
files.push(entry.file_name());
|
||||
} else if meta.is_dir() {
|
||||
subdirs.push(entry.file_name());
|
||||
}
|
||||
(subdirs, files)
|
||||
});
|
||||
subdirs.sort();
|
||||
files.sort();
|
||||
Ok((subdirs, files))
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
|
||||
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let t0 = state.perf.get_t0();
|
||||
state.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = state.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &state.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = state.project.process_tracks(client, scope);
|
||||
state.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
fn tek_jack_event (state: &mut App, event: JackEvent) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
197
src/app/bind.rs
197
src/app/bind.rs
|
|
@ -1,197 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
tui_keys!(self: App, input {
|
||||
let commands = tek_commands_collect(self, input)?;
|
||||
let results = tek_commands_execute(self, commands)?;
|
||||
self.history.extend(results.into_iter());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
fn tek_commands_collect (app: &App, input: &TuiEvent)
|
||||
-> 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())
|
||||
&& let Some(bindings) = event_map.query(input) {
|
||||
for binding in bindings {
|
||||
for command in binding.commands.iter() {
|
||||
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
|
||||
commands.push(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
fn tek_commands_execute (app: &mut App, commands: Vec<AppCommand>)
|
||||
-> Usually<Vec<(AppCommand, Option<AppCommand>)>>
|
||||
{
|
||||
let mut history = vec![];
|
||||
for command in commands.into_iter() {
|
||||
let result = command.act(app);
|
||||
match result { Err(err) => { history.push((command, None)); return Err(err) }
|
||||
Ok(undo) => { history.push((command, undo)); } };
|
||||
}
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
/// Collection of input bindings.
|
||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
||||
///
|
||||
/// ```
|
||||
/// 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
|
||||
/// all command expressions bound to it by
|
||||
/// all loaded input layers.
|
||||
pub BTreeMap<E, Vec<Binding<C>>>
|
||||
);
|
||||
|
||||
/// A sequence of zero or more commands (e.g. [AppCommand]),
|
||||
/// optionally filtered by [Condition] to form layers.
|
||||
///
|
||||
/// ```
|
||||
/// //FIXME: Why does it overflow?
|
||||
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Binding<C> {
|
||||
pub commands: Arc<[C]>,
|
||||
pub condition: Option<Condition>,
|
||||
pub description: Option<Arc<str>>,
|
||||
pub source: Option<Arc<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Condition that must evaluate to true in order to enable an input layer.
|
||||
///
|
||||
/// ```
|
||||
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
|
||||
/// ```
|
||||
#[derive(Clone)] pub struct Condition(
|
||||
pub Arc<Box<dyn Fn()->bool + Send + Sync>>
|
||||
);
|
||||
|
||||
impl Bind<TuiEvent, Arc<str>> {
|
||||
pub fn load (lang: &impl Language) -> Usually<Self> {
|
||||
let mut map = Self::new();
|
||||
lang.each(|item|if item.expr().head() == Ok(Some("see")) {
|
||||
// TODO
|
||||
Ok(())
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() {
|
||||
map.add(TuiEvent(event), 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())
|
||||
})?;
|
||||
Ok(map)
|
||||
}
|
||||
}
|
||||
|
||||
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||
impl<E, C> Default for Bind<E, C> {
|
||||
fn default () -> Self { Self(Default::default()) }
|
||||
}
|
||||
|
||||
impl<C: Default> Default for Binding<C> {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
commands: Default::default(),
|
||||
condition: Default::default(),
|
||||
description: Default::default(),
|
||||
source: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Clone + Ord, C> Bind<E, C> {
|
||||
/// Create a new event map
|
||||
pub fn new () -> Self {
|
||||
Default::default()
|
||||
}
|
||||
/// Add a binding to an owned event map.
|
||||
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
|
||||
self.add(event, binding);
|
||||
self
|
||||
}
|
||||
/// Add a binding to an event map.
|
||||
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
|
||||
if !self.0.contains_key(&event) {
|
||||
self.0.insert(event.clone(), Default::default());
|
||||
}
|
||||
self.0.get_mut(&event).unwrap().push(binding);
|
||||
self
|
||||
}
|
||||
/// Return the binding(s) that correspond to an event.
|
||||
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
|
||||
self.0.get(event).map(|x|x.as_slice())
|
||||
}
|
||||
/// Return the first binding that corresponds to an event, considering conditions.
|
||||
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
|
||||
self.query(event)
|
||||
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||
|
||||
impl_default!(AppCommand: Self::Nop);
|
||||
|
||||
def_command!(AppCommand: |app: App| {
|
||||
Nop => Ok(None),
|
||||
Cancel => todo!(), // TODO delegate:
|
||||
Confirm => app.confirm(),
|
||||
Inc { axis: ControlAxis } => app.inc(axis),
|
||||
Dec { axis: ControlAxis } => app.dec(axis),
|
||||
SetDialog { dialog: Dialog } => {
|
||||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||
},
|
||||
});
|
||||
|
||||
impl<'a> Namespace<'a, AppCommand> for App {
|
||||
symbols!('a |app| -> AppCommand {
|
||||
"x/inc" => AppCommand::Inc { axis: ControlAxis::X },
|
||||
"x/dec" => AppCommand::Dec { axis: ControlAxis::X },
|
||||
"y/inc" => AppCommand::Inc { axis: ControlAxis::Y },
|
||||
"y/dec" => AppCommand::Dec { axis: ControlAxis::Y },
|
||||
"confirm" => AppCommand::Confirm,
|
||||
"cancel" => AppCommand::Cancel,
|
||||
});
|
||||
}
|
||||
|
||||
/// A control axis.
|
||||
///
|
||||
/// ```
|
||||
/// let axis = tek::ControlAxis::X;
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||
X, Y, Z, I
|
||||
}
|
||||
|
||||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let config = tek::Config::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // Some dizzle.
|
||||
/// // What indentation to use here lol?
|
||||
/// let source = stringify!((mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
/// (bsp/n :ports/in
|
||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
||||
/// (fill :dialog/menu)))))))));
|
||||
/// // Add this definition to the config and try to load it.
|
||||
/// // A "mode" is basically a state machine
|
||||
/// // with associated input and output definitions.
|
||||
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Config {
|
||||
/// XDG base directories of running user.
|
||||
pub dirs: BaseDirectories,
|
||||
/// Active collection of interaction modes.
|
||||
pub modes: Modes,
|
||||
/// Active collection of event bindings.
|
||||
pub binds: Binds,
|
||||
/// Active collection of view definitions.
|
||||
pub views: Views,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
const CONFIG_DIR: &'static str = "tek";
|
||||
const CONFIG_SUB: &'static str = "v0";
|
||||
const CONFIG: &'static str = "tek.edn";
|
||||
const DEFAULTS: &'static str = include_str!("../tek.edn");
|
||||
|
||||
pub fn watch <T> (callback: impl FnOnce(Self)->T) -> Usually<T> {
|
||||
let config = Self::init_new(None)?;
|
||||
let watcher = notify_debouncer_mini::new_debouncer(Duration::from_millis(500), |res| {
|
||||
println!("{res:?}");
|
||||
})?;
|
||||
let result = callback(config);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn init_new (dirs: Option<BaseDirectories>) -> Usually<Self> {
|
||||
let mut config = Self::new(None);
|
||||
config.init()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Create a new app configuration from a set of XDG base directories,
|
||||
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
|
||||
let dirs = dirs.unwrap_or_else(default);
|
||||
Self { dirs, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Write initial contents of configuration.
|
||||
pub fn init (&mut self) -> Usually<()> {
|
||||
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
|
||||
cfgs.add(&dsl)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write initial contents of a configuration file.
|
||||
pub fn init_one (
|
||||
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||
) -> Usually<()> {
|
||||
if self.dirs.find_config_file(path).is_none() {
|
||||
//println!("Creating {path:?}");
|
||||
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
|
||||
}
|
||||
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||
//println!("Loading {path:?}");
|
||||
let src = std::fs::read_to_string(&path)?;
|
||||
src.as_str().each(move|item|each(self, item))?;
|
||||
} else {
|
||||
return Err(format!("{path}: not found").into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Add statements to configuration from [Dsl] source.
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
|
||||
dsl.each(|item|self.add_one(item))?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn add_one (&self, item: impl Language) -> Usually<()> {
|
||||
if let Some(expr) = item.expr()? {
|
||||
let head = expr.head()?;
|
||||
let tail = expr.tail()?;
|
||||
let name = tail.head()?;
|
||||
let body = tail.tail()?;
|
||||
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||
match head {
|
||||
Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?,
|
||||
Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?,
|
||||
Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?,
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||
self.modes.get(mode)
|
||||
}
|
||||
}
|
||||
|
||||
mod views; pub use self::views::*;
|
||||
mod modes; pub use self::modes::*;
|
||||
mod mode; pub use self::mode::*;
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<D>,
|
||||
pub info: Vec<D>,
|
||||
pub view: Vec<D>,
|
||||
pub keys: Vec<D>,
|
||||
pub modes: Modes,
|
||||
}
|
||||
|
||||
impl Mode<Arc<str>> {
|
||||
/// Add a definition to the mode.
|
||||
///
|
||||
/// Supported definitions:
|
||||
///
|
||||
/// - (name ...) -> name
|
||||
/// - (info ...) -> description
|
||||
/// - (keys ...) -> key bindings
|
||||
/// - (mode ...) -> submode
|
||||
/// - ... -> view
|
||||
///
|
||||
/// ```
|
||||
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||
/// mode.add("(name hello)").unwrap();
|
||||
/// ```
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
|
||||
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
match head {
|
||||
"name" => self.add_name(tail)?,
|
||||
"info" => self.add_info(tail)?,
|
||||
"keys" => self.add_keys(tail)?,
|
||||
"mode" => self.add_mode(tail)?,
|
||||
_ => self.add_view(tail)?,
|
||||
};
|
||||
} else if let Ok(Some(word)) = dsl.word() {
|
||||
self.add_view(word);
|
||||
} else {
|
||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||
})
|
||||
|
||||
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
|
||||
//.word(|word|self.add_view(word))
|
||||
//.expr(|expr|expr.head(|head|{
|
||||
////println!("Mode::add: {head} {:?}", expr.tail());
|
||||
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
//match head {
|
||||
//"name" => self.add_name(tail),
|
||||
//"info" => self.add_info(tail),
|
||||
//"keys" => self.add_keys(tail)?,
|
||||
//"mode" => self.add_mode(tail)?,
|
||||
//_ => self.add_view(tail),
|
||||
//};
|
||||
//}))
|
||||
}
|
||||
|
||||
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
|
||||
}
|
||||
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
|
||||
}
|
||||
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
|
||||
}
|
||||
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
|
||||
}
|
||||
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(if let Some(id) = dsl.head()? {
|
||||
self.modes.add(&id, &dsl.tail())?;
|
||||
} else {
|
||||
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of UI modes.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Modes(
|
||||
Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>
|
||||
);
|
||||
|
||||
impl Modes {
|
||||
pub fn add (&self, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||
let mut mode = Mode::default();
|
||||
body.each(|item|mode.add(item))?;
|
||||
self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||
Ok(())
|
||||
}
|
||||
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||
self.0.read().unwrap().get(name.as_ref()).cloned()
|
||||
}
|
||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode<Arc<str>>)->T) {
|
||||
for (k, v) in self.0.read().unwrap().iter() {
|
||||
let _ = ator(k.as_ref(), v.as_ref());
|
||||
}
|
||||
}
|
||||
pub fn len (&self) -> usize {
|
||||
self.0.read().unwrap().len()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of custom view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
|
||||
/// Load custom view definition.
|
||||
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(())
|
||||
}
|
||||
750
src/app/draw.rs
750
src/app/draw.rs
|
|
@ -1,750 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl View<Tui> for App {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
thunk(|to: &mut Tui|{
|
||||
let xywh = to.area().into();
|
||||
if let Some(e) = self.error.read().unwrap().as_ref() {
|
||||
to.show(area(xywh, e.as_ref()))?;
|
||||
}
|
||||
for (index, dsl) in self.mode.view.iter().enumerate() {
|
||||
if let Err(e) = self.interpret(to, dsl) {
|
||||
*self.error.write().unwrap() = Some(format!(
|
||||
"mode {:?} view #{index}: {e}", &self.mode.name,
|
||||
).into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Some(xywh))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Interpret<Tui, Option<XYWH<u16>>> for App {
|
||||
fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn<u16> {
|
||||
tek_draw_expr(self, to, lang)
|
||||
}
|
||||
fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn<u16> {
|
||||
tek_draw_word(self, to, lang)
|
||||
}
|
||||
}
|
||||
|
||||
fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn<u16> {
|
||||
Ok(Some(if let Some(area) = eval_view(state, to, lang)? {
|
||||
area
|
||||
} else if let Some(area) = eval_view_tui(state, to, lang)? {
|
||||
area
|
||||
} else {
|
||||
return Err(format!("App::interpret_expr: unexpected: {lang:?}").into())
|
||||
}))
|
||||
}
|
||||
|
||||
fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn<u16> {
|
||||
let mut frags = dsl.src()?.unwrap().split("/");
|
||||
match frags.next() {
|
||||
//Some(":logo") => view_logo().draw(to),
|
||||
Some(":meters") => draw_meter_section(to, frags),
|
||||
Some(":tracks") => draw_tracks(to, frags, state),
|
||||
Some(":scenes") => draw_scenes(to, frags),
|
||||
Some(":dialog") => draw_dialog(to, frags, state, dsl),
|
||||
Some(":templates") => draw_templates(to, frags, state),
|
||||
Some(":sessions") => view_sessions().draw(to),
|
||||
Some(":browse/title") => view_browse_title(state).draw(to),
|
||||
Some(":device") => view_device(state).draw(to),
|
||||
Some(":status") => "TODO: Status Bar".exact_h(1).draw(to),
|
||||
Some(":editor") => "TODO Editor".draw(to),
|
||||
Some(":transport") => view_transport(true, "", "", "").draw(to),
|
||||
Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to),
|
||||
Some(_) => {
|
||||
let views = state.config.views.read().unwrap();
|
||||
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
|
||||
let dsl = dsl.clone();
|
||||
std::mem::drop(views);
|
||||
state.interpret(to, &dsl)
|
||||
} else {
|
||||
unimplemented!("{dsl:?}");
|
||||
}
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn<u16> {
|
||||
match frags.next() {
|
||||
Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to),
|
||||
Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to),
|
||||
_ => panic!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn<u16> {
|
||||
match frags.next() {
|
||||
None => "TODO tracks".draw(to),
|
||||
Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), full_w(align_w("Track Names")))),
|
||||
Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to),
|
||||
Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to),
|
||||
Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to),
|
||||
_ => panic!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn<u16> {
|
||||
match frags.next() {
|
||||
None => "TODO Scenes".draw(to),
|
||||
Some(":scenes/names") => "TODO Scene Names".draw(to),
|
||||
_ => panic!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_dialog (
|
||||
to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn<u16> {
|
||||
match frags.next() {
|
||||
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
|
||||
let items = items.clone();
|
||||
let selected = selected;
|
||||
Some(thunk(move|to: &mut Tui|{
|
||||
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
|
||||
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
|
||||
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
|
||||
fg_bg(f, b, item.full_w().align_w().exact_h(2))
|
||||
.push_y((4 * index) as u16).draw(to)?;
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
}).full_wh())
|
||||
} else {
|
||||
None
|
||||
}.draw(to),
|
||||
_ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Drawn<u16> {
|
||||
let height = (state.config.modes.len() * 2) as u16;
|
||||
thunk(move |to: &mut Tui|{
|
||||
let mut index = 0;
|
||||
state.config.modes.for_each(|id, profile| {
|
||||
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = fg(fg1, name).align_w().full_w();
|
||||
let field_id = fg(fg2, id).align_e().full_w();
|
||||
let field_info = info.align_w().full_w();
|
||||
let _ = bg(b, south(above(field_name, field_id), field_info))
|
||||
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
|
||||
index += 1;
|
||||
});
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(30).exact_h(height).draw(to)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = "";
|
||||
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// ```
|
||||
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
bg(Black, east!(above(
|
||||
button_play_pause(play, false).align_w(),
|
||||
east!(
|
||||
field_h(theme, "BPM", bpm),
|
||||
field_h(theme, "Beat", beat),
|
||||
field_h(theme, "Time", time),
|
||||
).align_e().full_wh()
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = "";
|
||||
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
|
||||
/// ```
|
||||
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
let sr = field_h(theme, "SR", sr);
|
||||
let buf = field_h(theme, "Buf", buf);
|
||||
let lat = field_h(theme, "Lat", lat);
|
||||
bg(Black, east!(above(
|
||||
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
|
||||
east!(sr, buf, lat).align_e().full_wh(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_play_pause(true, true);
|
||||
/// let _ = tek::button_play_pause(true, false);
|
||||
/// let _ = tek::button_play_pause(false, true);
|
||||
/// let _ = tek::button_play_pause(false, false);
|
||||
/// ```
|
||||
pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw<Tui> {
|
||||
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||
either(compact,
|
||||
thunk(move|to: &mut Tui|either(playing,
|
||||
fg(Rgb(0, 255, 0), " PLAYING "),
|
||||
fg(Rgb(255, 128, 0), " STOPPED "),
|
||||
).exact_w(9).draw(to)),
|
||||
thunk(move|to: &mut Tui|either(playing,
|
||||
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)),
|
||||
).exact_w(5).draw(to)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] pub fn view_track_row_section (
|
||||
_theme: ItemTheme,
|
||||
button: impl Draw<Tui>,
|
||||
button_add: impl Draw<Tui>,
|
||||
content: impl Draw<Tui>,
|
||||
) -> impl Draw<Tui> {
|
||||
west(
|
||||
button_add.align_nw().exact_w(4).full_h(),
|
||||
east(
|
||||
button.align_nw().full_h().exact_w(20),
|
||||
content.align_c().full_wh()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
|
||||
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, bg(c, ()).exact_wh(w, 1))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
when(sample.is_some(), thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
east!(
|
||||
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||
field_h(theme, "Start", format!("{:<8}", sample.start)),
|
||||
field_h(theme, "End", format!("{:<8}", sample.end)),
|
||||
field_h(theme, "Trans", "0"),
|
||||
field_h(theme, "Gain", format!("{}", sample.gain)),
|
||||
).draw(to)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let a = thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
south!(
|
||||
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
|
||||
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
|
||||
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
|
||||
field_h(theme, "Trans ", "0") .align_w().full_w(),
|
||||
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
|
||||
).exact_w(20).draw(to)
|
||||
});
|
||||
|
||||
let b = thunk(|to: &mut Tui|fg(Red, south!(
|
||||
bold(true, "× No sample."),
|
||||
"[r] record",
|
||||
"[Shift-F9] import",
|
||||
)).draw(to));
|
||||
|
||||
either(sample.is_some(), a, b)
|
||||
}
|
||||
|
||||
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||||
bold(true, fg(g(224), sample
|
||||
.map(|sample|{
|
||||
let sample = sample.read().unwrap();
|
||||
format!("Sample {}-{}", sample.start, sample.end)
|
||||
})
|
||||
.unwrap_or_else(||"No sample".to_string())))
|
||||
}
|
||||
|
||||
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
|
||||
}
|
||||
|
||||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||
-> impl Draw<Tui> + use<'a, T>
|
||||
{
|
||||
let ins = ports.len() as u16;
|
||||
let frame = Outer(true, Style::default().fg(g(96)));
|
||||
let iter = move||ports.iter();
|
||||
let names = iter_south(iter, move|port, index|format!(" {index} {}", port.port_name()).align_w().full_h());
|
||||
let field = field_v(theme, title, names);
|
||||
border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
|
||||
}
|
||||
|
||||
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
|
||||
iter(items,
|
||||
move|(_index, name, connections, y, y2): Item<'a>, _| south(
|
||||
bold(true, fg_bg(fg, bg, east(" ", name).align_w())).full_h(),
|
||||
iter(||connections.iter(), move|connect: &'a Connect, index|{
|
||||
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
|
||||
})
|
||||
).exact_h((y2 - y) as u16).push_y(y as u16))
|
||||
}
|
||||
|
||||
pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> (
|
||||
scenes: impl Fn()->S,
|
||||
tracks: impl TracksSizes<'a>,
|
||||
select: &Selection,
|
||||
editor: Option<&MidiEditor>,
|
||||
size: &Sizer,
|
||||
editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh();
|
||||
let tracks = iter_once(tracks, move|(track_index, track, _, _), _| {
|
||||
let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| {
|
||||
let (name, theme): (Arc<str>, ItemTheme) = scene_name_theme(scene, track_index);
|
||||
let f = theme.lightest.term;
|
||||
let (b, o) = scene_bg(theme, select, track_index, scene_index);
|
||||
let w = scene_w(track, select, track_index, editor);
|
||||
let y = scene_y(select, scene_index, editor);
|
||||
let is_selected = scene_sel(select, track_index, scene_index, editing);
|
||||
below(
|
||||
Outer(true, Style::default().fg(o)).full_wh(),
|
||||
below(
|
||||
below(
|
||||
fg_bg(o, b, "".full_wh()),
|
||||
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
|
||||
),
|
||||
when(is_selected, editor.map(|e|e.view())).full_wh()
|
||||
).full_wh()
|
||||
).exact_wh(w, y)
|
||||
});
|
||||
scenes.full_h().exact_w(track.width as u16)
|
||||
});
|
||||
|
||||
return size.of(above(status, tracks).full_wh());
|
||||
|
||||
fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
||||
if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
}
|
||||
}
|
||||
|
||||
fn scene_bg (
|
||||
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
|
||||
) -> (Color, Color) {
|
||||
let mut outline = theme.base.term;
|
||||
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
|
||||
outline = theme.lighter.term;
|
||||
theme.light.term
|
||||
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
|
||||
outline = theme.darkest.term;
|
||||
theme.base.term
|
||||
} else {
|
||||
theme.dark.term
|
||||
}, outline)
|
||||
}
|
||||
|
||||
fn scene_w (
|
||||
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.track() == Some(track_index) && let Some(editor) = editor {
|
||||
(editor.size.w() as usize).max(24).max(track.width) as u16
|
||||
} else {
|
||||
track.width as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn scene_y (
|
||||
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
|
||||
) -> u16 {
|
||||
if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
||||
editor.size.h().max(12)
|
||||
} else {
|
||||
H_SCENE as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
|
||||
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_track_names (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
track_count: usize,
|
||||
scene_count: usize,
|
||||
selected: &Selection,
|
||||
) -> impl Draw<Tui> {
|
||||
let button = south(
|
||||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||||
let button_2 = south(
|
||||
button_2("T", "+", false),
|
||||
button_2("S", "+", false));
|
||||
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
|
||||
thunk(|to: &mut Tui|{
|
||||
for (index, track, x1, _x2) in tracks {
|
||||
let b = if selected.track() == Some(index) {
|
||||
track.color.light.term
|
||||
} else {
|
||||
track.color.base.term
|
||||
};
|
||||
bg(b, south(east(
|
||||
format!("·t{index:02} "),
|
||||
fg(Rgb(255, 255, 255), bold(true, &track.name))
|
||||
).align_nw().full_w(), ""))
|
||||
.exact_w(track_width(index, track))
|
||||
.push_x(x1 as u16)
|
||||
.draw(to)?;
|
||||
}
|
||||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||||
}).exact_h(2)))
|
||||
}
|
||||
|
||||
pub fn view_track_outputs (
|
||||
theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator<Item = &MidiOutput>,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
south(button_2("o", "utput", false).align_w().full_w(),
|
||||
thunk(|to: &mut Tui|{
|
||||
for port in midi_outs {
|
||||
let _ = port.port_name().align_w().full_w().draw(to)?;
|
||||
}
|
||||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||||
})),
|
||||
button_2("O", "+", false),
|
||||
bg(theme.darker.term, thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
let f = Rgb(255, 255, 255);
|
||||
let b = track.color.dark.term;
|
||||
let iter = ||track.sequencer.midi_outs.iter();
|
||||
let draw = |port: &MidiOutput, _|fg(f, bg(b,
|
||||
format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1));
|
||||
iter_south(iter, draw).full_h().align_nw()
|
||||
.exact_w(track_width(index, track))
|
||||
.draw(to)?;
|
||||
}
|
||||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||||
}).align_w()))
|
||||
}
|
||||
|
||||
pub fn view_track_inputs (
|
||||
theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||||
bg(theme.darker.term, thunk(move|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
south(
|
||||
bg(track.color.base.term,
|
||||
east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
||||
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
||||
).align_w().full_w()),
|
||||
iter_south(||track.sequencer.midi_ins.iter(),
|
||||
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
|
||||
format!("·i{index:02} {}", port.port_name()).align_w().full_w()))
|
||||
).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?;
|
||||
}
|
||||
Ok(Some(XYWH(0, 0, 0, 0)))
|
||||
}).align_w()))
|
||||
}
|
||||
|
||||
pub fn view_scenes_names (
|
||||
scenes: impl ScenesSizes<'_>,
|
||||
select: &Selection,
|
||||
editor: Option<&MidiEditor>,
|
||||
editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
thunk(move |to: &mut Tui|{
|
||||
for (index, scene, ..) in scenes {
|
||||
view_scene_name(select, editor, index, scene, editing).draw(to)?;
|
||||
}
|
||||
Ok(Some(XYWH(1, 1, 1, 1)))
|
||||
}).exact_w(20)
|
||||
}
|
||||
|
||||
pub fn view_scene_name (
|
||||
select: &Selection,
|
||||
editor: Option<&MidiEditor>,
|
||||
index: usize,
|
||||
scene: &Scene,
|
||||
editing: bool
|
||||
) -> impl Draw<Tui> {
|
||||
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
|
||||
7
|
||||
} else {
|
||||
H_SCENE as u16
|
||||
};
|
||||
let a = east(format!("·s{index:02} "),
|
||||
fg(g(255), bold(true, &scene.name))).align_w().full_w();
|
||||
let b = when(select.scene() == Some(index) && editing, south(
|
||||
editor.as_ref().map(|e|e.clip_status()),
|
||||
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
|
||||
let c = if select.scene() == Some(index) {
|
||||
scene.color.light.term
|
||||
} else {
|
||||
scene.color.base.term
|
||||
};
|
||||
bg(c, south(a, b).align_nw()).exact_wh(20, h)
|
||||
}
|
||||
|
||||
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
|
||||
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
|
||||
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
|
||||
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
|
||||
pub fn view_track_per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> {
|
||||
iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{
|
||||
fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track)
|
||||
).exact_w((x2 - x1) as u16)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_per_track () -> impl Draw<Tui> {}
|
||||
|
||||
pub fn view_per_track_top () -> impl Draw<Tui> {}
|
||||
|
||||
pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw<Tui> {
|
||||
let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1);
|
||||
let title_2 = button_2("I", "+", false).exact_wh(4, 1);
|
||||
east(title_1, west(title_2, thunk(move|to: &mut Tui|{
|
||||
for (_index, track, x1, _x2) in tracks {
|
||||
let _ = south(
|
||||
bg(track.color.dark.term, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
||||
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
||||
).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
|
||||
thunk(move |to: &mut Tui|{
|
||||
for (index, port) in midi_ins.iter().enumerate() {
|
||||
let _ = east(
|
||||
east(
|
||||
" ● ",
|
||||
bold(true, fg(Rgb(255,255,255), port.port_name()))
|
||||
).align_w().exact_w(20),
|
||||
west(
|
||||
().exact_w(4),
|
||||
bg(track.color.darker.term, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
||||
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
||||
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
||||
).exact_w(track.width as u16).align_w())
|
||||
)
|
||||
).push_x(index as u16 * 10).exact_h(1).draw(to)?;
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
).draw(to)?;
|
||||
}
|
||||
todo!()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn view_outputs (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
midi_outs: &[MidiOutput],
|
||||
height: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
|
||||
let list = south(
|
||||
button_3(
|
||||
"o", "utput", format!("{}", midi_outs.len()), false
|
||||
).align_w().full_w().exact_h(1),
|
||||
thunk(|to: &mut Tui|{
|
||||
for (_index, port) in midi_outs.iter().enumerate() {
|
||||
east(
|
||||
east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(),
|
||||
format!("{}/{} ",
|
||||
port.port().get_connections().len(),
|
||||
port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?;
|
||||
for (index, conn) in port.connections.iter().enumerate() {
|
||||
format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?;
|
||||
}
|
||||
}
|
||||
todo!();
|
||||
}).align_nw().full_wh().exact_h(height - 1)
|
||||
);
|
||||
|
||||
view_track_row_section(theme, list, button_2("O", "+", false),
|
||||
bg(theme.darker.term, thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
let _ = thunk(|to: &mut Tui|{
|
||||
east(
|
||||
either(true, fg(Green, "play "), "play "),
|
||||
either(false, fg(Yellow, "solo "), "solo "),
|
||||
).align_w().exact_h(1).draw(to)?;
|
||||
for (_index, port) in midi_outs.iter().enumerate() {
|
||||
east(
|
||||
either(true, fg(Green, " ● "), " · "),
|
||||
either(false, fg(Yellow, " ● "), " · "),
|
||||
).align_w().exact_h(1).draw(to)?;
|
||||
for (_index, _conn) in port.connections.iter().enumerate() {
|
||||
"".full_w().exact_h(1).draw(to)?;
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
}).exact_w(track_width(index, track)).draw(to)?;
|
||||
}
|
||||
todo!()
|
||||
}).align_w().full_w())).exact_h(height)
|
||||
}
|
||||
|
||||
pub fn view_track_devices (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
track: Option<&Track>,
|
||||
h: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||
button_2("D", "+", false),
|
||||
iter_once(tracks, move|(_, track, _x1, _x2), index|bg(
|
||||
track.color.dark.term,
|
||||
iter_south(move||0..h,
|
||||
|_, _index|fg_bg(
|
||||
ItemTheme::G[32].lightest.term,
|
||||
ItemTheme::G[32].dark.term,
|
||||
format!(" · {}", "--").align_nw()
|
||||
).exact_wh(track.width as u16, 2)
|
||||
).align_nw()).exact_wh(
|
||||
Some(track_width(index, track)),
|
||||
Some(h + 1),
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y())
|
||||
}
|
||||
|
||||
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
bg(Reset, iter_east(tracks,
|
||||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||
fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track)
|
||||
).exact_w((x2 - x1) as u16)
|
||||
}).align_x())
|
||||
}
|
||||
|
||||
pub fn field_h <T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn field_v <T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn view_sessions () -> impl Draw<Tui> {
|
||||
let h = 6;
|
||||
let w = Some(30);
|
||||
let f = Rgb(224, 192, 128);
|
||||
thunk(move |to: &mut Tui|{
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
let y = (2 * index) as u16;
|
||||
let h = 2;
|
||||
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(w).exact_h(h)
|
||||
}
|
||||
|
||||
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
|
||||
field_v(ItemTheme::default(),
|
||||
match state.dialog.browser_target().unwrap() {
|
||||
BrowseTarget::SaveProject => "Save project:",
|
||||
BrowseTarget::LoadProject => "Load project:",
|
||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||
}, fg(g(96), x_repeat("🭻")).exact_h(1)
|
||||
).align_w().full_w()
|
||||
}
|
||||
|
||||
pub fn view_device (state: &App) -> impl Draw<Tui> {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(bold(true, "Add device"), iter_south(
|
||||
move||device_kinds().iter(),
|
||||
move|_label: &&'static str, i|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl_has!(Sizer: |self: App|self.size);
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
pub trait $Type<'a> =
|
||||
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
|
||||
}
|
||||
}
|
||||
|
||||
def_sizes_iter!(InputsSizes => MidiInput);
|
||||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
/// Increment track width.
|
||||
fn width_inc (&mut self);
|
||||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||
fn width_dec (&mut self);
|
||||
}
|
||||
32
src/deps.rs
32
src/deps.rs
|
|
@ -1,32 +0,0 @@
|
|||
#[allow(unused)]
|
||||
pub(crate) use ::{
|
||||
std::{
|
||||
cmp::Ord,
|
||||
collections::BTreeMap,
|
||||
error::Error,
|
||||
ffi::OsString,
|
||||
fmt::{Write, Debug, Formatter},
|
||||
fs::File,
|
||||
ops::{Add, Sub, Mul, Div, Rem},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||||
time::Duration,
|
||||
thread::{spawn, JoinHandle},
|
||||
},
|
||||
xdg::{
|
||||
BaseDirectories,
|
||||
},
|
||||
tengri::{
|
||||
*,
|
||||
lang::*,
|
||||
midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*},
|
||||
crossterm::event::{Event, KeyEvent},
|
||||
ratatui::{
|
||||
self,
|
||||
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||||
widgets::{Widget, canvas::{Canvas, Line}},
|
||||
},
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "cli")]
|
||||
pub(crate) use ::clap::{self, Parser, Subcommand};
|
||||
144
src/device.rs
144
src/device.rs
|
|
@ -1,144 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
def_command!(DeviceCommand: |device: Device| {});
|
||||
|
||||
impl Device {
|
||||
pub fn name (&self) -> &str {
|
||||
match self {
|
||||
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
pub fn midi_ins (&self) -> &[MidiInput] {
|
||||
match self {
|
||||
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||||
match self {
|
||||
Self::Sampler(_) => &[],
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||
match self {
|
||||
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||
match self {
|
||||
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A device that can be plugged into the chain.
|
||||
///
|
||||
/// ```
|
||||
/// let device = tek::Device::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub enum Device {
|
||||
#[default]
|
||||
Bypass,
|
||||
Mute,
|
||||
#[cfg(feature = "sampler")]
|
||||
Sampler(Sampler),
|
||||
#[cfg(feature = "lv2")] // TODO
|
||||
Lv2(Lv2),
|
||||
#[cfg(feature = "vst2")] // TODO
|
||||
Vst2,
|
||||
#[cfg(feature = "vst3")] // TODO
|
||||
Vst3,
|
||||
#[cfg(feature = "clap")] // TODO
|
||||
Clap,
|
||||
#[cfg(feature = "sf2")] // TODO
|
||||
Sf2,
|
||||
}
|
||||
|
||||
/// Some sort of wrapper?
|
||||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||
|
||||
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||
use Device::*;
|
||||
match self.0 {
|
||||
Mute => { Control::Continue },
|
||||
Bypass => { /*TODO*/ Control::Continue },
|
||||
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||||
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||||
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||||
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
|
||||
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
|
||||
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||||
}
|
||||
});
|
||||
|
||||
pub fn device_kinds () -> &'static [&'static str] {
|
||||
&[
|
||||
#[cfg(feature = "sampler")] "Sampler",
|
||||
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||||
]
|
||||
}
|
||||
|
||||
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub use ::tengri::sing::*;
|
||||
pub mod arrange; pub use self::arrange::*;
|
||||
pub mod browse; pub use self::browse::*;
|
||||
pub mod clock; pub use self::clock::*;
|
||||
pub mod dialog; pub use self::dialog::*;
|
||||
pub mod editor; pub use self::editor::*;
|
||||
pub mod menu; pub use self::menu::*;
|
||||
pub mod meter; pub use self::meter::*;
|
||||
pub mod mix; pub use self::mix::*;
|
||||
pub mod sampler; pub use self::sampler::*;
|
||||
pub mod sequence; pub use self::sequence::*;
|
||||
|
||||
#[cfg(feature = "plugin")] pub mod plugin;
|
||||
#[cfg(feature = "plugin")] pub use self::plugin::*;
|
||||
|
||||
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_in: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_in: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
pub struct Junction<T: JackPort>(T);
|
||||
|
||||
impl<T: JackPort> View<Tui> for Junction<T> {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
T::KIND
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,12 @@ use crate::*;
|
|||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Arrangement {
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// FIXME a render of the project arrangement, redrawn on update.
|
||||
/// TODO rename to "render_cache" or smth
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{*, clock::*, sequence::*, sampler::*};
|
||||
use crate::*;
|
||||
|
||||
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||||
//("begin" [] Some(Self::Begin))
|
||||
|
|
@ -246,7 +246,7 @@ impl<'a> PoolView<'a> {
|
|||
move|clip: Arc<RwLock<MidiClip>>, i: usize|{
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let _item_offset = i as u16 * item_height;
|
||||
let selected = i == pool.clip_index();
|
||||
let b = if selected { color.light.term } else { color.base.term };
|
||||
let f = color.lightest.term;
|
||||
|
|
@ -317,7 +317,7 @@ impl Browse {
|
|||
fn tui (&self) -> impl Draw<Tui> {
|
||||
iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w())
|
||||
}
|
||||
fn tui_entries (&self) -> EntriesIterator<Tui> {
|
||||
fn tui_entries (&self) -> EntriesIterator<'_, Tui> {
|
||||
EntriesIterator {
|
||||
offset: 0,
|
||||
index: 0,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! rewrite {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::*;
|
||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||
use ::std::sync::Arc;
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
use crate::*;
|
||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::*;
|
||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||
use ::std::sync::Arc;
|
||||
use ::atomic_float::AtomicF64;
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{*, browse::*, device::*, menu::*};
|
||||
use crate::{*, device::*};
|
||||
|
||||
/// Various possible dialog modes.
|
||||
///
|
||||
|
|
@ -55,17 +55,23 @@ impl Dialog {
|
|||
/// ```
|
||||
pub fn welcome () -> Self {
|
||||
Self::Menu(1, MenuItems([
|
||||
|
||||
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||
|
||||
MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({
|
||||
app.dialog = Dialog::None;
|
||||
app.mode = app.config.modes.get(":arranger").unwrap();
|
||||
app.mode = app.config.modes.get(":arranger");
|
||||
})))),
|
||||
|
||||
MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||
|
||||
MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({
|
||||
()
|
||||
})))),
|
||||
|
||||
].into()))
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_selected();
|
||||
|
|
@ -121,3 +127,19 @@ impl Dialog {
|
|||
/// FIXME: implement
|
||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||
}
|
||||
|
||||
/// Increment a wrapping counter.
|
||||
pub const fn wrap_inc (index: usize, count: usize) -> usize {
|
||||
if count > 0 { (index + 1) % count } else { 0 }
|
||||
}
|
||||
|
||||
/// Decrement a wrapping counter.
|
||||
pub const fn wrap_dec (index: usize, count: usize) -> usize {
|
||||
if count > 0 {
|
||||
let a = index.overflowing_sub(1).0;
|
||||
let b = count.saturating_sub(1);
|
||||
if a < b { a } else { b }
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use crate::*;
|
||||
|
||||
/// Contains state for viewing and editing a clip.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unused)]
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{*, device::*, browse::*, mix::*};
|
||||
|
||||
#![allow(unused)]
|
||||
use crate::*;
|
||||
pub(crate) use symphonia::{
|
||||
default::get_codecs,
|
||||
core::{//errors::Error as SymphoniaError,
|
||||
|
|
@ -8,11 +8,6 @@ pub(crate) use symphonia::{
|
|||
},
|
||||
};
|
||||
|
||||
mod voice; pub use self::voice::*;
|
||||
mod sample; pub use self::sample::*;
|
||||
mod sample_add; pub use self::sample_add::*;
|
||||
mod sample_kit; pub use self::sample_kit::*;
|
||||
|
||||
/// Plays [Voice]s from [Sample]s.
|
||||
///
|
||||
/// ```
|
||||
|
|
@ -355,10 +350,6 @@ fn draw_sample (
|
|||
Ok(label1.len() + label2.len() + 4)
|
||||
}
|
||||
|
||||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
def_command!(SamplerCommand: |sampler: Sampler| {
|
||||
RecordToggle { slot: usize } => {
|
||||
let slot = *slot;
|
||||
|
|
@ -405,3 +396,324 @@ def_command!(SamplerCommand: |sampler: Sampler| {
|
|||
//Ok(None)
|
||||
},
|
||||
});
|
||||
|
||||
/// A currently playing instance of a sample.
|
||||
#[derive(Default, Debug, Clone)] pub struct Voice {
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub after: usize,
|
||||
pub position: usize,
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
impl Iterator for Voice {
|
||||
type Item = [f32;2];
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
if self.after > 0 {
|
||||
self.after -= 1;
|
||||
return Some([0.0, 0.0])
|
||||
}
|
||||
let sample = self.sample.read().unwrap();
|
||||
if self.position < sample.end {
|
||||
let position = self.position;
|
||||
self.position += 1;
|
||||
return sample.channels[0].get(position).map(|_amplitude|[
|
||||
sample.channels[0][position] * self.velocity * sample.gain,
|
||||
sample.channels[0][position] * self.velocity * sample.gain,
|
||||
])
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct SampleKit <const N: usize> (
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
|
||||
impl<const N: usize> Default for SampleKit<N> {
|
||||
fn default () -> Self { Self([const { None }; N]) }
|
||||
}
|
||||
|
||||
impl<const N: usize> SampleKit<N> {
|
||||
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||
if index < self.0.len() {
|
||||
&self.0[index]
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Sample {
|
||||
pub name: Arc<str>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
pub gain: f32,
|
||||
pub color: ItemTheme,
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().into(),
|
||||
start,
|
||||
end,
|
||||
channels,
|
||||
rate: None,
|
||||
gain: 1.0,
|
||||
color: ItemTheme::random(),
|
||||
}
|
||||
}
|
||||
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||
Voice {
|
||||
sample: sample.clone(),
|
||||
after,
|
||||
position: sample.read().unwrap().start,
|
||||
velocity: velocity.as_int() as f32 / 127.0,
|
||||
}
|
||||
}
|
||||
pub fn handle_cc (&mut self, controller: u7, value: u7) {
|
||||
let percentage = value.as_int() as f64 / 127.;
|
||||
match controller.as_int() {
|
||||
20 => {
|
||||
self.start = (percentage * self.end as f64) as usize;
|
||||
},
|
||||
21 => {
|
||||
let length = self.channels[0].len();
|
||||
self.end = length.min(
|
||||
self.start + (percentage * (length as f64 - self.start as f64)) as usize
|
||||
);
|
||||
},
|
||||
22 => { /*attack*/ },
|
||||
23 => { /*decay*/ },
|
||||
24 => {
|
||||
self.gain = percentage as f32 * 2.0;
|
||||
},
|
||||
26 => { /* pan */ }
|
||||
25 => { /* pitch */ }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// Read WAV from file
|
||||
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||
channels.push(channel);
|
||||
}
|
||||
let mut end = 0;
|
||||
let mut data: Vec<Vec<f32>> = vec![];
|
||||
for samples in channels.iter() {
|
||||
let channel = Vec::from(samples.as_ref());
|
||||
end = end.max(channel.len());
|
||||
data.push(channel);
|
||||
}
|
||||
Ok((end, data))
|
||||
}
|
||||
pub fn from_file (path: &PathBuf) -> Usually<Self> {
|
||||
let name = path.file_name().unwrap().to_string_lossy().into();
|
||||
let mut sample = Self { name, ..Default::default() };
|
||||
// Use file extension if present
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.extension() {
|
||||
hint.with_extension(&ext.to_string_lossy());
|
||||
}
|
||||
let probed = symphonia::default::get_probe().format(
|
||||
&hint,
|
||||
MediaSourceStream::new(
|
||||
Box::new(File::open(path)?),
|
||||
Default::default(),
|
||||
),
|
||||
&Default::default(),
|
||||
&Default::default()
|
||||
)?;
|
||||
let mut format = probed.format;
|
||||
let params = &format.tracks().iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.expect("no tracks found")
|
||||
.codec_params;
|
||||
let mut decoder = get_codecs().make(params, &Default::default())?;
|
||||
loop {
|
||||
match format.next_packet() {
|
||||
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
|
||||
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
};
|
||||
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
||||
Ok(sample)
|
||||
}
|
||||
fn decode_packet (
|
||||
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
|
||||
) -> Usually<()> {
|
||||
// Decode a packet
|
||||
let decoded = decoder
|
||||
.decode(&packet)
|
||||
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
|
||||
// Determine sample rate
|
||||
let spec = *decoded.spec();
|
||||
if let Some(rate) = self.rate {
|
||||
if rate != spec.rate as usize {
|
||||
panic!("sample rate changed");
|
||||
}
|
||||
} else {
|
||||
self.rate = Some(spec.rate as usize);
|
||||
}
|
||||
// Determine channel count
|
||||
while self.channels.len() < spec.channels.count() {
|
||||
self.channels.push(vec![]);
|
||||
}
|
||||
// Load sample
|
||||
let mut samples = SampleBuffer::new(
|
||||
decoded.frames() as u64,
|
||||
spec
|
||||
);
|
||||
if samples.capacity() > 0 {
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
for frame in samples.samples().chunks(spec.channels.count()) {
|
||||
for (chan, frame) in frame.iter().enumerate() {
|
||||
self.channels[chan].push(*frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)] pub struct SampleAdd {
|
||||
pub exited: bool,
|
||||
pub dir: PathBuf,
|
||||
pub subdirs: Vec<OsString>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub _search: Option<String>,
|
||||
}
|
||||
|
||||
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
|
||||
|
||||
impl SampleAdd {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
pub fn new (
|
||||
sample: &Arc<RwLock<Sample>>,
|
||||
voices: &Arc<RwLock<Vec<Voice>>>
|
||||
) -> Usually<Self> {
|
||||
let dir = std::env::current_dir()?;
|
||||
let (subdirs, files) = scan(&dir)?;
|
||||
Ok(Self {
|
||||
exited: false,
|
||||
dir,
|
||||
subdirs,
|
||||
files,
|
||||
cursor: 0,
|
||||
offset: 0,
|
||||
sample: sample.clone(),
|
||||
voices: voices.clone(),
|
||||
_search: None
|
||||
})
|
||||
}
|
||||
fn rescan (&mut self) -> Usually<()> {
|
||||
scan(&self.dir).map(|(subdirs, files)|{
|
||||
self.subdirs = subdirs;
|
||||
self.files = files;
|
||||
})
|
||||
}
|
||||
fn prev (&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
}
|
||||
fn next (&mut self) {
|
||||
self.cursor = self.cursor + 1;
|
||||
}
|
||||
fn try_preview (&mut self) -> Usually<()> {
|
||||
if let Some(path) = self.cursor_file() {
|
||||
if let Ok(sample) = Sample::from_file(&path) {
|
||||
*self.sample.write().unwrap() = sample;
|
||||
self.voices.write().unwrap().push(
|
||||
Sample::play(&self.sample, 0, &u7::from(100u8))
|
||||
);
|
||||
}
|
||||
//load_sample(&path)?;
|
||||
//let src = std::fs::File::open(&path)?;
|
||||
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||
//let mut hint = Hint::new();
|
||||
//if let Some(ext) = path.extension() {
|
||||
//hint.with_extension(&ext.to_string_lossy());
|
||||
//}
|
||||
//let meta_opts: MetadataOptions = Default::default();
|
||||
//let fmt_opts: FormatOptions = Default::default();
|
||||
//if let Ok(mut probed) = symphonia::default::get_probe()
|
||||
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||
//{
|
||||
//panic!("{:?}", probed.format.metadata());
|
||||
//};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn cursor_dir (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn cursor_file (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
return None
|
||||
}
|
||||
let index = self.cursor.saturating_sub(self.subdirs.len());
|
||||
if index < self.files.len() {
|
||||
Some(self.dir.join(&self.files[index]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn pick (&mut self) -> Usually<bool> {
|
||||
if self.cursor == 0 {
|
||||
if let Some(parent) = self.dir.parent() {
|
||||
self.dir = parent.into();
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
if let Some(dir) = self.cursor_dir() {
|
||||
self.dir = dir;
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
if let Some(path) = self.cursor_file() {
|
||||
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
||||
let mut sample = self.sample.write().unwrap();
|
||||
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||
sample.end = end;
|
||||
sample.channels = channels;
|
||||
return Ok(true)
|
||||
}
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
todo!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// A sound cut.
|
||||
///
|
||||
/// ```
|
||||
/// let sample = tek::Sample::default();
|
||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Sample {
|
||||
pub name: Arc<str>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
pub gain: f32,
|
||||
pub color: ItemTheme,
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||
Self {
|
||||
name: name.as_ref().into(),
|
||||
start,
|
||||
end,
|
||||
channels,
|
||||
rate: None,
|
||||
gain: 1.0,
|
||||
color: ItemTheme::random(),
|
||||
}
|
||||
}
|
||||
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||
Voice {
|
||||
sample: sample.clone(),
|
||||
after,
|
||||
position: sample.read().unwrap().start,
|
||||
velocity: velocity.as_int() as f32 / 127.0,
|
||||
}
|
||||
}
|
||||
pub fn handle_cc (&mut self, controller: u7, value: u7) {
|
||||
let percentage = value.as_int() as f64 / 127.;
|
||||
match controller.as_int() {
|
||||
20 => {
|
||||
self.start = (percentage * self.end as f64) as usize;
|
||||
},
|
||||
21 => {
|
||||
let length = self.channels[0].len();
|
||||
self.end = length.min(
|
||||
self.start + (percentage * (length as f64 - self.start as f64)) as usize
|
||||
);
|
||||
},
|
||||
22 => { /*attack*/ },
|
||||
23 => { /*decay*/ },
|
||||
24 => {
|
||||
self.gain = percentage as f32 * 2.0;
|
||||
},
|
||||
26 => { /* pan */ }
|
||||
25 => { /* pitch */ }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// Read WAV from file
|
||||
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||
channels.push(channel);
|
||||
}
|
||||
let mut end = 0;
|
||||
let mut data: Vec<Vec<f32>> = vec![];
|
||||
for samples in channels.iter() {
|
||||
let channel = Vec::from(samples.as_ref());
|
||||
end = end.max(channel.len());
|
||||
data.push(channel);
|
||||
}
|
||||
Ok((end, data))
|
||||
}
|
||||
pub fn from_file (path: &PathBuf) -> Usually<Self> {
|
||||
let name = path.file_name().unwrap().to_string_lossy().into();
|
||||
let mut sample = Self { name, ..Default::default() };
|
||||
// Use file extension if present
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.extension() {
|
||||
hint.with_extension(&ext.to_string_lossy());
|
||||
}
|
||||
let probed = symphonia::default::get_probe().format(
|
||||
&hint,
|
||||
MediaSourceStream::new(
|
||||
Box::new(File::open(path)?),
|
||||
Default::default(),
|
||||
),
|
||||
&Default::default(),
|
||||
&Default::default()
|
||||
)?;
|
||||
let mut format = probed.format;
|
||||
let params = &format.tracks().iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.expect("no tracks found")
|
||||
.codec_params;
|
||||
let mut decoder = get_codecs().make(params, &Default::default())?;
|
||||
loop {
|
||||
match format.next_packet() {
|
||||
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
|
||||
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
};
|
||||
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
||||
Ok(sample)
|
||||
}
|
||||
fn decode_packet (
|
||||
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
|
||||
) -> Usually<()> {
|
||||
// Decode a packet
|
||||
let decoded = decoder
|
||||
.decode(&packet)
|
||||
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
|
||||
// Determine sample rate
|
||||
let spec = *decoded.spec();
|
||||
if let Some(rate) = self.rate {
|
||||
if rate != spec.rate as usize {
|
||||
panic!("sample rate changed");
|
||||
}
|
||||
} else {
|
||||
self.rate = Some(spec.rate as usize);
|
||||
}
|
||||
// Determine channel count
|
||||
while self.channels.len() < spec.channels.count() {
|
||||
self.channels.push(vec![]);
|
||||
}
|
||||
// Load sample
|
||||
let mut samples = SampleBuffer::new(
|
||||
decoded.frames() as u64,
|
||||
spec
|
||||
);
|
||||
if samples.capacity() > 0 {
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
for frame in samples.samples().chunks(spec.channels.count()) {
|
||||
for (chan, frame) in frame.iter().enumerate() {
|
||||
self.channels[chan].push(*frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
use crate::{*, device::sampler::*};
|
||||
|
||||
#[derive(Default, Debug)] pub struct SampleAdd {
|
||||
pub exited: bool,
|
||||
pub dir: PathBuf,
|
||||
pub subdirs: Vec<OsString>,
|
||||
pub files: Vec<OsString>,
|
||||
pub cursor: usize,
|
||||
pub offset: usize,
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub _search: Option<String>,
|
||||
}
|
||||
|
||||
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
|
||||
|
||||
impl SampleAdd {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
pub fn new (
|
||||
sample: &Arc<RwLock<Sample>>,
|
||||
voices: &Arc<RwLock<Vec<Voice>>>
|
||||
) -> Usually<Self> {
|
||||
let dir = std::env::current_dir()?;
|
||||
let (subdirs, files) = scan(&dir)?;
|
||||
Ok(Self {
|
||||
exited: false,
|
||||
dir,
|
||||
subdirs,
|
||||
files,
|
||||
cursor: 0,
|
||||
offset: 0,
|
||||
sample: sample.clone(),
|
||||
voices: voices.clone(),
|
||||
_search: None
|
||||
})
|
||||
}
|
||||
fn rescan (&mut self) -> Usually<()> {
|
||||
scan(&self.dir).map(|(subdirs, files)|{
|
||||
self.subdirs = subdirs;
|
||||
self.files = files;
|
||||
})
|
||||
}
|
||||
fn prev (&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
}
|
||||
fn next (&mut self) {
|
||||
self.cursor = self.cursor + 1;
|
||||
}
|
||||
fn try_preview (&mut self) -> Usually<()> {
|
||||
if let Some(path) = self.cursor_file() {
|
||||
if let Ok(sample) = Sample::from_file(&path) {
|
||||
*self.sample.write().unwrap() = sample;
|
||||
self.voices.write().unwrap().push(
|
||||
Sample::play(&self.sample, 0, &u7::from(100u8))
|
||||
);
|
||||
}
|
||||
//load_sample(&path)?;
|
||||
//let src = std::fs::File::open(&path)?;
|
||||
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||
//let mut hint = Hint::new();
|
||||
//if let Some(ext) = path.extension() {
|
||||
//hint.with_extension(&ext.to_string_lossy());
|
||||
//}
|
||||
//let meta_opts: MetadataOptions = Default::default();
|
||||
//let fmt_opts: FormatOptions = Default::default();
|
||||
//if let Ok(mut probed) = symphonia::default::get_probe()
|
||||
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||
//{
|
||||
//panic!("{:?}", probed.format.metadata());
|
||||
//};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn cursor_dir (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn cursor_file (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
return None
|
||||
}
|
||||
let index = self.cursor.saturating_sub(self.subdirs.len());
|
||||
if index < self.files.len() {
|
||||
Some(self.dir.join(&self.files[index]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn pick (&mut self) -> Usually<bool> {
|
||||
if self.cursor == 0 {
|
||||
if let Some(parent) = self.dir.parent() {
|
||||
self.dir = parent.into();
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
if let Some(dir) = self.cursor_dir() {
|
||||
self.dir = dir;
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
if let Some(path) = self.cursor_file() {
|
||||
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
||||
let mut sample = self.sample.write().unwrap();
|
||||
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||
sample.end = end;
|
||||
sample.channels = channels;
|
||||
return Ok(true)
|
||||
}
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of samples, one per slot, fixed number of slots.
|
||||
///
|
||||
/// History: Separated to cleanly implement [Default].
|
||||
///
|
||||
/// ```
|
||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||
/// ```
|
||||
#[derive(Debug)] pub struct SampleKit <const N: usize> (
|
||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||
);
|
||||
|
||||
impl<const N: usize> Default for SampleKit<N> {
|
||||
fn default () -> Self { Self([const { None }; N]) }
|
||||
}
|
||||
|
||||
impl<const N: usize> SampleKit<N> {
|
||||
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||
if index < self.0.len() {
|
||||
&self.0[index]
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// A currently playing instance of a sample.
|
||||
#[derive(Default, Debug, Clone)] pub struct Voice {
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub after: usize,
|
||||
pub position: usize,
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
impl Iterator for Voice {
|
||||
type Item = [f32;2];
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
if self.after > 0 {
|
||||
self.after -= 1;
|
||||
return Some([0.0, 0.0])
|
||||
}
|
||||
let sample = self.sample.read().unwrap();
|
||||
if self.position < sample.end {
|
||||
let position = self.position;
|
||||
self.position += 1;
|
||||
return sample.channels[0].get(position).map(|_amplitude|[
|
||||
sample.channels[0][position] * self.velocity * sample.gain,
|
||||
sample.channels[0][position] * self.velocity * sample.gain,
|
||||
])
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{*, clock::*, device::*};
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
|
|||
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
||||
/// clip.set_length(96);
|
||||
/// clip.toggle_loop();
|
||||
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||
///
|
||||
|
|
|
|||
30
src/tek.edn
30
src/tek.edn
|
|
@ -2,17 +2,22 @@
|
|||
(bsp/s (exact/y 1 (text ~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~))
|
||||
(bsp/s (exact/y 1 (text ~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~)) (text dig?)))))
|
||||
|
||||
(view :browse (bsp/s (padding 3 1 :browse-title) (enclose (fg (g 96)) browser)))
|
||||
(view :browse (bsp/s
|
||||
(padding 3 1 :browse-title)
|
||||
(enclose (fg (g 96)) browser)))
|
||||
|
||||
(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global)
|
||||
(mode :transport
|
||||
(name Transport)
|
||||
(info JACK transport controller.)
|
||||
(keys :clock :global)
|
||||
:transport)
|
||||
|
||||
(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm)
|
||||
(view (bg (g 0)
|
||||
(bsp/s (max/y 2 :transport
|
||||
(view (bg (g 64)
|
||||
(bsp/s (max/xy 80 2 :transport)
|
||||
(bsp/s (max/y 3 (bg (g 80) :ports/out))
|
||||
(bsp/n (max/y 3 (bg (g 80) :ports/in))
|
||||
(bg (g 30) (bsp/s (max/h 6 (bg (g 70) :logo) :dialog/menu))))))))))
|
||||
(bg (g 30) (bsp/s (max/y 6 :logo) :dialog/menu))))))))
|
||||
|
||||
(mode :sequencer (name Sequencer) (info MIDI sequencer.)
|
||||
(keys :editor :clock :global)
|
||||
|
|
@ -29,13 +34,13 @@
|
|||
(bsp/n (fixed/y 1 :status)
|
||||
(fill :samples/grid))))
|
||||
|
||||
(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
|
||||
(bsp/e (text MIDI-OUT)
|
||||
(fill/x (align/e (text AUDIO-OUT-R)))))))
|
||||
(view :ports/out
|
||||
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
|
||||
(bsp/e (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))
|
||||
|
||||
(view :ports/in (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
|
||||
(bsp/e (text MIDI-IN)
|
||||
(fill/x (align/e (text AUDIO-IN-R)))))))
|
||||
(view :ports/in
|
||||
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
|
||||
(bsp/e (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))
|
||||
|
||||
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
|
||||
(keys :clock :editor :sampler :global)
|
||||
|
|
@ -84,7 +89,8 @@
|
|||
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
|
||||
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
||||
(keys :focus)
|
||||
(keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del))
|
||||
(keys :editor (see :axis/i :axis/i2 :axis/y
|
||||
:page :editor/view :editor/add :editor/del))
|
||||
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
||||
(@z toggle :lock))
|
||||
(keys :editor/add (@a editor/append :true)
|
||||
|
|
|
|||
2221
src/tek.rs
2221
src/tek.rs
File diff suppressed because it is too large
Load diff
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
|||
Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1
|
||||
Subproject commit 5ca329292f808c137b6e2b7e80892e2a9e855696
|
||||
Loading…
Add table
Add a link
Reference in a new issue