mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
233 lines
8.6 KiB
Rust
233 lines
8.6 KiB
Rust
use crate::*;
|
|
use super::*;
|
|
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
|
use ClockCommand::{Play, Pause};
|
|
use GrooveboxCommand as Cmd;
|
|
use MidiEditCommand::*;
|
|
use PhrasePoolCommand::*;
|
|
|
|
pub struct Groovebox {
|
|
_jack: Arc<RwLock<JackConnection>>,
|
|
|
|
pub player: MidiPlayer,
|
|
pub pool: PoolModel,
|
|
pub editor: MidiEditor,
|
|
pub sampler: Sampler,
|
|
|
|
pub compact: bool,
|
|
pub size: Measure<Tui>,
|
|
pub status: bool,
|
|
pub note_buf: Vec<u8>,
|
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
pub perf: PerfModel,
|
|
}
|
|
impl Groovebox {
|
|
pub fn new (
|
|
jack: &Arc<RwLock<JackConnection>>,
|
|
midi_from: &[impl AsRef<str>],
|
|
midi_to: &[impl AsRef<str>],
|
|
audio_from: &[&[impl AsRef<str>];2],
|
|
audio_to: &[&[impl AsRef<str>];2],
|
|
) -> Usually<Self> {
|
|
let sampler = crate::sampler::Sampler::new(jack, &"sampler", midi_from, audio_from, audio_to)?;
|
|
let mut player = crate::midi::MidiPlayer::new(jack, &"sequencer", &midi_from, &midi_to)?;
|
|
jack.read().unwrap().client().connect_ports(&player.midi_outs[0], &sampler.midi_in)?;
|
|
let phrase = Arc::new(RwLock::new(MidiClip::new(
|
|
"New", true, 4 * player.clock.timebase.ppq.get() as usize,
|
|
None, Some(ItemColor::random().into())
|
|
)));
|
|
player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone())));
|
|
Ok(Self {
|
|
player,
|
|
sampler,
|
|
_jack: jack.clone(),
|
|
pool: crate::pool::PoolModel::from(&phrase),
|
|
editor: crate::midi::MidiEditor::from(&phrase),
|
|
size: Measure::new(),
|
|
midi_buf: vec![vec![];65536],
|
|
note_buf: vec![],
|
|
perf: PerfModel::default(),
|
|
status: true,
|
|
compact: true,
|
|
})
|
|
}
|
|
}
|
|
has_clock!(|self: Groovebox|self.player.clock());
|
|
audio!(|self: Groovebox, client, scope|{
|
|
let t0 = self.perf.get_t0();
|
|
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
|
|
return Control::Quit
|
|
}
|
|
if Control::Quit == PlayerAudio(
|
|
&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
|
).process(client, scope) {
|
|
return Control::Quit
|
|
}
|
|
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
|
|
return Control::Quit
|
|
}
|
|
// TODO move these to editor and sampler:
|
|
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
|
|
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
|
match message {
|
|
MidiMessage::NoteOn { ref key, .. } => {
|
|
self.editor.set_note_point(key.as_int() as usize);
|
|
},
|
|
MidiMessage::Controller { controller, value } => {
|
|
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
|
|
sample.write().unwrap().handle_cc(controller, value)
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
self.perf.update(t0, scope);
|
|
Control::Continue
|
|
});
|
|
render!(Tui: (self: Groovebox) => {
|
|
let w = self.size.w();
|
|
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
let pool_w = if !self.compact { phrase_w } else { 5 };
|
|
let sampler_w = if !self.compact { phrase_w } else { 4 };
|
|
let sample_h = if !self.compact { 5 } else { 0 };
|
|
let note_pt = self.editor.note_point();
|
|
let color = self.player.play_phrase().as_ref()
|
|
.and_then(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color))
|
|
.clone();
|
|
|
|
let sampler =
|
|
Fixed::x(sampler_w, Push::y(1, Align::w(Fill::y(SampleList::new(self.compact, &self.sampler, &self.editor)))));
|
|
let selectors =
|
|
Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player));
|
|
let edit_clip =
|
|
MidiEditClip(&self.editor);
|
|
|
|
self.size.of(Bsp::s(
|
|
Fill::x(Fixed::y(2, lay!(
|
|
Align::w(Meter("L/", self.sampler.input_meter[0])),
|
|
Align::e(Meter("R/", self.sampler.input_meter[1])),
|
|
Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))),
|
|
))),
|
|
Bsp::s(
|
|
lay!(Align::w(edit_clip), Align::e(selectors)),
|
|
Bsp::n(
|
|
Bsp::a(
|
|
Outer(Style::default().fg(TuiTheme::g(128))),
|
|
Fill::x(Fixed::y(sample_h, if let Some((_, sample)) = &self.sampler.recording {
|
|
SampleViewer(Some(sample.clone()))
|
|
} else if let Some(sample) = &self.sampler.mapped[note_pt] {
|
|
SampleViewer(Some(sample.clone()))
|
|
} else {
|
|
SampleViewer(None)
|
|
})),
|
|
),
|
|
Bsp::n(
|
|
lay!(
|
|
Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt))),
|
|
Align::x(Fixed::y(1, MidiEditStatus(&self.editor))),
|
|
),
|
|
Bsp::w(
|
|
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))),
|
|
Fill::xy(Bsp::e(sampler, &self.editor)),
|
|
),
|
|
)
|
|
)
|
|
)
|
|
))
|
|
});
|
|
|
|
pub enum GrooveboxCommand {
|
|
Compact(bool),
|
|
History(isize),
|
|
Clock(ClockCommand),
|
|
Pool(PoolCommand),
|
|
Editor(MidiEditCommand),
|
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
Sampler(SamplerCommand),
|
|
}
|
|
|
|
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input));
|
|
|
|
input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.event() {
|
|
// TODO: k: toggle on-screen keyboard
|
|
key_pat!(Ctrl-Char('k')) => {
|
|
todo!("keyboard")
|
|
},
|
|
|
|
// Transport: Play from start or rewind to start
|
|
key_pat!(Char(' ')) => Cmd::Clock(
|
|
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
|
),
|
|
|
|
// Tab: Toggle visibility of sidebars
|
|
key_pat!(Tab) => Cmd::Compact(!state.compact),
|
|
|
|
// q: Enqueue currently edited phrase
|
|
key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
|
// 0: Enqueue phrase 0 (stop all)
|
|
key_pat!(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
|
|
|
|
key_pat!(Shift-Char('R')) => Cmd::Sampler(if state.sampler.recording.is_some() {
|
|
SamplerCommand::RecordFinish
|
|
} else {
|
|
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
|
}),
|
|
|
|
// e: Toggle between editing currently playing or other phrase
|
|
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
|
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
|
let selected = state.pool.phrase().clone();
|
|
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
|
selected
|
|
} else {
|
|
playing.clone()
|
|
})))
|
|
} else {
|
|
return None
|
|
},
|
|
|
|
// For the rest, use the default keybindings of the components.
|
|
// The ones defined above supersede them.
|
|
_ => if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
|
Cmd::Editor(command)
|
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
|
Cmd::Pool(command)
|
|
} else {
|
|
return None
|
|
}
|
|
});
|
|
|
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
|
Self::Enqueue(phrase) => {
|
|
state.player.enqueue_next(phrase.as_ref());
|
|
None
|
|
},
|
|
Self::Pool(cmd) => {
|
|
match cmd {
|
|
// autoselect: automatically load selected phrase in editor
|
|
PoolCommand::Select(_) => {
|
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
|
undo
|
|
},
|
|
// update color in all places simultaneously
|
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
|
undo
|
|
},
|
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
}
|
|
},
|
|
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
Self::History(delta) => { todo!("undo/redo") },
|
|
Self::Compact(compact) => if state.compact != compact {
|
|
state.compact = compact;
|
|
Some(Self::Compact(!compact))
|
|
} else {
|
|
None
|
|
},
|
|
});
|