mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
wip: refactor pt.6, fixed tek_api
This commit is contained in:
parent
5df08409e5
commit
869d92110d
29 changed files with 1678 additions and 1679 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use tek_core::jack::*;
|
use tek_core::jack::{*, Transport as JackTransport};
|
||||||
|
|
||||||
/// Trait for things that have a JACK process callback.
|
/// Trait for things that have a JACK process callback.
|
||||||
pub trait Audio: Send + Sync {
|
pub trait Audio: Send + Sync {
|
||||||
|
|
@ -38,7 +38,7 @@ pub trait AudioEngine {
|
||||||
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
||||||
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
||||||
fn client (&self) -> &Client;
|
fn client (&self) -> &Client;
|
||||||
fn transport (&self) -> Transport {
|
fn transport (&self) -> JackTransport {
|
||||||
self.client().transport()
|
self.client().transport()
|
||||||
}
|
}
|
||||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,22 @@ pub struct Arrangement {
|
||||||
/// Collection of phrases.
|
/// Collection of phrases.
|
||||||
pub phrases: Arc<RwLock<Vec<Phrase>>>,
|
pub phrases: Arc<RwLock<Vec<Phrase>>>,
|
||||||
/// Collection of tracks.
|
/// Collection of tracks.
|
||||||
pub tracks: Vec<SequencerTrack>,
|
pub tracks: Vec<ArrangementTrack>,
|
||||||
/// Collection of scenes.
|
/// Collection of scenes.
|
||||||
pub scenes: Vec<Scene>,
|
pub scenes: Vec<Scene>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ArrangementTrack {
|
||||||
|
/// Name of track
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Preferred width of track column
|
||||||
|
pub width: usize,
|
||||||
|
/// Identifying color of track
|
||||||
|
pub color: ItemColor,
|
||||||
|
/// The MIDI player for the track
|
||||||
|
pub player: MIDIPlayer
|
||||||
|
}
|
||||||
|
|
||||||
impl Audio for Arrangement {
|
impl Audio for Arrangement {
|
||||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
for track in self.tracks.iter_mut() {
|
for track in self.tracks.iter_mut() {
|
||||||
|
|
@ -24,6 +35,12 @@ impl Audio for Arrangement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Arrangement {
|
||||||
|
fn is_stopped (&self) -> bool {
|
||||||
|
*self.clock.playing.read().unwrap() == Some(TransportState::Stopped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ArrangementCommand {
|
pub enum ArrangementCommand {
|
||||||
New,
|
New,
|
||||||
|
|
@ -58,8 +75,8 @@ pub enum ArrangementTrackCommand {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ArrangementClipCommand {
|
pub enum ArrangementClipCommand {
|
||||||
SetLoop(bool),
|
|
||||||
Get(usize, usize),
|
Get(usize, usize),
|
||||||
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
SetLoop(bool),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ submod! {
|
||||||
pool
|
pool
|
||||||
sampler
|
sampler
|
||||||
sample
|
sample
|
||||||
scene
|
scene scene_cmd
|
||||||
sequencer
|
sequencer
|
||||||
track
|
track
|
||||||
transport transport_cmd
|
transport transport_cmd
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@ use crate::*;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Plugin {
|
pub struct Plugin {
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
pub plugin: Option<PluginKind>,
|
pub plugin: Option<PluginKind>,
|
||||||
pub selected: usize,
|
pub selected: usize,
|
||||||
pub mapping: bool,
|
pub mapping: bool,
|
||||||
|
pub midi_ins: Vec<Port<MidiIn>>,
|
||||||
|
pub midi_outs: Vec<Port<MidiOut>>,
|
||||||
|
pub audio_ins: Vec<Port<AudioIn>>,
|
||||||
|
pub audio_outs: Vec<Port<AudioOut>>,
|
||||||
}
|
}
|
||||||
impl Plugin {
|
impl Plugin {
|
||||||
pub fn new_lv2 (
|
pub fn new_lv2 (
|
||||||
|
|
@ -24,6 +28,10 @@ impl Plugin {
|
||||||
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
||||||
selected: 0,
|
selected: 0,
|
||||||
mapping: false,
|
mapping: false,
|
||||||
|
midi_ins: vec![],
|
||||||
|
midi_outs: vec![],
|
||||||
|
audio_ins: vec![],
|
||||||
|
audio_outs: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +65,7 @@ impl Audio for Plugin {
|
||||||
})) => {
|
})) => {
|
||||||
let urid = features.midi_urid();
|
let urid = features.midi_urid();
|
||||||
input_buffer.clear();
|
input_buffer.clear();
|
||||||
for port in self.ports.midi_ins.values() {
|
for port in self.midi_ins.iter() {
|
||||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||||
&features,
|
&features,
|
||||||
scope.n_frames() as usize
|
scope.n_frames() as usize
|
||||||
|
|
@ -75,24 +83,20 @@ impl Audio for Plugin {
|
||||||
input_buffer.push(atom);
|
input_buffer.push(atom);
|
||||||
}
|
}
|
||||||
let mut outputs = vec![];
|
let mut outputs = vec![];
|
||||||
for _ in self.ports.midi_outs.iter() {
|
for _ in self.midi_outs.iter() {
|
||||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||||
&features,
|
&features,
|
||||||
scope.n_frames() as usize
|
scope.n_frames() as usize
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let ports = ::livi::EmptyPortConnections::new()
|
let ports = ::livi::EmptyPortConnections::new()
|
||||||
.with_atom_sequence_inputs(
|
.with_atom_sequence_inputs(input_buffer.iter())
|
||||||
input_buffer.iter()
|
.with_atom_sequence_outputs(outputs.iter_mut())
|
||||||
)
|
|
||||||
.with_atom_sequence_outputs(
|
|
||||||
outputs.iter_mut()
|
|
||||||
)
|
|
||||||
.with_audio_inputs(
|
.with_audio_inputs(
|
||||||
self.ports.audio_ins.values().map(|o|o.as_slice(scope))
|
self.audio_ins.iter().map(|o|o.as_slice(scope))
|
||||||
)
|
)
|
||||||
.with_audio_outputs(
|
.with_audio_outputs(
|
||||||
self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope))
|
self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))
|
||||||
);
|
);
|
||||||
unsafe {
|
unsafe {
|
||||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl LV2Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LV2Plugin {
|
impl LV2Plugin {
|
||||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Plugin> {
|
||||||
let mut name = String::new();
|
let mut name = String::new();
|
||||||
let mut path = String::new();
|
let mut path = String::new();
|
||||||
edn!(edn in args {
|
edn!(edn in args {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub struct Sampler {
|
||||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
pub midi_in: Port<MidiIn>,
|
pub midi_in: Port<MidiIn>,
|
||||||
pub audio_outs: Vec<Port<MidiOut>>,
|
pub audio_outs: Vec<Port<AudioOut>>,
|
||||||
pub buffer: Vec<Vec<f32>>,
|
pub buffer: Vec<Vec<f32>>,
|
||||||
pub output_gain: f32
|
pub output_gain: f32
|
||||||
}
|
}
|
||||||
|
|
@ -52,33 +52,20 @@ impl Sampler {
|
||||||
_ => panic!("unexpected in sampler {name}: {edn:?}")
|
_ => panic!("unexpected in sampler {name}: {edn:?}")
|
||||||
});
|
});
|
||||||
Ok(Sampler {
|
Ok(Sampler {
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
mapped: samples,
|
mapped: samples,
|
||||||
unmapped: Default::default(),
|
unmapped: Default::default(),
|
||||||
voices: Default::default(),
|
voices: Default::default(),
|
||||||
ports: Default::default(),
|
buffer: Default::default(),
|
||||||
buffer: Default::default(),
|
midi_in: jack.read().unwrap().register_port("in", MidiIn::default())?,
|
||||||
|
audio_outs: vec![],
|
||||||
output_gain: 0.
|
output_gain: 0.
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/// Immutable reference to sample at cursor.
|
|
||||||
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
|
||||||
for (i, sample) in self.mapped.values().enumerate() {
|
|
||||||
if i == self.cursor.0 {
|
|
||||||
return Some(sample)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i, sample) in self.unmapped.iter().enumerate() {
|
|
||||||
if i + self.mapped.len() == self.cursor.0 {
|
|
||||||
return Some(sample)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
||||||
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
|
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
|
||||||
for RawMidi { time, bytes } in self.ports.midi_ins.get("midi").unwrap().iter(scope) {
|
for RawMidi { time, bytes } in self.midi_in.iter(scope) {
|
||||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||||
if let MidiMessage::NoteOn { ref key, ref vel } = message {
|
if let MidiMessage::NoteOn { ref key, ref vel } = message {
|
||||||
if let Some(sample) = self.mapped.get(key) {
|
if let Some(sample) = self.mapped.get(key) {
|
||||||
|
|
@ -117,7 +104,7 @@ impl Sampler {
|
||||||
}
|
}
|
||||||
/// Write output buffer to output ports.
|
/// Write output buffer to output ports.
|
||||||
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
|
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
|
||||||
for (i, port) in self.ports.audio_outs.values_mut().enumerate() {
|
for (i, port) in self.audio_outs.iter_mut().enumerate() {
|
||||||
let buffer = &self.buffer[i];
|
let buffer = &self.buffer[i];
|
||||||
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
|
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
|
||||||
*value = *buffer.get(i).unwrap_or(&0.0);
|
*value = *buffer.get(i).unwrap_or(&0.0);
|
||||||
|
|
|
||||||
|
|
@ -11,43 +11,60 @@ pub struct Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
//TODO
|
||||||
let mut name = None;
|
//pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
let mut clips = vec![];
|
//let mut name = None;
|
||||||
edn!(edn in args {
|
//let mut clips = vec![];
|
||||||
Edn::Map(map) => {
|
//edn!(edn in args {
|
||||||
let key = map.get(&Edn::Key(":name"));
|
//Edn::Map(map) => {
|
||||||
if let Some(Edn::Str(n)) = key {
|
//let key = map.get(&Edn::Key(":name"));
|
||||||
name = Some(*n);
|
//if let Some(Edn::Str(n)) = key {
|
||||||
} else {
|
//name = Some(*n);
|
||||||
panic!("unexpected key in scene '{name:?}': {key:?}")
|
//} else {
|
||||||
}
|
//panic!("unexpected key in scene '{name:?}': {key:?}")
|
||||||
},
|
//}
|
||||||
Edn::Symbol("_") => {
|
//},
|
||||||
clips.push(None);
|
//Edn::Symbol("_") => {
|
||||||
},
|
//clips.push(None);
|
||||||
Edn::Int(i) => {
|
//},
|
||||||
clips.push(Some(*i as usize));
|
//Edn::Int(i) => {
|
||||||
},
|
//clips.push(Some(*i as usize));
|
||||||
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
//},
|
||||||
});
|
//_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||||||
Ok(Scene {
|
//});
|
||||||
name: Arc::new(name.unwrap_or("").to_string().into()),
|
//Ok(Scene {
|
||||||
color: ItemColor::random(),
|
//name: Arc::new(name.unwrap_or("").to_string().into()),
|
||||||
clips,
|
//color: ItemColor::random(),
|
||||||
|
//clips,
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// Returns the pulse length of the longest phrase in the scene
|
||||||
|
pub fn pulses (&self) -> usize {
|
||||||
|
self.clips.iter().fold(0, |a, p|{
|
||||||
|
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// Returns true if all phrases in the scene are
|
||||||
pub enum SceneCommand {
|
/// currently playing on the given collection of tracks.
|
||||||
Next,
|
pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool {
|
||||||
Prev,
|
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||||
Add,
|
.all(|(track_index, clip)|match clip {
|
||||||
Delete,
|
Some(clip) => tracks
|
||||||
MoveForward,
|
.get(track_index)
|
||||||
MoveBack,
|
.map(|track|if let Some((_, Some(phrase))) = &track.phrase {
|
||||||
RandomColor,
|
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||||
SetSize(usize),
|
} else {
|
||||||
SetZoom(usize),
|
false
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
None => true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
|
||||||
|
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
crates/tek_api/src/scene_cmd.rs
Normal file
14
crates/tek_api/src/scene_cmd.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SceneCommand {
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Add,
|
||||||
|
Delete,
|
||||||
|
MoveForward,
|
||||||
|
MoveBack,
|
||||||
|
RandomColor,
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
}
|
||||||
|
|
@ -1,38 +1,10 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
/// MIDI message structural
|
||||||
pub enum SequencerCommand {
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
Focus(FocusCommand),
|
|
||||||
Transport(TransportCommand),
|
|
||||||
Phrases(PhrasePoolCommand),
|
|
||||||
Editor(PhraseEditorCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum PhraseEditorCommand {
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
|
||||||
ToggleDirection,
|
|
||||||
EnterEditMode,
|
|
||||||
ExitEditMode,
|
|
||||||
NoteAppend,
|
|
||||||
NoteSet,
|
|
||||||
NoteCursorSet(usize),
|
|
||||||
NoteLengthSet(usize),
|
|
||||||
NoteScrollSet(usize),
|
|
||||||
TimeCursorSet(usize),
|
|
||||||
TimeScrollSet(usize),
|
|
||||||
TimeZoomSet(usize),
|
|
||||||
Go(Direction),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SequencerTrack {
|
pub struct MIDIPlayer {
|
||||||
/// Name of track
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub width: usize,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub color: ItemColor,
|
|
||||||
/// Global timebase
|
/// Global timebase
|
||||||
pub clock: Arc<Clock>,
|
pub clock: Arc<Clock>,
|
||||||
/// Start time and phrase being played
|
/// Start time and phrase being played
|
||||||
|
|
@ -62,7 +34,7 @@ pub struct SequencerTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JACK process callback for a sequencer's phrase player/recorder.
|
/// JACK process callback for a sequencer's phrase player/recorder.
|
||||||
impl Audio for SequencerTrack {
|
impl Audio for MIDIPlayer {
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
let has_midi_outputs = self.has_midi_outputs();
|
let has_midi_outputs = self.has_midi_outputs();
|
||||||
let has_midi_inputs = self.has_midi_inputs();
|
let has_midi_inputs = self.has_midi_inputs();
|
||||||
|
|
@ -88,7 +60,33 @@ impl Audio for SequencerTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods used primarily by the process callback
|
/// Methods used primarily by the process callback
|
||||||
impl SequencerTrack {
|
impl MIDIPlayer {
|
||||||
|
pub fn new (
|
||||||
|
jack: &Arc<RwLock<JackClient>>,
|
||||||
|
clock: &Arc<Clock>,
|
||||||
|
name: &str
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let jack = jack.read().unwrap();
|
||||||
|
Ok(Self {
|
||||||
|
clock: clock.clone(),
|
||||||
|
phrase: None,
|
||||||
|
next_phrase: None,
|
||||||
|
notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
|
notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
|
monitoring: false,
|
||||||
|
recording: false,
|
||||||
|
overdub: true,
|
||||||
|
reset: true,
|
||||||
|
midi_note: Vec::with_capacity(8),
|
||||||
|
midi_chunk: vec![Vec::with_capacity(16);16384],
|
||||||
|
midi_outputs: vec![
|
||||||
|
jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
||||||
|
],
|
||||||
|
midi_inputs: vec![
|
||||||
|
jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
fn is_rolling (&self) -> bool {
|
fn is_rolling (&self) -> bool {
|
||||||
*self.clock.playing.read().unwrap() == Some(TransportState::Rolling)
|
*self.clock.playing.read().unwrap() == Some(TransportState::Rolling)
|
||||||
}
|
}
|
||||||
|
|
@ -251,10 +249,28 @@ impl SequencerTrack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; }
|
||||||
|
pub fn toggle_record (&mut self) { self.recording = !self.recording; }
|
||||||
|
pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; }
|
||||||
|
pub fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||||
|
let start = self.clock.next_launch_pulse();
|
||||||
|
self.next_phrase = Some((
|
||||||
|
Instant::from_pulse(&self.clock.timebase(), start as f64),
|
||||||
|
phrase.map(|p|p.clone())
|
||||||
|
));
|
||||||
|
self.reset = true;
|
||||||
|
}
|
||||||
|
pub fn pulses_since_start (&self) -> Option<f64> {
|
||||||
|
if let Some((started, Some(_))) = self.phrase.as_ref() {
|
||||||
|
Some(self.clock.current.pulse.get() - started.pulse.get())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add "all notes off" to the start of a buffer.
|
/// Add "all notes off" to the start of a buffer.
|
||||||
pub fn all_notes_off (output: &mut PhraseChunk) {
|
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ pub trait MixerTrackDevice: Audio + Debug {
|
||||||
|
|
||||||
impl MixerTrackDevice for Sampler {}
|
impl MixerTrackDevice for Sampler {}
|
||||||
|
|
||||||
impl MixerTrackDevice for LV2Plugin {}
|
impl MixerTrackDevice for Plugin {}
|
||||||
|
|
||||||
|
//impl MixerTrackDevice for LV2Plugin {}
|
||||||
|
|
||||||
impl MixerTrack {
|
impl MixerTrack {
|
||||||
const SYM_NAME: &'static str = ":name";
|
const SYM_NAME: &'static str = ":name";
|
||||||
|
|
@ -37,8 +39,6 @@ impl MixerTrack {
|
||||||
audio_outs: vec![],
|
audio_outs: vec![],
|
||||||
devices: vec![],
|
devices: vec![],
|
||||||
};
|
};
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut devices: Vec<JackClient> = vec![];
|
|
||||||
edn!(edn in args {
|
edn!(edn in args {
|
||||||
Edn::Map(map) => {
|
Edn::Map(map) => {
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) {
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) {
|
||||||
|
|
@ -51,8 +51,8 @@ impl MixerTrack {
|
||||||
Edn::List(args) => match args.get(0) {
|
Edn::List(args) => match args.get(0) {
|
||||||
// Add a sampler device to the track
|
// Add a sampler device to the track
|
||||||
Some(Edn::Symbol(Self::SYM_SAMPLER)) => {
|
Some(Edn::Symbol(Self::SYM_SAMPLER)) => {
|
||||||
track.add_device(
|
track.devices.push(
|
||||||
Sampler::from_edn(jack, &args[1..])?
|
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||||
);
|
);
|
||||||
panic!(
|
panic!(
|
||||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
||||||
|
|
@ -62,8 +62,8 @@ impl MixerTrack {
|
||||||
},
|
},
|
||||||
// Add a LV2 plugin to the track.
|
// Add a LV2 plugin to the track.
|
||||||
Some(Edn::Symbol(Self::SYM_LV2)) => {
|
Some(Edn::Symbol(Self::SYM_LV2)) => {
|
||||||
track.add_device(
|
track.devices.push(
|
||||||
LV2Plugin::from_edn(jack, &args[1..])?
|
Box::new(LV2Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||||
);
|
);
|
||||||
panic!(
|
panic!(
|
||||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
||||||
|
|
@ -80,7 +80,4 @@ impl MixerTrack {
|
||||||
});
|
});
|
||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
pub fn add_device (&mut self, device: impl MixerTrackDevice) {
|
|
||||||
self.devices.push(device.boxed())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,23 @@ pub struct Voice {
|
||||||
pub position: usize,
|
pub position: usize,
|
||||||
pub velocity: f32,
|
pub velocity: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Iterator for Voice {
|
||||||
|
type Item = [f32;2];
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
if self.after > 0 {
|
||||||
|
self.after = 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 = self.position + 1;
|
||||||
|
return sample.channels[0].get(position).map(|_amplitude|[
|
||||||
|
sample.channels[0][position] * self.velocity,
|
||||||
|
sample.channels[0][position] * self.velocity,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ impl ArrangerCli {
|
||||||
Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32))
|
Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32))
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(ArrangerView::new(
|
Ok(ArrangerApp::new(
|
||||||
jack,
|
jack,
|
||||||
self.transport.then_some(transport),
|
self.transport.then_some(transport),
|
||||||
arrangement,
|
arrangement,
|
||||||
|
|
@ -53,33 +53,3 @@ impl ArrangerCli {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Audio for ArrangerView {
|
|
||||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
if let Some(ref transport) = self.transport {
|
|
||||||
transport.write().unwrap().process(client, scope);
|
|
||||||
}
|
|
||||||
let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement;
|
|
||||||
for track in tracks.iter_mut() {
|
|
||||||
track.player.process(client, scope);
|
|
||||||
}
|
|
||||||
if let ArrangementFocus::Clip(t, s) = selected {
|
|
||||||
if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) {
|
|
||||||
if let Some(track) = tracks.get(*t) {
|
|
||||||
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
|
||||||
let phrase = phrase.read().unwrap();
|
|
||||||
if *playing.read().unwrap() == *phrase {
|
|
||||||
let pulse = self.clock.current.pulse.get();
|
|
||||||
let start = started_at.pulse.get();
|
|
||||||
let now = (pulse - start) % phrase.length as f64;
|
|
||||||
self.editor.now.set(now);
|
|
||||||
return Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.editor.now.set(0.);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl SequencerCli {
|
||||||
//phrase.write().unwrap().length = length;
|
//phrase.write().unwrap().length = length;
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
Ok(SequencerView {
|
Ok(SequencerApp {
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
focus_cursor: (1, 1),
|
focus_cursor: (1, 1),
|
||||||
entered: false,
|
entered: false,
|
||||||
|
|
@ -45,14 +45,3 @@ impl SequencerCli {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JACK process callback for sequencer app
|
|
||||||
impl Audio for SequencerView {
|
|
||||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
if let Some(ref transport) = self.transport {
|
|
||||||
transport.write().unwrap().process(client, scope);
|
|
||||||
}
|
|
||||||
self.player.process(client, scope);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ pub(crate) use std::ffi::OsString;
|
||||||
pub(crate) use std::fs::read_dir;
|
pub(crate) use std::fs::read_dir;
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
|
tui_arrangement
|
||||||
tui_arranger
|
tui_arranger
|
||||||
tui_arranger_bar
|
tui_arranger_bar
|
||||||
tui_arranger_cmd
|
tui_arranger_cmd
|
||||||
tui_arranger_col
|
tui_arranger_col
|
||||||
|
tui_arranger_foc
|
||||||
tui_arranger_hor
|
tui_arranger_hor
|
||||||
tui_arranger_ver
|
tui_arranger_ver
|
||||||
tui_sequencer
|
tui_sequencer
|
||||||
|
|
|
||||||
149
crates/tek_tui/src/tui_arrangement.rs
Normal file
149
crates/tek_tui/src/tui_arrangement.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct ArrangementEditor<E: Engine> {
|
||||||
|
/// Global JACK client
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
/// Global timebase
|
||||||
|
pub clock: Arc<TransportTime>,
|
||||||
|
/// Name of arranger
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Collection of phrases.
|
||||||
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
|
/// Collection of tracks.
|
||||||
|
pub tracks: Vec<ArrangementTrack>,
|
||||||
|
/// Collection of scenes.
|
||||||
|
pub scenes: Vec<Scene>,
|
||||||
|
/// Currently selected element.
|
||||||
|
pub selected: ArrangementEditorFocus,
|
||||||
|
/// Display mode of arranger
|
||||||
|
pub mode: ArrangementViewMode,
|
||||||
|
/// Whether the arranger is currently focused
|
||||||
|
pub focused: bool,
|
||||||
|
/// Background color of arrangement
|
||||||
|
pub color: ItemColor,
|
||||||
|
/// Width and height of arrangement area at last render
|
||||||
|
pub size: Measure<E>,
|
||||||
|
/// Whether this is currently in edit mode
|
||||||
|
pub entered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display mode of arranger
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum ArrangementViewMode {
|
||||||
|
/// Tracks are rows
|
||||||
|
Horizontal,
|
||||||
|
/// Tracks are columns
|
||||||
|
Vertical(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for ArrangementEditor<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Layers::new(move |add|{
|
||||||
|
match self.mode {
|
||||||
|
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
|
||||||
|
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
|
||||||
|
}?;
|
||||||
|
add(&self.size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangementEditorFocus {
|
||||||
|
/// The whole mix is selected
|
||||||
|
Mix,
|
||||||
|
/// A track is selected.
|
||||||
|
Track(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
Scene(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
Clip(usize, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus identification methods
|
||||||
|
impl ArrangementEditorFocus {
|
||||||
|
pub fn description <E: Engine> (
|
||||||
|
&self,
|
||||||
|
tracks: &Vec<ArrangementTrack>,
|
||||||
|
scenes: &Vec<Scene>,
|
||||||
|
) -> String {
|
||||||
|
format!("Selected: {}", match self {
|
||||||
|
Self::Mix => format!("Everything"),
|
||||||
|
Self::Track(t) => match tracks.get(*t) {
|
||||||
|
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
||||||
|
None => format!("T??"),
|
||||||
|
},
|
||||||
|
Self::Scene(s) => match scenes.get(*s) {
|
||||||
|
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
||||||
|
None => format!("S??"),
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||||
|
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||||
|
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{t} S{s}: Empty")
|
||||||
|
},
|
||||||
|
_ => format!("T{t} S{s}: Empty"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } }
|
||||||
|
pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } }
|
||||||
|
pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } }
|
||||||
|
pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } }
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None }
|
||||||
|
}
|
||||||
|
pub fn track_next (&mut self, last_track: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix =>
|
||||||
|
Self::Track(0),
|
||||||
|
Self::Track(t) =>
|
||||||
|
Self::Track(last_track.min(*t + 1)),
|
||||||
|
Self::Scene(s) =>
|
||||||
|
Self::Clip(0, *s),
|
||||||
|
Self::Clip(t, s) =>
|
||||||
|
Self::Clip(last_track.min(*t + 1), *s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix =>
|
||||||
|
Self::Mix,
|
||||||
|
Self::Scene(s) =>
|
||||||
|
Self::Scene(*s),
|
||||||
|
Self::Track(t) =>
|
||||||
|
if *t == 0 { Self::Mix } else { Self::Track(*t - 1) },
|
||||||
|
Self::Clip(t, s) =>
|
||||||
|
if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None }
|
||||||
|
}
|
||||||
|
pub fn scene_next (&mut self, last_scene: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix =>
|
||||||
|
Self::Scene(0),
|
||||||
|
Self::Track(t) =>
|
||||||
|
Self::Clip(*t, 0),
|
||||||
|
Self::Scene(s) =>
|
||||||
|
Self::Scene(last_scene.min(*s + 1)),
|
||||||
|
Self::Clip(t, s) =>
|
||||||
|
Self::Clip(*t, last_scene.min(*s + 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix =>
|
||||||
|
Self::Mix,
|
||||||
|
Self::Track(t) =>
|
||||||
|
Self::Track(*t),
|
||||||
|
Self::Scene(s) =>
|
||||||
|
if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) },
|
||||||
|
Self::Clip(t, s) =>
|
||||||
|
if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Root level object for standalone `tek_arranger`
|
/// Root level object for standalone `tek_arranger`
|
||||||
pub struct ArrangerView<E: Engine> {
|
pub struct ArrangerApp<E: Engine> {
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
/// Which view is focused
|
/// Which view is focused
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct ArrangerView<E: Engine> {
|
||||||
/// Global timebase
|
/// Global timebase
|
||||||
pub clock: Arc<TransportTime>,
|
pub clock: Arc<TransportTime>,
|
||||||
/// Contains all the sequencers.
|
/// Contains all the sequencers.
|
||||||
pub arrangement: Arrangement<E>,
|
pub arrangement: ArrangementEditor<E>,
|
||||||
/// Pool of all phrases in the arrangement
|
/// Pool of all phrases in the arrangement
|
||||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
/// Phrase editor view
|
/// Phrase editor view
|
||||||
|
|
@ -31,102 +31,79 @@ pub struct ArrangerView<E: Engine> {
|
||||||
/// Command history
|
/// Command history
|
||||||
pub history: Vec<ArrangerCommand>,
|
pub history: Vec<ArrangerCommand>,
|
||||||
}
|
}
|
||||||
/// Sections in the arranger app that may be focused
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
impl Audio for ArrangerApp {
|
||||||
pub enum ArrangerFocus {
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
/// The transport (toolbar) is focused
|
if let Some(ref transport) = self.transport {
|
||||||
Transport,
|
transport.write().unwrap().process(client, scope);
|
||||||
/// The arrangement (grid) is focused
|
}
|
||||||
Arrangement,
|
let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement;
|
||||||
/// The phrase list (pool) is focused
|
for track in tracks.iter_mut() {
|
||||||
PhrasePool,
|
track.player.process(client, scope);
|
||||||
/// The phrase editor (sequencer) is focused
|
}
|
||||||
PhraseEditor,
|
if let ArrangementEditorFocus::Clip(t, s) = selected {
|
||||||
|
if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) {
|
||||||
|
if let Some(track) = tracks.get(*t) {
|
||||||
|
if let Some((ref started_at, Some(ref playing))) = track.player.phrase {
|
||||||
|
let phrase = phrase.read().unwrap();
|
||||||
|
if *playing.read().unwrap() == *phrase {
|
||||||
|
let pulse = self.clock.current.pulse.get();
|
||||||
|
let start = started_at.pulse.get();
|
||||||
|
let now = (pulse - start) % phrase.length as f64;
|
||||||
|
self.editor.now.set(now);
|
||||||
|
return Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.editor.now.set(0.);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Status bar for arranger ap
|
|
||||||
pub enum ArrangerStatusBar {
|
/// Layout for standalone arranger app.
|
||||||
Transport,
|
impl Content for ArrangerApp {
|
||||||
ArrangementMix,
|
type Engine = Tui;
|
||||||
ArrangementTrack,
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
ArrangementScene,
|
let focused = self.arrangement.focused;
|
||||||
ArrangementClip,
|
let border_bg = Arranger::<Tui>::border_bg();
|
||||||
PhrasePool,
|
let border_fg = Arranger::<Tui>::border_fg(focused);
|
||||||
PhraseView,
|
let title_fg = Arranger::<Tui>::title_fg(focused);
|
||||||
PhraseEdit,
|
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
|
||||||
|
let entered = if self.arrangement.entered { "■" } else { " " };
|
||||||
|
Split::down(
|
||||||
|
1,
|
||||||
|
row!(menu in self.menu.menus.iter() => {
|
||||||
|
row!(" ", menu.title.as_str(), " ")
|
||||||
|
}),
|
||||||
|
Split::up(
|
||||||
|
1,
|
||||||
|
widget(&self.status),
|
||||||
|
Split::up(
|
||||||
|
1,
|
||||||
|
widget(&self.transport),
|
||||||
|
Split::down(
|
||||||
|
self.arrangement_split,
|
||||||
|
lay!(
|
||||||
|
widget(&self.arrangement).grow_y(1).border(border),
|
||||||
|
widget(&self.arrangement.size),
|
||||||
|
widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1),
|
||||||
|
),
|
||||||
|
Split::right(
|
||||||
|
self.phrases_split,
|
||||||
|
self.phrases.clone(),
|
||||||
|
widget(&self.editor),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Represents the tracks and scenes of the composition.
|
|
||||||
pub struct Arrangement<E: Engine> {
|
|
||||||
/// Global JACK client
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
/// Global timebase
|
|
||||||
pub clock: Arc<TransportTime>,
|
|
||||||
/// Name of arranger
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
/// Collection of phrases.
|
|
||||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
|
||||||
/// Collection of tracks.
|
|
||||||
pub tracks: Vec<ArrangementTrack>,
|
|
||||||
/// Collection of scenes.
|
|
||||||
pub scenes: Vec<Scene>,
|
|
||||||
/// Currently selected element.
|
|
||||||
pub selected: ArrangementFocus,
|
|
||||||
/// Display mode of arranger
|
|
||||||
pub mode: ArrangementViewMode,
|
|
||||||
/// Whether the arranger is currently focused
|
|
||||||
pub focused: bool,
|
|
||||||
/// Background color of arrangement
|
|
||||||
pub color: ItemColor,
|
|
||||||
/// Width and height of arrangement area at last render
|
|
||||||
pub size: Measure<E>,
|
|
||||||
/// Whether this is currently in edit mode
|
|
||||||
pub entered: bool,
|
|
||||||
}
|
|
||||||
/// Represents a track in the arrangement
|
|
||||||
pub struct ArrangementTrack {
|
|
||||||
/// Name of track
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub width: usize,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub color: ItemColor,
|
|
||||||
/// MIDI player/recorder
|
|
||||||
pub player: PhrasePlayer,
|
|
||||||
}
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Scene {
|
|
||||||
/// Name of scene
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub clips: Vec<Option<Arc<RwLock<Phrase>>>>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub color: ItemColor,
|
|
||||||
}
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangementFocus {
|
|
||||||
/// The whole mix is selected
|
|
||||||
Mix,
|
|
||||||
/// A track is selected.
|
|
||||||
Track(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
Scene(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
Clip(usize, usize),
|
|
||||||
}
|
|
||||||
/// Display mode of arranger
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum ArrangementViewMode {
|
|
||||||
/// Tracks are rows
|
|
||||||
Horizontal,
|
|
||||||
/// Tracks are columns
|
|
||||||
Vertical(usize),
|
|
||||||
}
|
|
||||||
/// Arrangement, rendered vertically (session/grid mode).
|
|
||||||
pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement<E>, pub usize);
|
|
||||||
/// Arrangement, rendered horizontally (arrangement/track mode).
|
|
||||||
pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement<E>);
|
|
||||||
/// General methods for arranger
|
/// General methods for arranger
|
||||||
impl<E: Engine> Arranger<E> {
|
impl<E: Engine> ArrangerApp<E> {
|
||||||
pub fn new (
|
pub fn new (
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
jack: &Arc<RwLock<JackClient>>,
|
||||||
transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||||||
|
|
@ -208,6 +185,29 @@ impl<E: Engine> Arranger<E> {
|
||||||
app.update_focus();
|
app.update_focus();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//pub fn new (
|
||||||
|
//jack: &Arc<RwLock<JackClient>>,
|
||||||
|
//clock: &Arc<TransportTime>,
|
||||||
|
//name: &str,
|
||||||
|
//phrases: &Arc<RwLock<PhrasePool<E>>>
|
||||||
|
//) -> Self {
|
||||||
|
//Self {
|
||||||
|
//jack: jack.clone(),
|
||||||
|
//clock: clock.clone(),
|
||||||
|
//name: Arc::new(RwLock::new(name.into())),
|
||||||
|
//mode: ArrangementViewMode::Vertical(2),
|
||||||
|
//selected: ArrangementEditorFocus::Clip(0, 0),
|
||||||
|
//phrases: phrases.clone(),
|
||||||
|
//scenes: vec![],
|
||||||
|
//tracks: vec![],
|
||||||
|
//focused: false,
|
||||||
|
//color: Color::Rgb(28, 35, 25).into(),
|
||||||
|
//size: Measure::new(),
|
||||||
|
//entered: false,
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
/// Toggle global play/pause
|
/// Toggle global play/pause
|
||||||
pub fn toggle_play (&mut self) -> Perhaps<bool> {
|
pub fn toggle_play (&mut self) -> Perhaps<bool> {
|
||||||
match self.transport {
|
match self.transport {
|
||||||
|
|
@ -217,7 +217,7 @@ impl<E: Engine> Arranger<E> {
|
||||||
Ok(Some(true))
|
Ok(Some(true))
|
||||||
}
|
}
|
||||||
pub fn next_color (&self) -> ItemColor {
|
pub fn next_color (&self) -> ItemColor {
|
||||||
if let ArrangementFocus::Clip(track, scene) = self.arrangement.selected {
|
if let ArrangementEditorFocus::Clip(track, scene) = self.arrangement.selected {
|
||||||
let track_color = self.arrangement.tracks[track].color;
|
let track_color = self.arrangement.tracks[track].color;
|
||||||
let scene_color = self.arrangement.scenes[scene].color;
|
let scene_color = self.arrangement.scenes[scene].color;
|
||||||
track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
|
track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25)
|
||||||
|
|
@ -234,20 +234,20 @@ impl<E: Engine> Arranger<E> {
|
||||||
self.arrangement.phrase_put();
|
self.arrangement.phrase_put();
|
||||||
}
|
}
|
||||||
self.show_phrase();
|
self.show_phrase();
|
||||||
self.focus(ArrangerFocus::PhraseEditor);
|
self.focus(ArrangerAppFocus::PhraseEditor);
|
||||||
self.editor.entered = true;
|
self.editor.entered = true;
|
||||||
}
|
}
|
||||||
/// Rename the selected track, scene, or clip
|
/// Rename the selected track, scene, or clip
|
||||||
pub fn rename_selected (&mut self) {
|
pub fn rename_selected (&mut self) {
|
||||||
let Arrangement { selected, ref scenes, .. } = self.arrangement;
|
let Arrangement { selected, ref scenes, .. } = self.arrangement;
|
||||||
match selected {
|
match selected {
|
||||||
ArrangementFocus::Mix => {},
|
ArrangementEditorFocus::Mix => {},
|
||||||
ArrangementFocus::Track(_) => { todo!("rename track"); },
|
ArrangementEditorFocus::Track(_) => { todo!("rename track"); },
|
||||||
ArrangementFocus::Scene(_) => { todo!("rename scene"); },
|
ArrangementEditorFocus::Scene(_) => { todo!("rename scene"); },
|
||||||
ArrangementFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] {
|
ArrangementEditorFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] {
|
||||||
let index = self.phrases.read().unwrap().index_of(&*phrase.read().unwrap());
|
let index = self.phrases.read().unwrap().index_of(&*phrase.read().unwrap());
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
self.focus(ArrangerFocus::PhrasePool);
|
self.focus(ArrangerAppFocus::PhrasePool);
|
||||||
self.phrases.write().unwrap().phrase = index;
|
self.phrases.write().unwrap().phrase = index;
|
||||||
self.phrases.write().unwrap().begin_rename();
|
self.phrases.write().unwrap().begin_rename();
|
||||||
}
|
}
|
||||||
|
|
@ -257,103 +257,23 @@ impl<E: Engine> Arranger<E> {
|
||||||
/// Update status bar
|
/// Update status bar
|
||||||
pub fn update_status (&mut self) {
|
pub fn update_status (&mut self) {
|
||||||
self.status = match self.focused() {
|
self.status = match self.focused() {
|
||||||
ArrangerFocus::Transport => ArrangerStatusBar::Transport,
|
ArrangerAppFocus::Transport => ArrangerStatusBar::Transport,
|
||||||
ArrangerFocus::Arrangement => match self.arrangement.selected {
|
ArrangerAppFocus::Arrangement => match self.arrangement.selected {
|
||||||
ArrangementFocus::Mix => ArrangerStatusBar::ArrangementMix,
|
ArrangementEditorFocus::Mix => ArrangerStatusBar::ArrangementMix,
|
||||||
ArrangementFocus::Track(_) => ArrangerStatusBar::ArrangementTrack,
|
ArrangementEditorFocus::Track(_) => ArrangerStatusBar::ArrangementTrack,
|
||||||
ArrangementFocus::Scene(_) => ArrangerStatusBar::ArrangementScene,
|
ArrangementEditorFocus::Scene(_) => ArrangerStatusBar::ArrangementScene,
|
||||||
ArrangementFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip,
|
ArrangementEditorFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip,
|
||||||
},
|
},
|
||||||
ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool,
|
ArrangerAppFocus::PhrasePool => ArrangerStatusBar::PhrasePool,
|
||||||
ArrangerFocus::PhraseEditor => match self.editor.entered {
|
ArrangerAppFocus::PhraseEditor => match self.editor.entered {
|
||||||
true => ArrangerStatusBar::PhraseEdit,
|
true => ArrangerStatusBar::PhraseEdit,
|
||||||
false => ArrangerStatusBar::PhraseView,
|
false => ArrangerStatusBar::PhraseView,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/// Focus layout of arranger app
|
|
||||||
impl<E: Engine> FocusGrid for Arranger<E> {
|
|
||||||
type Item = ArrangerFocus;
|
|
||||||
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
|
|
||||||
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
|
|
||||||
fn focus_enter (&mut self) {
|
|
||||||
let focused = self.focused();
|
|
||||||
if !self.entered {
|
|
||||||
self.entered = true;
|
|
||||||
use ArrangerFocus::*;
|
|
||||||
if let Some(transport) = self.transport.as_ref() {
|
|
||||||
//transport.write().unwrap().entered = focused == Transport
|
|
||||||
}
|
|
||||||
self.arrangement.entered = focused == Arrangement;
|
|
||||||
self.phrases.write().unwrap().entered = focused == PhrasePool;
|
|
||||||
self.editor.entered = focused == PhraseEditor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn focus_exit (&mut self) {
|
|
||||||
if self.entered {
|
|
||||||
self.entered = false;
|
|
||||||
self.arrangement.entered = false;
|
|
||||||
self.editor.entered = false;
|
|
||||||
self.phrases.write().unwrap().entered = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn entered (&self) -> Option<ArrangerFocus> {
|
|
||||||
if self.entered {
|
|
||||||
Some(self.focused())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn layout (&self) -> &[&[ArrangerFocus]] {
|
|
||||||
use ArrangerFocus::*;
|
|
||||||
&[
|
|
||||||
&[Transport, Transport],
|
|
||||||
&[Arrangement, Arrangement],
|
|
||||||
&[PhrasePool, PhraseEditor],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
fn update_focus (&mut self) {
|
|
||||||
use ArrangerFocus::*;
|
|
||||||
let focused = self.focused();
|
|
||||||
if let Some(transport) = self.transport.as_ref() {
|
|
||||||
transport.write().unwrap().focused = focused == Transport
|
|
||||||
}
|
|
||||||
self.arrangement.focused = focused == Arrangement;
|
|
||||||
self.phrases.write().unwrap().focused = focused == PhrasePool;
|
|
||||||
self.editor.focused = focused == PhraseEditor;
|
|
||||||
self.update_status();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// General methods for arrangement
|
|
||||||
impl<E: Engine> Arrangement<E> {
|
|
||||||
pub fn new (
|
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
|
||||||
clock: &Arc<TransportTime>,
|
|
||||||
name: &str,
|
|
||||||
phrases: &Arc<RwLock<PhrasePool<E>>>
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
jack: jack.clone(),
|
|
||||||
clock: clock.clone(),
|
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
|
||||||
mode: ArrangementViewMode::Vertical(2),
|
|
||||||
selected: ArrangementFocus::Clip(0, 0),
|
|
||||||
phrases: phrases.clone(),
|
|
||||||
scenes: vec![],
|
|
||||||
tracks: vec![],
|
|
||||||
focused: false,
|
|
||||||
color: Color::Rgb(28, 35, 25).into(),
|
|
||||||
size: Measure::new(),
|
|
||||||
entered: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_stopped (&self) -> bool {
|
|
||||||
*self.clock.playing.read().unwrap() == Some(TransportState::Stopped)
|
|
||||||
}
|
|
||||||
pub fn activate (&mut self) {
|
pub fn activate (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Scene(s) => {
|
ArrangementEditorFocus::Scene(s) => {
|
||||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||||
let player = &mut track.player;
|
let player = &mut track.player;
|
||||||
let clip = self.scenes[s].clips[t].as_ref();
|
let clip = self.scenes[s].clips[t].as_ref();
|
||||||
|
|
@ -367,7 +287,7 @@ impl<E: Engine> Arrangement<E> {
|
||||||
//self.transport.toggle_play()
|
//self.transport.toggle_play()
|
||||||
//}
|
//}
|
||||||
},
|
},
|
||||||
ArrangementFocus::Clip(t, s) => {
|
ArrangementEditorFocus::Clip(t, s) => {
|
||||||
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref());
|
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref());
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -375,26 +295,26 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
pub fn delete (&mut self) {
|
pub fn delete (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Track(_) => self.track_del(),
|
ArrangementEditorFocus::Track(_) => self.track_del(),
|
||||||
ArrangementFocus::Scene(_) => self.scene_del(),
|
ArrangementEditorFocus::Scene(_) => self.scene_del(),
|
||||||
ArrangementFocus::Clip(_, _) => self.phrase_del(),
|
ArrangementEditorFocus::Clip(_, _) => self.phrase_del(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn increment (&mut self) {
|
pub fn increment (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Track(_) => self.track_width_inc(),
|
ArrangementEditorFocus::Track(_) => self.track_width_inc(),
|
||||||
ArrangementFocus::Scene(_) => self.scene_next(),
|
ArrangementEditorFocus::Scene(_) => self.scene_next(),
|
||||||
ArrangementFocus::Clip(_, _) => self.phrase_next(),
|
ArrangementEditorFocus::Clip(_, _) => self.phrase_next(),
|
||||||
ArrangementFocus::Mix => self.zoom_in(),
|
ArrangementEditorFocus::Mix => self.zoom_in(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn decrement (&mut self) {
|
pub fn decrement (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Track(_) => self.track_width_dec(),
|
ArrangementEditorFocus::Track(_) => self.track_width_dec(),
|
||||||
ArrangementFocus::Scene(_) => self.scene_prev(),
|
ArrangementEditorFocus::Scene(_) => self.scene_prev(),
|
||||||
ArrangementFocus::Clip(_, _) => self.phrase_prev(),
|
ArrangementEditorFocus::Clip(_, _) => self.phrase_prev(),
|
||||||
ArrangementFocus::Mix => self.zoom_out(),
|
ArrangementEditorFocus::Mix => self.zoom_out(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn zoom_in (&mut self) {
|
pub fn zoom_in (&mut self) {
|
||||||
|
|
@ -414,8 +334,8 @@ impl<E: Engine> Arrangement<E> {
|
||||||
pub fn is_last_row (&self) -> bool {
|
pub fn is_last_row (&self) -> bool {
|
||||||
let selected = self.selected;
|
let selected = self.selected;
|
||||||
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
||||||
ArrangementFocus::Scene(s) => s == self.scenes.len() - 1,
|
ArrangementEditorFocus::Scene(s) => s == self.scenes.len() - 1,
|
||||||
ArrangementFocus::Clip(_, s) => s == self.scenes.len() - 1,
|
ArrangementEditorFocus::Clip(_, s) => s == self.scenes.len() - 1,
|
||||||
_ => false
|
_ => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -450,16 +370,16 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
pub fn move_back (&mut self) {
|
pub fn move_back (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Scene(s) => {
|
ArrangementEditorFocus::Scene(s) => {
|
||||||
if s > 0 {
|
if s > 0 {
|
||||||
self.scenes.swap(s, s - 1);
|
self.scenes.swap(s, s - 1);
|
||||||
self.selected = ArrangementFocus::Scene(s - 1);
|
self.selected = ArrangementEditorFocus::Scene(s - 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ArrangementFocus::Track(t) => {
|
ArrangementEditorFocus::Track(t) => {
|
||||||
if t > 0 {
|
if t > 0 {
|
||||||
self.tracks.swap(t, t - 1);
|
self.tracks.swap(t, t - 1);
|
||||||
self.selected = ArrangementFocus::Track(t - 1);
|
self.selected = ArrangementEditorFocus::Track(t - 1);
|
||||||
// FIXME: also swap clip order in scenes
|
// FIXME: also swap clip order in scenes
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -468,16 +388,16 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
pub fn move_forward (&mut self) {
|
pub fn move_forward (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Scene(s) => {
|
ArrangementEditorFocus::Scene(s) => {
|
||||||
if s < self.scenes.len().saturating_sub(1) {
|
if s < self.scenes.len().saturating_sub(1) {
|
||||||
self.scenes.swap(s, s + 1);
|
self.scenes.swap(s, s + 1);
|
||||||
self.selected = ArrangementFocus::Scene(s + 1);
|
self.selected = ArrangementEditorFocus::Scene(s + 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ArrangementFocus::Track(t) => {
|
ArrangementEditorFocus::Track(t) => {
|
||||||
if t < self.tracks.len().saturating_sub(1) {
|
if t < self.tracks.len().saturating_sub(1) {
|
||||||
self.tracks.swap(t, t + 1);
|
self.tracks.swap(t, t + 1);
|
||||||
self.selected = ArrangementFocus::Track(t + 1);
|
self.selected = ArrangementEditorFocus::Track(t + 1);
|
||||||
// FIXME: also swap clip order in scenes
|
// FIXME: also swap clip order in scenes
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -486,15 +406,16 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
pub fn randomize_color (&mut self) {
|
pub fn randomize_color (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangementFocus::Mix => { self.color = ItemColor::random_dark() },
|
ArrangementEditorFocus::Mix => { self.color = ItemColor::random_dark() },
|
||||||
ArrangementFocus::Track(t) => { self.tracks[t].color = ItemColor::random() },
|
ArrangementEditorFocus::Track(t) => { self.tracks[t].color = ItemColor::random() },
|
||||||
ArrangementFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() },
|
ArrangementEditorFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() },
|
||||||
ArrangementFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
|
ArrangementEditorFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
|
||||||
phrase.write().unwrap().color = ItemColorTriplet::random();
|
phrase.write().unwrap().color = ItemColorTriplet::random();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods for tracks in arrangement
|
/// Methods for tracks in arrangement
|
||||||
impl<E: Engine> Arrangement<E> {
|
impl<E: Engine> Arrangement<E> {
|
||||||
pub fn track (&self) -> Option<&ArrangementTrack> {
|
pub fn track (&self) -> Option<&ArrangementTrack> {
|
||||||
|
|
@ -597,12 +518,12 @@ impl<E: Engine> Arrangement<E> {
|
||||||
.map(|scene|scene.clips[track_index] = None));
|
.map(|scene|scene.clips[track_index] = None));
|
||||||
}
|
}
|
||||||
pub fn phrase_put (&mut self) {
|
pub fn phrase_put (&mut self) {
|
||||||
if let ArrangementFocus::Clip(track, scene) = self.selected {
|
if let ArrangementEditorFocus::Clip(track, scene) = self.selected {
|
||||||
self.scenes[scene].clips[track] = Some(self.phrases.read().unwrap().phrase().clone());
|
self.scenes[scene].clips[track] = Some(self.phrases.read().unwrap().phrase().clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn phrase_get (&mut self) {
|
pub fn phrase_get (&mut self) {
|
||||||
if let ArrangementFocus::Clip(track, scene) = self.selected {
|
if let ArrangementEditorFocus::Clip(track, scene) = self.selected {
|
||||||
if let Some(phrase) = &self.scenes[scene].clips[track] {
|
if let Some(phrase) = &self.scenes[scene].clips[track] {
|
||||||
let mut phrases = self.phrases.write().unwrap();
|
let mut phrases = self.phrases.write().unwrap();
|
||||||
if let Some(index) = phrases.index_of(&*phrase.read().unwrap()) {
|
if let Some(index) = phrases.index_of(&*phrase.read().unwrap()) {
|
||||||
|
|
@ -612,7 +533,7 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn phrase_next (&mut self) {
|
pub fn phrase_next (&mut self) {
|
||||||
if let ArrangementFocus::Clip(track, scene) = self.selected {
|
if let ArrangementEditorFocus::Clip(track, scene) = self.selected {
|
||||||
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
|
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
|
||||||
let phrases = self.phrases.read().unwrap();
|
let phrases = self.phrases.read().unwrap();
|
||||||
let index = phrases.index_of(&*phrase.read().unwrap());
|
let index = phrases.index_of(&*phrase.read().unwrap());
|
||||||
|
|
@ -625,7 +546,7 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn phrase_prev (&mut self) {
|
pub fn phrase_prev (&mut self) {
|
||||||
if let ArrangementFocus::Clip(track, scene) = self.selected {
|
if let ArrangementEditorFocus::Clip(track, scene) = self.selected {
|
||||||
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
|
if let Some(ref mut phrase) = self.scenes[scene].clips[track] {
|
||||||
let phrases = self.phrases.read().unwrap();
|
let phrases = self.phrases.read().unwrap();
|
||||||
let index = phrases.index_of(&*phrase.read().unwrap());
|
let index = phrases.index_of(&*phrase.read().unwrap());
|
||||||
|
|
@ -638,6 +559,7 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArrangementTrack {
|
impl ArrangementTrack {
|
||||||
pub fn new (
|
pub fn new (
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
jack: &Arc<RwLock<JackClient>>,
|
||||||
|
|
@ -659,91 +581,7 @@ impl ArrangementTrack {
|
||||||
pub fn width_inc (&mut self) { self.width += 1; }
|
pub fn width_inc (&mut self) { self.width += 1; }
|
||||||
pub fn width_dec (&mut self) { if self.width > Self::MIN_WIDTH { self.width -= 1; } }
|
pub fn width_dec (&mut self) { if self.width > Self::MIN_WIDTH { self.width -= 1; } }
|
||||||
}
|
}
|
||||||
/// Focus identification methods
|
|
||||||
impl ArrangementFocus {
|
|
||||||
pub fn description <E: Engine> (
|
|
||||||
&self,
|
|
||||||
tracks: &Vec<ArrangementTrack>,
|
|
||||||
scenes: &Vec<Scene>,
|
|
||||||
) -> String {
|
|
||||||
format!("Selected: {}", match self {
|
|
||||||
Self::Mix => format!("Everything"),
|
|
||||||
Self::Track(t) => match tracks.get(*t) {
|
|
||||||
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
|
||||||
None => format!("T??"),
|
|
||||||
},
|
|
||||||
Self::Scene(s) => match scenes.get(*s) {
|
|
||||||
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
|
||||||
None => format!("S??"),
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
|
||||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
|
||||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
|
||||||
None => format!("T{t} S{s}: Empty")
|
|
||||||
},
|
|
||||||
_ => format!("T{t} S{s}: Empty"),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } }
|
|
||||||
pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } }
|
|
||||||
pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } }
|
|
||||||
pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } }
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None }
|
|
||||||
}
|
|
||||||
pub fn track_next (&mut self, last_track: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix =>
|
|
||||||
Self::Track(0),
|
|
||||||
Self::Track(t) =>
|
|
||||||
Self::Track(last_track.min(*t + 1)),
|
|
||||||
Self::Scene(s) =>
|
|
||||||
Self::Clip(0, *s),
|
|
||||||
Self::Clip(t, s) =>
|
|
||||||
Self::Clip(last_track.min(*t + 1), *s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix =>
|
|
||||||
Self::Mix,
|
|
||||||
Self::Scene(s) =>
|
|
||||||
Self::Scene(*s),
|
|
||||||
Self::Track(t) =>
|
|
||||||
if *t == 0 { Self::Mix } else { Self::Track(*t - 1) },
|
|
||||||
Self::Clip(t, s) =>
|
|
||||||
if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None }
|
|
||||||
}
|
|
||||||
pub fn scene_next (&mut self, last_scene: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix =>
|
|
||||||
Self::Scene(0),
|
|
||||||
Self::Track(t) =>
|
|
||||||
Self::Clip(*t, 0),
|
|
||||||
Self::Scene(s) =>
|
|
||||||
Self::Scene(last_scene.min(*s + 1)),
|
|
||||||
Self::Clip(t, s) =>
|
|
||||||
Self::Clip(*t, last_scene.min(*s + 1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix =>
|
|
||||||
Self::Mix,
|
|
||||||
Self::Track(t) =>
|
|
||||||
Self::Track(*t),
|
|
||||||
Self::Scene(s) =>
|
|
||||||
if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) },
|
|
||||||
Self::Clip(t, s) =>
|
|
||||||
if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Arranger display mode can be cycled
|
/// Arranger display mode can be cycled
|
||||||
impl ArrangementViewMode {
|
impl ArrangementViewMode {
|
||||||
/// Cycle arranger display mode
|
/// Cycle arranger display mode
|
||||||
|
|
@ -769,27 +607,6 @@ impl Scene {
|
||||||
color: color.unwrap_or_else(ItemColor::random),
|
color: color.unwrap_or_else(ItemColor::random),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Returns the pulse length of the longest phrase in the scene
|
|
||||||
pub fn pulses (&self) -> usize {
|
|
||||||
self.clips.iter().fold(0, |a, p|{
|
|
||||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// Returns true if all phrases in the scene are currently playing
|
|
||||||
pub fn is_playing (&self, tracks: &[ArrangementTrack]) -> bool {
|
|
||||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
|
||||||
.all(|(track_index, clip)|match clip {
|
|
||||||
Some(clip) => tracks
|
|
||||||
.get(track_index)
|
|
||||||
.map(|track|if let Some((_, Some(phrase))) = &track.player.phrase {
|
|
||||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.unwrap_or(false),
|
|
||||||
None => true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
|
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
if factor == 0 {
|
if factor == 0 {
|
||||||
|
|
@ -807,60 +624,4 @@ impl Scene {
|
||||||
pub fn longest_name (scenes: &[Self]) -> usize {
|
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||||
scenes.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max)
|
scenes.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max)
|
||||||
}
|
}
|
||||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
|
|
||||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout for standalone arranger app.
|
|
||||||
impl Content for Arranger<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let focused = self.arrangement.focused;
|
|
||||||
let border_bg = Arranger::<Tui>::border_bg();
|
|
||||||
let border_fg = Arranger::<Tui>::border_fg(focused);
|
|
||||||
let title_fg = Arranger::<Tui>::title_fg(focused);
|
|
||||||
let border = Lozenge(Style::default().bg(border_bg).fg(border_fg));
|
|
||||||
let entered = if self.arrangement.entered { "■" } else { " " };
|
|
||||||
Split::down(
|
|
||||||
1,
|
|
||||||
row!(menu in self.menu.menus.iter() => {
|
|
||||||
row!(" ", menu.title.as_str(), " ")
|
|
||||||
}),
|
|
||||||
Split::up(
|
|
||||||
1,
|
|
||||||
widget(&self.status),
|
|
||||||
Split::up(
|
|
||||||
1,
|
|
||||||
widget(&self.transport),
|
|
||||||
Split::down(
|
|
||||||
self.arrangement_split,
|
|
||||||
lay!(
|
|
||||||
widget(&self.arrangement).grow_y(1).border(border),
|
|
||||||
widget(&self.arrangement.size),
|
|
||||||
widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1),
|
|
||||||
),
|
|
||||||
Split::right(
|
|
||||||
self.phrases_split,
|
|
||||||
self.phrases.clone(),
|
|
||||||
widget(&self.editor),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for Arrangement<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Layers::new(move |add|{
|
|
||||||
match self.mode {
|
|
||||||
ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) },
|
|
||||||
ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) },
|
|
||||||
}?;
|
|
||||||
add(&self.size)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Status bar for arranger ap
|
||||||
|
pub enum ArrangerStatusBar {
|
||||||
|
Transport,
|
||||||
|
ArrangementMix,
|
||||||
|
ArrangementTrack,
|
||||||
|
ArrangementScene,
|
||||||
|
ArrangementClip,
|
||||||
|
PhrasePool,
|
||||||
|
PhraseView,
|
||||||
|
PhraseEdit,
|
||||||
|
}
|
||||||
impl Content for ArrangerStatusBar {
|
impl Content for ArrangerStatusBar {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
|
||||||
68
crates/tek_tui/src/tui_arranger_foc.rs
Normal file
68
crates/tek_tui/src/tui_arranger_foc.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Sections in the arranger app that may be focused
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ArrangerAppFocus {
|
||||||
|
/// The transport (toolbar) is focused
|
||||||
|
Transport,
|
||||||
|
/// The arrangement (grid) is focused
|
||||||
|
Arrangement,
|
||||||
|
/// The phrase list (pool) is focused
|
||||||
|
PhrasePool,
|
||||||
|
/// The phrase editor (sequencer) is focused
|
||||||
|
PhraseEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus layout of arranger app
|
||||||
|
impl<E: Engine> FocusGrid for ArrangerApp<E> {
|
||||||
|
type Item = ArrangerAppFocus;
|
||||||
|
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
|
||||||
|
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
|
||||||
|
fn focus_enter (&mut self) {
|
||||||
|
let focused = self.focused();
|
||||||
|
if !self.entered {
|
||||||
|
self.entered = true;
|
||||||
|
use ArrangerAppFocus::*;
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
//transport.write().unwrap().entered = focused == Transport
|
||||||
|
}
|
||||||
|
self.arrangement.entered = focused == Arrangement;
|
||||||
|
self.phrases.write().unwrap().entered = focused == PhrasePool;
|
||||||
|
self.editor.entered = focused == PhraseEditor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn focus_exit (&mut self) {
|
||||||
|
if self.entered {
|
||||||
|
self.entered = false;
|
||||||
|
self.arrangement.entered = false;
|
||||||
|
self.editor.entered = false;
|
||||||
|
self.phrases.write().unwrap().entered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn entered (&self) -> Option<ArrangerAppFocus> {
|
||||||
|
if self.entered {
|
||||||
|
Some(self.focused())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn layout (&self) -> &[&[ArrangerAppFocus]] {
|
||||||
|
use ArrangerAppFocus::*;
|
||||||
|
&[
|
||||||
|
&[Transport, Transport],
|
||||||
|
&[Arrangement, Arrangement],
|
||||||
|
&[PhrasePool, PhraseEditor],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
fn update_focus (&mut self) {
|
||||||
|
use ArrangerAppFocus::*;
|
||||||
|
let focused = self.focused();
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
transport.write().unwrap().focused = focused == Transport
|
||||||
|
}
|
||||||
|
self.arrangement.focused = focused == Arrangement;
|
||||||
|
self.phrases.write().unwrap().focused = focused == PhrasePool;
|
||||||
|
self.editor.focused = focused == PhraseEditor;
|
||||||
|
self.update_status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Arrangement, rendered horizontally (arrangement/track mode).
|
||||||
|
pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement<E>);
|
||||||
|
|
||||||
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Arrangement, rendered vertically (session/grid mode).
|
||||||
|
pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement<E>, pub usize);
|
||||||
|
|
||||||
impl<'a> Content for VerticalArranger<'a, Tui> {
|
impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
|
||||||
418
crates/tek_tui/src/tui_phrase.rs
Normal file
418
crates/tek_tui/src/tui_phrase.rs
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Contains state for viewing and editing a phrase
|
||||||
|
pub struct PhraseEditor<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// Phrase being played
|
||||||
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||||||
|
/// Length of note that will be inserted, in pulses
|
||||||
|
pub note_len: usize,
|
||||||
|
/// The full piano keys are rendered to this buffer
|
||||||
|
pub keys: Buffer,
|
||||||
|
/// The full piano roll is rendered to this buffer
|
||||||
|
pub buffer: BigBuffer,
|
||||||
|
/// Cursor/scroll/zoom in pitch axis
|
||||||
|
pub note_axis: RwLock<FixedAxis<usize>>,
|
||||||
|
/// Cursor/scroll/zoom in time axis
|
||||||
|
pub time_axis: RwLock<ScaledAxis<usize>>,
|
||||||
|
/// Whether this widget is focused
|
||||||
|
pub focused: bool,
|
||||||
|
/// Whether note enter mode is enabled
|
||||||
|
pub entered: bool,
|
||||||
|
/// Display mode
|
||||||
|
pub mode: bool,
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Current position of global playhead
|
||||||
|
pub now: Arc<Pulse>,
|
||||||
|
/// Width of notes area at last render
|
||||||
|
pub width: AtomicUsize,
|
||||||
|
/// Height of notes area at last render
|
||||||
|
pub height: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for PhraseEditor<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self;
|
||||||
|
let FixedAxis {
|
||||||
|
start: note_start, point: note_point, clamp: note_clamp
|
||||||
|
} = *self.note_axis.read().unwrap();
|
||||||
|
let ScaledAxis {
|
||||||
|
start: time_start, point: time_point, clamp: time_clamp, scale: time_scale
|
||||||
|
} = *self.time_axis.read().unwrap();
|
||||||
|
//let color = Color::Rgb(0,255,0);
|
||||||
|
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
|
||||||
|
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
||||||
|
Ok(if to.area().h() >= 2 {
|
||||||
|
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
|
||||||
|
let y = y + (note_start / 2) as u16;
|
||||||
|
if x < keys.area.width && y < keys.area.height {
|
||||||
|
*cell = keys.get(x, y).clone()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}).fill_y();
|
||||||
|
let notes_bg_null = Color::Rgb(28, 35, 25);
|
||||||
|
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||||
|
let area = to.area();
|
||||||
|
let h = area.h() as usize;
|
||||||
|
self.height.store(h, Ordering::Relaxed);
|
||||||
|
self.width.store(area.w() as usize, Ordering::Relaxed);
|
||||||
|
let mut axis = self.note_axis.write().unwrap();
|
||||||
|
if let Some(point) = axis.point {
|
||||||
|
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
|
||||||
|
axis.start += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(if to.area().h() >= 2 {
|
||||||
|
let area = to.area();
|
||||||
|
to.buffer_update(area, &move |cell, x, y|{
|
||||||
|
cell.set_bg(notes_bg_null);
|
||||||
|
let src_x = (x as usize + time_start) * time_scale;
|
||||||
|
let src_y = y as usize + note_start / 2;
|
||||||
|
if src_x < buffer.width && src_y < buffer.height - 1 {
|
||||||
|
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
|
||||||
|
cell.set_symbol(src.symbol());
|
||||||
|
cell.set_fg(src.fg);
|
||||||
|
cell.set_bg(src.bg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}).fill_x();
|
||||||
|
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
||||||
|
Ok(if *focused && *entered {
|
||||||
|
let area = to.area();
|
||||||
|
if let (Some(time), Some(note)) = (time_point, note_point) {
|
||||||
|
let x1 = area.x() + (time / time_scale) as u16;
|
||||||
|
let x2 = x1 + (self.note_len / time_scale) as u16;
|
||||||
|
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
|
||||||
|
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||||
|
for x in x1..x2 {
|
||||||
|
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
||||||
|
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
||||||
|
let playhead = CustomWidget::new(
|
||||||
|
|to:[u16;2]|Ok(Some(to.clip_h(1))),
|
||||||
|
move|to: &mut TuiOutput|{
|
||||||
|
if let Some(_) = phrase {
|
||||||
|
let now = self.now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
||||||
|
let time_clamp = time_clamp
|
||||||
|
.expect("time_axis of sequencer expected to be clamped");
|
||||||
|
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
|
||||||
|
let this_step = time_start + (x + 0) * time_scale;
|
||||||
|
let next_step = time_start + (x + 1) * time_scale;
|
||||||
|
let x = to.area().x() + x as u16;
|
||||||
|
let active = this_step <= now && now < next_step;
|
||||||
|
let character = if active { "|" } else { "·" };
|
||||||
|
let style = if active { playhead_active } else { playhead_inactive };
|
||||||
|
to.blit(&character, x, to.area.y(), Some(style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
).push_x(6).align_sw();
|
||||||
|
let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
|
||||||
|
let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
||||||
|
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||||
|
let note_area = lay!(notes, cursor).fill_x();
|
||||||
|
let piano_roll = row!(keys, note_area).fill_x();
|
||||||
|
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
|
||||||
|
let content = lay!(content, playhead);
|
||||||
|
let mut upper_left = format!("[{}] Sequencer", if *entered {"■"} else {" "});
|
||||||
|
if let Some(phrase) = phrase {
|
||||||
|
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
|
||||||
|
}
|
||||||
|
let mut lower_right = format!(
|
||||||
|
"┤{}x{}├",
|
||||||
|
self.width.load(Ordering::Relaxed),
|
||||||
|
self.height.load(Ordering::Relaxed),
|
||||||
|
);
|
||||||
|
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
|
||||||
|
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
|
||||||
|
//pulses_to_name(time_scale),
|
||||||
|
//time_start, time_point.unwrap_or(0),
|
||||||
|
//time_scale, time_clamp.unwrap_or(0),
|
||||||
|
//);
|
||||||
|
if *focused && *entered {
|
||||||
|
lower_right = format!("┤Note: {} {}├─{lower_right}",
|
||||||
|
self.note_axis.read().unwrap().point.unwrap(),
|
||||||
|
pulses_to_name(*note_len));
|
||||||
|
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
|
||||||
|
//pulses_to_name(*note_len),
|
||||||
|
//note_start,
|
||||||
|
//note_point.unwrap_or(0),
|
||||||
|
//note_clamp.unwrap_or(0),
|
||||||
|
//);
|
||||||
|
}
|
||||||
|
let upper_right = if let Some(phrase) = phrase {
|
||||||
|
format!("┤Length: {}├", phrase.read().unwrap().length)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
lay!(
|
||||||
|
content,
|
||||||
|
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||||
|
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||||
|
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> PhraseEditor<E> {
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
phrase: None,
|
||||||
|
note_len: 24,
|
||||||
|
notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
|
notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
|
keys: keys_vert(),
|
||||||
|
buffer: Default::default(),
|
||||||
|
focused: false,
|
||||||
|
entered: false,
|
||||||
|
mode: false,
|
||||||
|
now: Arc::new(0.into()),
|
||||||
|
width: 0.into(),
|
||||||
|
height: 0.into(),
|
||||||
|
note_axis: RwLock::new(FixedAxis {
|
||||||
|
start: 12,
|
||||||
|
point: Some(36),
|
||||||
|
clamp: Some(127)
|
||||||
|
}),
|
||||||
|
time_axis: RwLock::new(ScaledAxis {
|
||||||
|
start: 00,
|
||||||
|
point: Some(00),
|
||||||
|
clamp: Some(000),
|
||||||
|
scale: 24
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_cursor_inc (&self) {
|
||||||
|
let mut axis = self.note_axis.write().unwrap();
|
||||||
|
axis.point_dec(1);
|
||||||
|
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_cursor_dec (&self) {
|
||||||
|
let mut axis = self.note_axis.write().unwrap();
|
||||||
|
axis.point_inc(1);
|
||||||
|
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_page_up (&self) {
|
||||||
|
let mut axis = self.note_axis.write().unwrap();
|
||||||
|
axis.start_dec(3);
|
||||||
|
axis.point_dec(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_page_down (&self) {
|
||||||
|
let mut axis = self.note_axis.write().unwrap();
|
||||||
|
axis.start_inc(3);
|
||||||
|
axis.point_inc(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_scroll_inc (&self) {
|
||||||
|
self.note_axis.write().unwrap().start_dec(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_scroll_dec (&self) {
|
||||||
|
self.note_axis.write().unwrap().start_inc(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_length_inc (&mut self) {
|
||||||
|
self.note_len = next_note_length(self.note_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_length_dec (&mut self) {
|
||||||
|
self.note_len = prev_note_length(self.note_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_cursor_advance (&self) {
|
||||||
|
let point = self.time_axis.read().unwrap().point;
|
||||||
|
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||||
|
let forward = |time|(time + self.note_len) % length;
|
||||||
|
self.time_axis.write().unwrap().point = point.map(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_cursor_inc (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().point_inc(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_cursor_dec (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().point_dec(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_scroll_inc (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().start_inc(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_scroll_dec (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().start_dec(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_zoom_in (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().scale = prev_note_length(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_zoom_out (&self) {
|
||||||
|
let scale = self.time_axis.read().unwrap().scale;
|
||||||
|
self.time_axis.write().unwrap().scale = next_note_length(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> PhraseEditor<E> {
|
||||||
|
pub fn put (&mut self) {
|
||||||
|
if let (Some(phrase), Some(time), Some(note)) = (
|
||||||
|
&self.phrase,
|
||||||
|
self.time_axis.read().unwrap().point,
|
||||||
|
self.note_axis.read().unwrap().point,
|
||||||
|
) {
|
||||||
|
let mut phrase = phrase.write().unwrap();
|
||||||
|
let key: u7 = u7::from((127 - note) as u8);
|
||||||
|
let vel: u7 = 100.into();
|
||||||
|
let start = time;
|
||||||
|
let end = (start + self.note_len) % phrase.length;
|
||||||
|
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
||||||
|
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
||||||
|
self.buffer = Self::redraw(&phrase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||||
|
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||||
|
if let Some(phrase) = phrase {
|
||||||
|
self.phrase = Some(phrase.clone());
|
||||||
|
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
|
||||||
|
self.buffer = Self::redraw(&*phrase.read().unwrap());
|
||||||
|
} else {
|
||||||
|
self.phrase = None;
|
||||||
|
self.time_axis.write().unwrap().clamp = Some(0);
|
||||||
|
self.buffer = Default::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn redraw (phrase: &Phrase) -> BigBuffer {
|
||||||
|
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
|
||||||
|
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
|
||||||
|
Self::fill_seq_fg(&mut buf, &phrase);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
|
||||||
|
for x in 0..buf.width {
|
||||||
|
// Only fill as far as phrase length
|
||||||
|
if x as usize >= length { break }
|
||||||
|
// Fill each row with background characters
|
||||||
|
for y in 0 .. buf.height {
|
||||||
|
buf.get_mut(x, y).map(|cell|{
|
||||||
|
cell.set_char(if ppq == 0 {
|
||||||
|
'·'
|
||||||
|
} else if x % (4 * ppq) == 0 {
|
||||||
|
'│'
|
||||||
|
} else if x % ppq == 0 {
|
||||||
|
'╎'
|
||||||
|
} else {
|
||||||
|
'·'
|
||||||
|
});
|
||||||
|
cell.set_fg(Color::Rgb(48, 64, 56));
|
||||||
|
cell.modifier = Modifier::DIM;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
|
||||||
|
let mut notes_on = [false;128];
|
||||||
|
for x in 0..buf.width {
|
||||||
|
if x as usize >= phrase.length {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let Some(notes) = phrase.notes.get(x as usize) {
|
||||||
|
if phrase.percussive {
|
||||||
|
for note in notes {
|
||||||
|
match note {
|
||||||
|
MidiMessage::NoteOn { key, .. } =>
|
||||||
|
notes_on[key.as_int() as usize] = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for note in notes {
|
||||||
|
match note {
|
||||||
|
MidiMessage::NoteOn { key, .. } =>
|
||||||
|
notes_on[key.as_int() as usize] = true,
|
||||||
|
MidiMessage::NoteOff { key, .. } =>
|
||||||
|
notes_on[key.as_int() as usize] = false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for y in 0..buf.height {
|
||||||
|
if y >= 64 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let Some(block) = half_block(
|
||||||
|
notes_on[y as usize * 2],
|
||||||
|
notes_on[y as usize * 2 + 1],
|
||||||
|
) {
|
||||||
|
buf.get_mut(x, y).map(|cell|{
|
||||||
|
cell.set_char(block);
|
||||||
|
cell.set_fg(Color::White);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if phrase.percussive {
|
||||||
|
notes_on.fill(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Colors of piano keys
|
||||||
|
const KEY_COLORS: [(Color, Color);6] = [
|
||||||
|
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
||||||
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||||
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||||
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||||
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||||
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) fn keys_vert () -> Buffer {
|
||||||
|
let area = [0, 0, 5, 64];
|
||||||
|
let mut buffer = Buffer::empty(Rect {
|
||||||
|
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
||||||
|
});
|
||||||
|
buffer_update(&mut buffer, area, &|cell, x, y| {
|
||||||
|
let y = 63 - y;
|
||||||
|
match x {
|
||||||
|
0 => {
|
||||||
|
cell.set_char('▀');
|
||||||
|
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
||||||
|
cell.set_fg(fg);
|
||||||
|
cell.set_bg(bg);
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
cell.set_char('▀');
|
||||||
|
cell.set_fg(Color::White);
|
||||||
|
cell.set_bg(Color::White);
|
||||||
|
},
|
||||||
|
2 => if y % 6 == 0 { cell.set_char('C'); },
|
||||||
|
3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buffer
|
||||||
|
}
|
||||||
95
crates/tek_tui/src/tui_phrase_cmd.rs
Normal file
95
crates/tek_tui/src/tui_phrase_cmd.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum PhraseEditorCommand {
|
||||||
|
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||||
|
ToggleDirection,
|
||||||
|
EnterEditMode,
|
||||||
|
ExitEditMode,
|
||||||
|
NoteAppend,
|
||||||
|
NoteSet,
|
||||||
|
NoteCursorSet(usize),
|
||||||
|
NoteLengthSet(usize),
|
||||||
|
NoteScrollSet(usize),
|
||||||
|
TimeCursorSet(usize),
|
||||||
|
TimeScrollSet(usize),
|
||||||
|
TimeZoomSet(usize),
|
||||||
|
Go(Direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for PhraseEditor<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
PhraseEditorCommand::execute_with_state(self, from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
|
||||||
|
fn input_to_command (_: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Char('`')) => Some(Self::ToggleDirection),
|
||||||
|
key!(KeyCode::Enter) => Some(Self::EnterEditMode),
|
||||||
|
key!(KeyCode::Esc) => Some(Self::ExitEditMode),
|
||||||
|
key!(KeyCode::Char('[')) => Some(Self::NoteLengthDec),
|
||||||
|
key!(KeyCode::Char(']')) => Some(Self::NoteLengthInc),
|
||||||
|
key!(KeyCode::Char('a')) => Some(Self::NoteAppend),
|
||||||
|
key!(KeyCode::Char('s')) => Some(Self::NoteSet),
|
||||||
|
key!(KeyCode::Char('-')) => Some(Self::TimeZoomOut),
|
||||||
|
key!(KeyCode::Char('_')) => Some(Self::TimeZoomOut),
|
||||||
|
key!(KeyCode::Char('=')) => Some(Self::TimeZoomIn),
|
||||||
|
key!(KeyCode::Char('+')) => Some(Self::TimeZoomIn),
|
||||||
|
key!(KeyCode::PageUp) => Some(Self::NotePageUp),
|
||||||
|
key!(KeyCode::PageDown) => Some(Self::NotePageDown),
|
||||||
|
key!(KeyCode::Up) => Some(Self::GoUp),
|
||||||
|
key!(KeyCode::Down) => Some(Self::GoDown),
|
||||||
|
key!(KeyCode::Left) => Some(Self::GoLeft),
|
||||||
|
key!(KeyCode::Right) => Some(Self::GoRight),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
|
||||||
|
fn translate (self, state: &PhraseEditor<E>) -> Self {
|
||||||
|
use PhraseEditorCommand::*;
|
||||||
|
match self {
|
||||||
|
GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
|
||||||
|
GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
|
||||||
|
GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, },
|
||||||
|
GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, },
|
||||||
|
_ => self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
|
||||||
|
use PhraseEditorCommand::*;
|
||||||
|
match self.translate(state) {
|
||||||
|
ToggleDirection => { state.mode = !state.mode; },
|
||||||
|
EnterEditMode => { state.entered = true; },
|
||||||
|
ExitEditMode => { state.entered = false; },
|
||||||
|
TimeZoomOut => { state.time_zoom_out() },
|
||||||
|
TimeZoomIn => { state.time_zoom_in() },
|
||||||
|
TimeCursorDec => { state.time_cursor_dec() },
|
||||||
|
TimeCursorInc => { state.time_cursor_inc() },
|
||||||
|
TimeScrollDec => { state.time_scroll_dec() },
|
||||||
|
TimeScrollInc => { state.time_scroll_inc() },
|
||||||
|
NoteCursorDec => { state.note_cursor_dec() },
|
||||||
|
NoteCursorInc => { state.note_cursor_inc() },
|
||||||
|
NoteScrollDec => { state.note_scroll_dec() },
|
||||||
|
NoteScrollInc => { state.note_scroll_inc() },
|
||||||
|
NoteLengthDec => { state.note_length_dec() },
|
||||||
|
NoteLengthInc => { state.note_length_inc() },
|
||||||
|
NotePageUp => { state.note_page_up() },
|
||||||
|
NotePageDown => { state.note_page_down() },
|
||||||
|
NoteAppend => {
|
||||||
|
if state.entered {
|
||||||
|
state.put();
|
||||||
|
state.time_cursor_advance();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NoteSet => {
|
||||||
|
if state.entered { state.put(); }
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
232
crates/tek_tui/src/tui_pool.rs
Normal file
232
crates/tek_tui/src/tui_pool.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub struct PhrasePoolView<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
state: PhrasePool,
|
||||||
|
/// Scroll offset
|
||||||
|
pub scroll: usize,
|
||||||
|
/// Mode switch
|
||||||
|
pub mode: Option<PhrasePoolMode>,
|
||||||
|
/// Whether this widget is focused
|
||||||
|
pub focused: bool,
|
||||||
|
/// Whether this widget is entered
|
||||||
|
pub entered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modes for phrase pool
|
||||||
|
pub enum PhrasePoolMode {
|
||||||
|
/// Renaming a pattern
|
||||||
|
Rename(usize, String),
|
||||||
|
/// Editing the length of a pattern
|
||||||
|
Length(usize, usize, PhraseLengthFocus),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays and edits phrase length.
|
||||||
|
pub struct PhraseLength<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// Pulses per beat (quaver)
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Beats per bar
|
||||||
|
pub bpb: usize,
|
||||||
|
/// Length of phrase in pulses
|
||||||
|
pub pulses: usize,
|
||||||
|
/// Selected subdivision
|
||||||
|
pub focus: Option<PhraseLengthFocus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> PhraseLength<E> {
|
||||||
|
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||||
|
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
||||||
|
}
|
||||||
|
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
|
||||||
|
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
|
||||||
|
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
|
||||||
|
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
|
||||||
|
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
|
||||||
|
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focused field of `PhraseLength`
|
||||||
|
#[derive(Copy, Clone)] pub enum PhraseLengthFocus {
|
||||||
|
/// Editing the number of bars
|
||||||
|
Bar,
|
||||||
|
/// Editing the number of beats
|
||||||
|
Beat,
|
||||||
|
/// Editing the number of ticks
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhraseLengthFocus {
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Bar => Self::Beat,
|
||||||
|
Self::Beat => Self::Tick,
|
||||||
|
Self::Tick => Self::Bar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Bar => Self::Tick,
|
||||||
|
Self::Beat => Self::Bar,
|
||||||
|
Self::Tick => Self::Beat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> PhrasePool<E> {
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
scroll: 0,
|
||||||
|
phrase: 0,
|
||||||
|
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
|
||||||
|
mode: None,
|
||||||
|
focused: false,
|
||||||
|
entered: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn len (&self) -> usize { self.phrases.len() }
|
||||||
|
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
|
||||||
|
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
|
||||||
|
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
|
||||||
|
pub fn index_before (&self, index: usize) -> usize {
|
||||||
|
index.overflowing_sub(1).0.min(self.len() - 1)
|
||||||
|
}
|
||||||
|
pub fn index_after (&self, index: usize) -> usize {
|
||||||
|
(index + 1) % self.len()
|
||||||
|
}
|
||||||
|
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
|
||||||
|
for i in 0..self.phrases.len() {
|
||||||
|
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
|
||||||
|
Arc::new(RwLock::new(Phrase::new(
|
||||||
|
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
pub fn delete_selected (&mut self) {
|
||||||
|
if self.phrase > 0 {
|
||||||
|
self.phrases.remove(self.phrase);
|
||||||
|
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
|
||||||
|
self.phrases.push(Self::new_phrase(name, color));
|
||||||
|
self.phrase = self.phrases.len() - 1;
|
||||||
|
}
|
||||||
|
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
|
||||||
|
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
|
||||||
|
self.phrase += 1;
|
||||||
|
}
|
||||||
|
pub fn insert_dup (&mut self) {
|
||||||
|
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
|
||||||
|
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||||
|
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
|
||||||
|
self.phrase += 1;
|
||||||
|
}
|
||||||
|
pub fn randomize_color (&mut self) {
|
||||||
|
let mut phrase = self.phrases[self.phrase].write().unwrap();
|
||||||
|
phrase.color = ItemColorTriplet::random();
|
||||||
|
}
|
||||||
|
pub fn begin_rename (&mut self) {
|
||||||
|
self.mode = Some(PhrasePoolMode::Rename(
|
||||||
|
self.phrase,
|
||||||
|
self.phrases[self.phrase].read().unwrap().name.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub fn begin_length (&mut self) {
|
||||||
|
self.mode = Some(PhrasePoolMode::Length(
|
||||||
|
self.phrase,
|
||||||
|
self.phrases[self.phrase].read().unwrap().length,
|
||||||
|
PhraseLengthFocus::Bar
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub fn move_up (&mut self) {
|
||||||
|
if self.phrase > 1 {
|
||||||
|
self.phrases.swap(self.phrase - 1, self.phrase);
|
||||||
|
self.phrase -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn move_down (&mut self) {
|
||||||
|
if self.phrase < self.phrases.len().saturating_sub(1) {
|
||||||
|
self.phrases.swap(self.phrase + 1, self.phrase);
|
||||||
|
self.phrase += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Display phrases always in order of appearance
|
||||||
|
impl Content for PhrasePool<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
let Self { focused, phrases, mode, .. } = self;
|
||||||
|
let content = col!(
|
||||||
|
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
||||||
|
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||||
|
let mut length = PhraseLength::new(length, None);
|
||||||
|
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
|
||||||
|
if *focused && i == *phrase {
|
||||||
|
length.pulses = *new_length;
|
||||||
|
length.focus = Some(*focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let length = length.align_e().fill_x();
|
||||||
|
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
||||||
|
let mut row2 = format!(" {name}");
|
||||||
|
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
|
||||||
|
if *focused && i == *phrase { row2 = format!("{row2}▄"); }
|
||||||
|
};
|
||||||
|
let row2 = TuiStyle::bold(row2, true);
|
||||||
|
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
||||||
|
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
||||||
|
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
||||||
|
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
||||||
|
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
||||||
|
let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "});
|
||||||
|
let upper_right = format!("({})", phrases.len());
|
||||||
|
lay!(
|
||||||
|
content,
|
||||||
|
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
||||||
|
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for PhraseLength<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Layers::new(move|add|{
|
||||||
|
match self.focus {
|
||||||
|
None => add(&row!(
|
||||||
|
" ", self.bars_string(),
|
||||||
|
".", self.beats_string(),
|
||||||
|
".", self.ticks_string(),
|
||||||
|
" "
|
||||||
|
)),
|
||||||
|
Some(PhraseLengthFocus::Bar) => add(&row!(
|
||||||
|
"[", self.bars_string(),
|
||||||
|
"]", self.beats_string(),
|
||||||
|
".", self.ticks_string(),
|
||||||
|
" "
|
||||||
|
)),
|
||||||
|
Some(PhraseLengthFocus::Beat) => add(&row!(
|
||||||
|
" ", self.bars_string(),
|
||||||
|
"[", self.beats_string(),
|
||||||
|
"]", self.ticks_string(),
|
||||||
|
" "
|
||||||
|
)),
|
||||||
|
Some(PhraseLengthFocus::Tick) => add(&row!(
|
||||||
|
" ", self.bars_string(),
|
||||||
|
".", self.beats_string(),
|
||||||
|
"[", self.ticks_string(),
|
||||||
|
"]"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
225
crates/tek_tui/src/tui_pool_cmd.rs
Normal file
225
crates/tek_tui/src/tui_pool_cmd.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum PhrasePoolCommand {
|
||||||
|
Prev,
|
||||||
|
Next,
|
||||||
|
MoveUp,
|
||||||
|
MoveDown,
|
||||||
|
Delete,
|
||||||
|
Append,
|
||||||
|
Insert,
|
||||||
|
Duplicate,
|
||||||
|
RandomColor,
|
||||||
|
Edit,
|
||||||
|
Import,
|
||||||
|
Export,
|
||||||
|
Rename(PhraseRenameCommand),
|
||||||
|
Length(PhraseLengthCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum PhraseRenameCommand {
|
||||||
|
Begin,
|
||||||
|
Backspace,
|
||||||
|
Append(char),
|
||||||
|
Set(String),
|
||||||
|
Confirm,
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum PhraseLengthCommand {
|
||||||
|
Begin,
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Inc,
|
||||||
|
Dec,
|
||||||
|
Set(usize),
|
||||||
|
Confirm,
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for PhrasePool<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
if let Some(command) = PhrasePoolCommand::input_to_command(self, from) {
|
||||||
|
let _undo = command.execute(self)?;
|
||||||
|
return Ok(Some(true))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputToCommand<Tui, PhrasePool<Tui>> for PhrasePoolCommand {
|
||||||
|
fn input_to_command (state: &PhrasePool<Tui>, input: &TuiInput) -> Option<Self> {
|
||||||
|
match input.event() {
|
||||||
|
key!(KeyCode::Up) => Some(Self::Prev),
|
||||||
|
key!(KeyCode::Down) => Some(Self::Next),
|
||||||
|
key!(KeyCode::Char(',')) => Some(Self::MoveUp),
|
||||||
|
key!(KeyCode::Char('.')) => Some(Self::MoveDown),
|
||||||
|
key!(KeyCode::Delete) => Some(Self::Delete),
|
||||||
|
key!(KeyCode::Char('a')) => Some(Self::Append),
|
||||||
|
key!(KeyCode::Char('i')) => Some(Self::Insert),
|
||||||
|
key!(KeyCode::Char('d')) => Some(Self::Duplicate),
|
||||||
|
key!(KeyCode::Char('c')) => Some(Self::RandomColor),
|
||||||
|
key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)),
|
||||||
|
key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)),
|
||||||
|
_ => match state.mode {
|
||||||
|
Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input)
|
||||||
|
.map(Self::Rename),
|
||||||
|
Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input)
|
||||||
|
.map(Self::Length),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseRenameCommand {
|
||||||
|
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Backspace) => Some(Self::Backspace),
|
||||||
|
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||||
|
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||||
|
key!(KeyCode::Char(c)) => Some(Self::Append(*c)),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseLengthCommand {
|
||||||
|
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Up) => Some(Self::Inc),
|
||||||
|
key!(KeyCode::Down) => Some(Self::Dec),
|
||||||
|
key!(KeyCode::Right) => Some(Self::Next),
|
||||||
|
key!(KeyCode::Left) => Some(Self::Prev),
|
||||||
|
key!(KeyCode::Enter) => Some(Self::Confirm),
|
||||||
|
key!(KeyCode::Esc) => Some(Self::Cancel),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
|
||||||
|
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||||
|
use PhrasePoolCommand::*;
|
||||||
|
use PhraseRenameCommand as Rename;
|
||||||
|
use PhraseLengthCommand as Length;
|
||||||
|
match self {
|
||||||
|
Rename(Rename::Begin) => { state.begin_rename() },
|
||||||
|
Length(Length::Begin) => { state.begin_length() },
|
||||||
|
Prev => { state.select_prev() },
|
||||||
|
Next => { state.select_next() },
|
||||||
|
Delete => { state.delete_selected() },
|
||||||
|
Append => { state.append_new(None, None) },
|
||||||
|
Insert => { state.insert_new(None, None) },
|
||||||
|
Duplicate => { state.insert_dup() },
|
||||||
|
RandomColor => { state.randomize_color() },
|
||||||
|
MoveUp => { state.move_up() },
|
||||||
|
MoveDown => { state.move_down() },
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
|
||||||
|
fn translate (self, state: &PhrasePool<E>) -> Self {
|
||||||
|
use PhraseRenameCommand::*;
|
||||||
|
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode {
|
||||||
|
match self {
|
||||||
|
Backspace => {
|
||||||
|
let mut new_name = old_name.clone();
|
||||||
|
new_name.pop();
|
||||||
|
return Self::Set(new_name)
|
||||||
|
},
|
||||||
|
Append(c) => {
|
||||||
|
let mut new_name = old_name.clone();
|
||||||
|
new_name.push(c);
|
||||||
|
return Self::Set(new_name)
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else if self != Begin {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||||
|
use PhraseRenameCommand::*;
|
||||||
|
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
|
||||||
|
match self {
|
||||||
|
Set(s) => {
|
||||||
|
state.phrases[phrase].write().unwrap().name = s.into();
|
||||||
|
return Ok(Some(Self::Set(old_name.clone())))
|
||||||
|
},
|
||||||
|
Confirm => {
|
||||||
|
let old_name = old_name.clone();
|
||||||
|
state.mode = None;
|
||||||
|
return Ok(Some(Self::Set(old_name)))
|
||||||
|
},
|
||||||
|
Cancel => {
|
||||||
|
let mut phrase = state.phrases[phrase].write().unwrap();
|
||||||
|
phrase.name = old_name.clone();
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
Ok(None)
|
||||||
|
} else if self == Begin {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
|
||||||
|
fn translate (self, state: &PhrasePool<E>) -> Self {
|
||||||
|
use PhraseLengthCommand::*;
|
||||||
|
if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode {
|
||||||
|
match self {
|
||||||
|
Confirm => { return Self::Set(length) },
|
||||||
|
_ => self
|
||||||
|
}
|
||||||
|
} else if self == Begin {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
||||||
|
use PhraseLengthFocus::*;
|
||||||
|
use PhraseLengthCommand::*;
|
||||||
|
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
|
||||||
|
match self {
|
||||||
|
Cancel => { state.mode = None; },
|
||||||
|
Prev => { focus.prev() },
|
||||||
|
Next => { focus.next() },
|
||||||
|
Inc => match focus {
|
||||||
|
Bar => { *length += 4 * PPQ },
|
||||||
|
Beat => { *length += PPQ },
|
||||||
|
Tick => { *length += 1 },
|
||||||
|
},
|
||||||
|
Dec => match focus {
|
||||||
|
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||||
|
Beat => { *length = length.saturating_sub(PPQ) },
|
||||||
|
Tick => { *length = length.saturating_sub(1) },
|
||||||
|
},
|
||||||
|
Set(length) => {
|
||||||
|
let mut phrase = state.phrases[phrase].write().unwrap();
|
||||||
|
let old_length = phrase.length;
|
||||||
|
phrase.length = length;
|
||||||
|
state.mode = None;
|
||||||
|
return Ok(Some(Self::Set(old_length)))
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
} else if self == Begin {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,20 @@ impl<E: Engine> SamplerView<E> {
|
||||||
modal: Default::default()
|
modal: Default::default()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
/// Immutable reference to sample at cursor.
|
||||||
|
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
||||||
|
for (i, sample) in self.mapped.values().enumerate() {
|
||||||
|
if i == self.cursor.0 {
|
||||||
|
return Some(sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, sample) in self.unmapped.iter().enumerate() {
|
||||||
|
if i + self.mapped.len() == self.cursor.0 {
|
||||||
|
return Some(sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sound sample.
|
/// A sound sample.
|
||||||
|
|
@ -49,7 +63,6 @@ pub struct Sample {
|
||||||
pub rate: Option<usize>,
|
pub rate: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Load sample from WAV and assign to MIDI note.
|
/// Load sample from WAV and assign to MIDI note.
|
||||||
#[macro_export] macro_rules! sample {
|
#[macro_export] macro_rules! sample {
|
||||||
($note:expr, $name:expr, $src:expr) => {{
|
($note:expr, $name:expr, $src:expr) => {{
|
||||||
|
|
@ -303,35 +316,7 @@ impl Sample {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A currently playing instance of a sample.
|
impl Widget for SamplerView<Tui> {
|
||||||
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 = 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 = self.position + 1;
|
|
||||||
return sample.channels[0].get(position).map(|_amplitude|[
|
|
||||||
sample.channels[0][position] * self.velocity,
|
|
||||||
sample.channels[0][position] * self.velocity,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Sampler<Tui> {
|
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
@ -341,7 +326,7 @@ impl Widget for Sampler<Tui> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
|
pub fn tui_render_sampler (sampler: &SamplerView<Tui>, to: &mut TuiOutput) -> Usually<()> {
|
||||||
let [x, y, _, height] = to.area();
|
let [x, y, _, height] = to.area();
|
||||||
let style = Style::default().gray();
|
let style = Style::default().gray();
|
||||||
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
|
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use std::cmp::PartialEq;
|
use std::cmp::PartialEq;
|
||||||
/// MIDI message structural
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
||||||
/// MIDI message serialized
|
|
||||||
pub type PhraseMessage = Vec<u8>;
|
|
||||||
/// Collection of serialized MIDI messages
|
|
||||||
pub type PhraseChunk = [Vec<PhraseMessage>];
|
|
||||||
/// Root level object for standalone `tek_sequencer`
|
/// Root level object for standalone `tek_sequencer`
|
||||||
pub struct SequencerView<E: Engine> {
|
pub struct SequencerApp<E: Engine> {
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
/// Controls the JACK transport.
|
/// Controls the JACK transport.
|
||||||
|
|
@ -25,394 +20,8 @@ pub struct SequencerView<E: Engine> {
|
||||||
/// Whether the currently focused item is entered
|
/// Whether the currently focused item is entered
|
||||||
pub entered: bool,
|
pub entered: bool,
|
||||||
}
|
}
|
||||||
pub struct PhrasePoolView<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
impl Content for SequencerApp<Tui> {
|
||||||
state: PhrasePool,
|
|
||||||
/// Scroll offset
|
|
||||||
pub scroll: usize,
|
|
||||||
/// Mode switch
|
|
||||||
pub mode: Option<PhrasePoolMode>,
|
|
||||||
/// Whether this widget is focused
|
|
||||||
pub focused: bool,
|
|
||||||
/// Whether this widget is entered
|
|
||||||
pub entered: bool,
|
|
||||||
}
|
|
||||||
/// Sections in the sequencer app that may be focused
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
|
|
||||||
/// The transport (toolbar) is focused
|
|
||||||
Transport,
|
|
||||||
/// The phrase list (pool) is focused
|
|
||||||
PhrasePool,
|
|
||||||
/// The phrase editor (sequencer) is focused
|
|
||||||
PhraseEditor,
|
|
||||||
}
|
|
||||||
/// Status bar for sequencer app
|
|
||||||
pub enum SequencerStatusBar {
|
|
||||||
Transport,
|
|
||||||
PhrasePool,
|
|
||||||
PhraseEditor,
|
|
||||||
}
|
|
||||||
/// Modes for phrase pool
|
|
||||||
pub enum PhrasePoolMode {
|
|
||||||
/// Renaming a pattern
|
|
||||||
Rename(usize, String),
|
|
||||||
/// Editing the length of a pattern
|
|
||||||
Length(usize, usize, PhraseLengthFocus),
|
|
||||||
}
|
|
||||||
/// Contains state for viewing and editing a phrase
|
|
||||||
pub struct PhraseEditor<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Phrase being played
|
|
||||||
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
|
||||||
/// Length of note that will be inserted, in pulses
|
|
||||||
pub note_len: usize,
|
|
||||||
/// The full piano keys are rendered to this buffer
|
|
||||||
pub keys: Buffer,
|
|
||||||
/// The full piano roll is rendered to this buffer
|
|
||||||
pub buffer: BigBuffer,
|
|
||||||
/// Cursor/scroll/zoom in pitch axis
|
|
||||||
pub note_axis: RwLock<FixedAxis<usize>>,
|
|
||||||
/// Cursor/scroll/zoom in time axis
|
|
||||||
pub time_axis: RwLock<ScaledAxis<usize>>,
|
|
||||||
/// Whether this widget is focused
|
|
||||||
pub focused: bool,
|
|
||||||
/// Whether note enter mode is enabled
|
|
||||||
pub entered: bool,
|
|
||||||
/// Display mode
|
|
||||||
pub mode: bool,
|
|
||||||
/// Notes currently held at input
|
|
||||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Notes currently held at output
|
|
||||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Current position of global playhead
|
|
||||||
pub now: Arc<Pulse>,
|
|
||||||
/// Width of notes area at last render
|
|
||||||
pub width: AtomicUsize,
|
|
||||||
/// Height of notes area at last render
|
|
||||||
pub height: AtomicUsize,
|
|
||||||
}
|
|
||||||
/// Phrase player.
|
|
||||||
pub struct PhrasePlayer {
|
|
||||||
/// Global timebase
|
|
||||||
pub clock: Arc<TransportTime>,
|
|
||||||
/// Start time and phrase being played
|
|
||||||
pub phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
/// Start time and next phrase
|
|
||||||
pub next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
/// Play input through output.
|
|
||||||
pub monitoring: bool,
|
|
||||||
/// Write input to sequence.
|
|
||||||
pub recording: bool,
|
|
||||||
/// Overdub input to sequence.
|
|
||||||
pub overdub: bool,
|
|
||||||
/// Send all notes off
|
|
||||||
pub reset: bool, // TODO?: after Some(nframes)
|
|
||||||
/// Record from MIDI ports to current sequence.
|
|
||||||
pub midi_inputs: Vec<Port<MidiIn>>,
|
|
||||||
/// Play from current sequence to MIDI ports
|
|
||||||
pub midi_outputs: Vec<Port<MidiOut>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub midi_note: Vec<u8>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub midi_chunk: Vec<Vec<Vec<u8>>>,
|
|
||||||
/// Notes currently held at input
|
|
||||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Notes currently held at output
|
|
||||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
}
|
|
||||||
/// Displays and edits phrase length.
|
|
||||||
pub struct PhraseLength<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Pulses per beat (quaver)
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Beats per bar
|
|
||||||
pub bpb: usize,
|
|
||||||
/// Length of phrase in pulses
|
|
||||||
pub pulses: usize,
|
|
||||||
/// Selected subdivision
|
|
||||||
pub focus: Option<PhraseLengthFocus>,
|
|
||||||
}
|
|
||||||
/// Focused field of `PhraseLength`
|
|
||||||
#[derive(Copy, Clone)] pub enum PhraseLengthFocus {
|
|
||||||
/// Editing the number of bars
|
|
||||||
Bar,
|
|
||||||
/// Editing the number of beats
|
|
||||||
Beat,
|
|
||||||
/// Editing the number of ticks
|
|
||||||
Tick,
|
|
||||||
}
|
|
||||||
/// Focus layout of sequencer app
|
|
||||||
impl<E: Engine> FocusGrid for Sequencer<E> {
|
|
||||||
type Item = SequencerFocus;
|
|
||||||
fn cursor (&self) -> (usize, usize) {
|
|
||||||
self.focus_cursor
|
|
||||||
}
|
|
||||||
fn cursor_mut (&mut self) -> &mut (usize, usize) {
|
|
||||||
&mut self.focus_cursor
|
|
||||||
}
|
|
||||||
fn layout (&self) -> &[&[SequencerFocus]] { &[
|
|
||||||
&[SequencerFocus::Transport],
|
|
||||||
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
|
|
||||||
] }
|
|
||||||
fn focus_enter (&mut self) {
|
|
||||||
self.entered = true
|
|
||||||
}
|
|
||||||
fn focus_exit (&mut self) {
|
|
||||||
self.entered = false
|
|
||||||
}
|
|
||||||
fn entered (&self) -> Option<Self::Item> {
|
|
||||||
if self.entered { Some(self.focused()) } else { None }
|
|
||||||
}
|
|
||||||
fn update_focus (&mut self) {
|
|
||||||
let focused = self.focused();
|
|
||||||
if let Some(transport) = self.transport.as_ref() {
|
|
||||||
transport.write().unwrap().focused = focused == SequencerFocus::Transport
|
|
||||||
}
|
|
||||||
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
|
|
||||||
self.editor.focused = focused == SequencerFocus::PhraseEditor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> PhrasePool<E> {
|
|
||||||
pub fn new () -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
scroll: 0,
|
|
||||||
phrase: 0,
|
|
||||||
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
|
|
||||||
mode: None,
|
|
||||||
focused: false,
|
|
||||||
entered: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn len (&self) -> usize { self.phrases.len() }
|
|
||||||
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
|
|
||||||
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
|
|
||||||
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
|
|
||||||
pub fn index_before (&self, index: usize) -> usize {
|
|
||||||
index.overflowing_sub(1).0.min(self.len() - 1)
|
|
||||||
}
|
|
||||||
pub fn index_after (&self, index: usize) -> usize {
|
|
||||||
(index + 1) % self.len()
|
|
||||||
}
|
|
||||||
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
|
|
||||||
for i in 0..self.phrases.len() {
|
|
||||||
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
fn new_phrase (name: Option<&str>, color: Option<ItemColorTriplet>) -> Arc<RwLock<Phrase>> {
|
|
||||||
Arc::new(RwLock::new(Phrase::new(
|
|
||||||
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
pub fn delete_selected (&mut self) {
|
|
||||||
if self.phrase > 0 {
|
|
||||||
self.phrases.remove(self.phrase);
|
|
||||||
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn append_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
|
|
||||||
self.phrases.push(Self::new_phrase(name, color));
|
|
||||||
self.phrase = self.phrases.len() - 1;
|
|
||||||
}
|
|
||||||
pub fn insert_new (&mut self, name: Option<&str>, color: Option<ItemColorTriplet>) {
|
|
||||||
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
|
|
||||||
self.phrase += 1;
|
|
||||||
}
|
|
||||||
pub fn insert_dup (&mut self) {
|
|
||||||
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
|
|
||||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
|
||||||
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
|
|
||||||
self.phrase += 1;
|
|
||||||
}
|
|
||||||
pub fn randomize_color (&mut self) {
|
|
||||||
let mut phrase = self.phrases[self.phrase].write().unwrap();
|
|
||||||
phrase.color = ItemColorTriplet::random();
|
|
||||||
}
|
|
||||||
pub fn begin_rename (&mut self) {
|
|
||||||
self.mode = Some(PhrasePoolMode::Rename(
|
|
||||||
self.phrase,
|
|
||||||
self.phrases[self.phrase].read().unwrap().name.clone()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
pub fn begin_length (&mut self) {
|
|
||||||
self.mode = Some(PhrasePoolMode::Length(
|
|
||||||
self.phrase,
|
|
||||||
self.phrases[self.phrase].read().unwrap().length,
|
|
||||||
PhraseLengthFocus::Bar
|
|
||||||
));
|
|
||||||
}
|
|
||||||
pub fn move_up (&mut self) {
|
|
||||||
if self.phrase > 1 {
|
|
||||||
self.phrases.swap(self.phrase - 1, self.phrase);
|
|
||||||
self.phrase -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn move_down (&mut self) {
|
|
||||||
if self.phrase < self.phrases.len().saturating_sub(1) {
|
|
||||||
self.phrases.swap(self.phrase + 1, self.phrase);
|
|
||||||
self.phrase += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> PhraseEditor<E> {
|
|
||||||
pub fn new () -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
phrase: None,
|
|
||||||
note_len: 24,
|
|
||||||
notes_in: Arc::new(RwLock::new([false;128])),
|
|
||||||
notes_out: Arc::new(RwLock::new([false;128])),
|
|
||||||
keys: keys_vert(),
|
|
||||||
buffer: Default::default(),
|
|
||||||
focused: false,
|
|
||||||
entered: false,
|
|
||||||
mode: false,
|
|
||||||
now: Arc::new(0.into()),
|
|
||||||
width: 0.into(),
|
|
||||||
height: 0.into(),
|
|
||||||
note_axis: RwLock::new(FixedAxis {
|
|
||||||
start: 12,
|
|
||||||
point: Some(36),
|
|
||||||
clamp: Some(127)
|
|
||||||
}),
|
|
||||||
time_axis: RwLock::new(ScaledAxis {
|
|
||||||
start: 00,
|
|
||||||
point: Some(00),
|
|
||||||
clamp: Some(000),
|
|
||||||
scale: 24
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn note_cursor_inc (&self) {
|
|
||||||
let mut axis = self.note_axis.write().unwrap();
|
|
||||||
axis.point_dec(1);
|
|
||||||
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
|
|
||||||
}
|
|
||||||
pub fn note_cursor_dec (&self) {
|
|
||||||
let mut axis = self.note_axis.write().unwrap();
|
|
||||||
axis.point_inc(1);
|
|
||||||
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
|
|
||||||
}
|
|
||||||
pub fn note_page_up (&self) {
|
|
||||||
let mut axis = self.note_axis.write().unwrap();
|
|
||||||
axis.start_dec(3);
|
|
||||||
axis.point_dec(3);
|
|
||||||
}
|
|
||||||
pub fn note_page_down (&self) {
|
|
||||||
let mut axis = self.note_axis.write().unwrap();
|
|
||||||
axis.start_inc(3);
|
|
||||||
axis.point_inc(3);
|
|
||||||
}
|
|
||||||
pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); }
|
|
||||||
pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); }
|
|
||||||
pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) }
|
|
||||||
pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) }
|
|
||||||
pub fn time_cursor_advance (&self) {
|
|
||||||
let point = self.time_axis.read().unwrap().point;
|
|
||||||
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
|
||||||
let forward = |time|(time + self.note_len) % length;
|
|
||||||
self.time_axis.write().unwrap().point = point.map(forward);
|
|
||||||
}
|
|
||||||
pub fn time_cursor_inc (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().point_inc(scale);
|
|
||||||
}
|
|
||||||
pub fn time_cursor_dec (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().point_dec(scale);
|
|
||||||
}
|
|
||||||
pub fn time_scroll_inc (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().start_inc(scale);
|
|
||||||
}
|
|
||||||
pub fn time_scroll_dec (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().start_dec(scale);
|
|
||||||
}
|
|
||||||
pub fn time_zoom_in (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().scale = prev_note_length(scale)
|
|
||||||
}
|
|
||||||
pub fn time_zoom_out (&self) {
|
|
||||||
let scale = self.time_axis.read().unwrap().scale;
|
|
||||||
self.time_axis.write().unwrap().scale = next_note_length(scale)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PhrasePlayer {
|
|
||||||
pub fn new (
|
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
|
||||||
clock: &Arc<TransportTime>,
|
|
||||||
name: &str
|
|
||||||
) -> Usually<Self> {
|
|
||||||
let jack = jack.read().unwrap();
|
|
||||||
Ok(Self {
|
|
||||||
clock: clock.clone(),
|
|
||||||
phrase: None,
|
|
||||||
next_phrase: None,
|
|
||||||
notes_in: Arc::new(RwLock::new([false;128])),
|
|
||||||
notes_out: Arc::new(RwLock::new([false;128])),
|
|
||||||
monitoring: false,
|
|
||||||
recording: false,
|
|
||||||
overdub: true,
|
|
||||||
reset: true,
|
|
||||||
midi_note: Vec::with_capacity(8),
|
|
||||||
midi_chunk: vec![Vec::with_capacity(16);16384],
|
|
||||||
midi_outputs: vec![
|
|
||||||
jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
|
||||||
],
|
|
||||||
midi_inputs: vec![
|
|
||||||
jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; }
|
|
||||||
pub fn toggle_record (&mut self) { self.recording = !self.recording; }
|
|
||||||
pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; }
|
|
||||||
pub fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
|
||||||
let start = self.clock.next_launch_pulse();
|
|
||||||
self.next_phrase = Some((
|
|
||||||
Instant::from_pulse(&self.clock.timebase(), start as f64),
|
|
||||||
phrase.map(|p|p.clone())
|
|
||||||
));
|
|
||||||
self.reset = true;
|
|
||||||
}
|
|
||||||
pub fn pulses_since_start (&self) -> Option<f64> {
|
|
||||||
if let Some((started, Some(_))) = self.phrase.as_ref() {
|
|
||||||
Some(self.clock.current.pulse.get() - started.pulse.get())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> PhraseLength<E> {
|
|
||||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
|
||||||
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
|
||||||
}
|
|
||||||
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
|
|
||||||
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
|
|
||||||
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
|
|
||||||
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
|
|
||||||
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
|
|
||||||
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
|
|
||||||
}
|
|
||||||
impl PhraseLengthFocus {
|
|
||||||
pub fn next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Bar => Self::Beat,
|
|
||||||
Self::Beat => Self::Tick,
|
|
||||||
Self::Tick => Self::Bar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Bar => Self::Tick,
|
|
||||||
Self::Beat => Self::Bar,
|
|
||||||
Self::Tick => Self::Beat,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Content for Sequencer<Tui> {
|
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
Stack::down(move|add|{
|
Stack::down(move|add|{
|
||||||
|
|
@ -423,452 +32,14 @@ impl Content for Sequencer<Tui> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Handle<Tui> for Sequencer<Tui> {
|
|
||||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
/// JACK process callback for sequencer app
|
||||||
if let Some(entered) = self.entered() {
|
impl Audio for SequencerApp {
|
||||||
use SequencerFocus::*;
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
if let Some(true) = match entered {
|
if let Some(ref transport) = self.transport {
|
||||||
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
|
transport.write().unwrap().process(client, scope);
|
||||||
PhrasePool => self.phrases.write().unwrap().handle(i)?,
|
|
||||||
PhraseEditor => self.editor.handle(i)?,
|
|
||||||
} {
|
|
||||||
return Ok(Some(true))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(command) = SequencerCommand::input_to_command(self, i) {
|
self.player.process(client, scope);
|
||||||
let _undo = command.execute(self)?;
|
Control::Continue
|
||||||
return Ok(Some(true))
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl InputToCommand<Tui, Sequencer<Tui>> for SequencerCommand {
|
|
||||||
fn input_to_command (state: &Sequencer<Tui>, input: &TuiInput) -> Option<Self> {
|
|
||||||
use SequencerCommand::*;
|
|
||||||
use FocusCommand::*;
|
|
||||||
match input.event() {
|
|
||||||
key!(KeyCode::Tab) => Some(Focus(Next)),
|
|
||||||
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
|
|
||||||
key!(KeyCode::BackTab) => Some(Focus(Prev)),
|
|
||||||
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
|
|
||||||
key!(KeyCode::Up) => Some(Focus(Up)),
|
|
||||||
key!(KeyCode::Down) => Some(Focus(Down)),
|
|
||||||
key!(KeyCode::Left) => Some(Focus(Left)),
|
|
||||||
key!(KeyCode::Right) => Some(Focus(Right)),
|
|
||||||
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
|
|
||||||
_ => match state.focused() {
|
|
||||||
SequencerFocus::Transport => if let Some(t) = state.transport.as_ref() {
|
|
||||||
TransportCommand::input_to_command(&*t.read().unwrap(), input).map(Transport)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
SequencerFocus::PhrasePool =>
|
|
||||||
PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input)
|
|
||||||
.map(Phrases),
|
|
||||||
SequencerFocus::PhraseEditor =>
|
|
||||||
PhraseEditorCommand::input_to_command(&state.editor, input)
|
|
||||||
.map(Editor),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: Display phrases always in order of appearance
|
|
||||||
impl Content for PhrasePool<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let Self { focused, phrases, mode, .. } = self;
|
|
||||||
let content = col!(
|
|
||||||
(i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{
|
|
||||||
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
|
|
||||||
let mut length = PhraseLength::new(length, None);
|
|
||||||
if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode {
|
|
||||||
if *focused && i == *phrase {
|
|
||||||
length.pulses = *new_length;
|
|
||||||
length.focus = Some(*focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let length = length.align_e().fill_x();
|
|
||||||
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
|
|
||||||
let mut row2 = format!(" {name}");
|
|
||||||
if let Some(PhrasePoolMode::Rename(phrase, _)) = mode {
|
|
||||||
if *focused && i == *phrase { row2 = format!("{row2}▄"); }
|
|
||||||
};
|
|
||||||
let row2 = TuiStyle::bold(row2, true);
|
|
||||||
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
|
|
||||||
Ok(if *focused && i == self.phrase { add(&CORNERS)?; })
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
|
|
||||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
|
||||||
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
|
|
||||||
let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
|
|
||||||
let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "});
|
|
||||||
let upper_right = format!("({})", phrases.len());
|
|
||||||
lay!(
|
|
||||||
content,
|
|
||||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
|
||||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Handle<Tui> for PhrasePool<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
if let Some(command) = PhrasePoolCommand::input_to_command(self, from) {
|
|
||||||
let _undo = command.execute(self)?;
|
|
||||||
return Ok(Some(true))
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhrasePoolCommand {
|
|
||||||
fn input_to_command (state: &PhrasePool<Tui>, input: &TuiInput) -> Option<Self> {
|
|
||||||
match input.event() {
|
|
||||||
key!(KeyCode::Up) => Some(Self::Prev),
|
|
||||||
key!(KeyCode::Down) => Some(Self::Next),
|
|
||||||
key!(KeyCode::Char(',')) => Some(Self::MoveUp),
|
|
||||||
key!(KeyCode::Char('.')) => Some(Self::MoveDown),
|
|
||||||
key!(KeyCode::Delete) => Some(Self::Delete),
|
|
||||||
key!(KeyCode::Char('a')) => Some(Self::Append),
|
|
||||||
key!(KeyCode::Char('i')) => Some(Self::Insert),
|
|
||||||
key!(KeyCode::Char('d')) => Some(Self::Duplicate),
|
|
||||||
key!(KeyCode::Char('c')) => Some(Self::RandomColor),
|
|
||||||
key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)),
|
|
||||||
key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)),
|
|
||||||
_ => match state.mode {
|
|
||||||
Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input)
|
|
||||||
.map(Self::Rename),
|
|
||||||
Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input)
|
|
||||||
.map(Self::Length),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseRenameCommand {
|
|
||||||
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Backspace) => Some(Self::Backspace),
|
|
||||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
|
||||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
|
||||||
key!(KeyCode::Char(c)) => Some(Self::Append(*c)),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl InputToCommand<Tui, PhrasePool<Tui>> for PhraseLengthCommand {
|
|
||||||
fn input_to_command (_: &PhrasePool<Tui>, from: &TuiInput) -> Option<Self> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Up) => Some(Self::Inc),
|
|
||||||
key!(KeyCode::Down) => Some(Self::Dec),
|
|
||||||
key!(KeyCode::Right) => Some(Self::Next),
|
|
||||||
key!(KeyCode::Left) => Some(Self::Prev),
|
|
||||||
key!(KeyCode::Enter) => Some(Self::Confirm),
|
|
||||||
key!(KeyCode::Esc) => Some(Self::Cancel),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Content for PhraseLength<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Layers::new(move|add|{
|
|
||||||
match self.focus {
|
|
||||||
None => add(&row!(
|
|
||||||
" ", self.bars_string(),
|
|
||||||
".", self.beats_string(),
|
|
||||||
".", self.ticks_string(),
|
|
||||||
" "
|
|
||||||
)),
|
|
||||||
Some(PhraseLengthFocus::Bar) => add(&row!(
|
|
||||||
"[", self.bars_string(),
|
|
||||||
"]", self.beats_string(),
|
|
||||||
".", self.ticks_string(),
|
|
||||||
" "
|
|
||||||
)),
|
|
||||||
Some(PhraseLengthFocus::Beat) => add(&row!(
|
|
||||||
" ", self.bars_string(),
|
|
||||||
"[", self.beats_string(),
|
|
||||||
"]", self.ticks_string(),
|
|
||||||
" "
|
|
||||||
)),
|
|
||||||
Some(PhraseLengthFocus::Tick) => add(&row!(
|
|
||||||
" ", self.bars_string(),
|
|
||||||
".", self.beats_string(),
|
|
||||||
"[", self.ticks_string(),
|
|
||||||
"]"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Content for PhraseEditor<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self;
|
|
||||||
let FixedAxis {
|
|
||||||
start: note_start, point: note_point, clamp: note_clamp
|
|
||||||
} = *self.note_axis.read().unwrap();
|
|
||||||
let ScaledAxis {
|
|
||||||
start: time_start, point: time_point, clamp: time_clamp, scale: time_scale
|
|
||||||
} = *self.time_axis.read().unwrap();
|
|
||||||
//let color = Color::Rgb(0,255,0);
|
|
||||||
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
|
|
||||||
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
|
|
||||||
Ok(if to.area().h() >= 2 {
|
|
||||||
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
|
|
||||||
let y = y + (note_start / 2) as u16;
|
|
||||||
if x < keys.area.width && y < keys.area.height {
|
|
||||||
*cell = keys.get(x, y).clone()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}).fill_y();
|
|
||||||
let notes_bg_null = Color::Rgb(28, 35, 25);
|
|
||||||
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
|
||||||
let area = to.area();
|
|
||||||
let h = area.h() as usize;
|
|
||||||
self.height.store(h, Ordering::Relaxed);
|
|
||||||
self.width.store(area.w() as usize, Ordering::Relaxed);
|
|
||||||
let mut axis = self.note_axis.write().unwrap();
|
|
||||||
if let Some(point) = axis.point {
|
|
||||||
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
|
|
||||||
axis.start += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(if to.area().h() >= 2 {
|
|
||||||
let area = to.area();
|
|
||||||
to.buffer_update(area, &move |cell, x, y|{
|
|
||||||
cell.set_bg(notes_bg_null);
|
|
||||||
let src_x = (x as usize + time_start) * time_scale;
|
|
||||||
let src_y = y as usize + note_start / 2;
|
|
||||||
if src_x < buffer.width && src_y < buffer.height - 1 {
|
|
||||||
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
|
|
||||||
cell.set_symbol(src.symbol());
|
|
||||||
cell.set_fg(src.fg);
|
|
||||||
cell.set_bg(src.bg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}).fill_x();
|
|
||||||
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
|
|
||||||
Ok(if *focused && *entered {
|
|
||||||
let area = to.area();
|
|
||||||
if let (Some(time), Some(note)) = (time_point, note_point) {
|
|
||||||
let x1 = area.x() + (time / time_scale) as u16;
|
|
||||||
let x2 = x1 + (self.note_len / time_scale) as u16;
|
|
||||||
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
|
|
||||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
|
||||||
for x in x1..x2 {
|
|
||||||
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
|
|
||||||
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
|
|
||||||
let playhead = CustomWidget::new(
|
|
||||||
|to:[u16;2]|Ok(Some(to.clip_h(1))),
|
|
||||||
move|to: &mut TuiOutput|{
|
|
||||||
if let Some(_) = phrase {
|
|
||||||
let now = self.now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
|
|
||||||
let time_clamp = time_clamp
|
|
||||||
.expect("time_axis of sequencer expected to be clamped");
|
|
||||||
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
|
|
||||||
let this_step = time_start + (x + 0) * time_scale;
|
|
||||||
let next_step = time_start + (x + 1) * time_scale;
|
|
||||||
let x = to.area().x() + x as u16;
|
|
||||||
let active = this_step <= now && now < next_step;
|
|
||||||
let character = if active { "|" } else { "·" };
|
|
||||||
let style = if active { playhead_active } else { playhead_inactive };
|
|
||||||
to.blit(&character, x, to.area.y(), Some(style));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
).push_x(6).align_sw();
|
|
||||||
let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
|
|
||||||
let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
|
|
||||||
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
|
|
||||||
let note_area = lay!(notes, cursor).fill_x();
|
|
||||||
let piano_roll = row!(keys, note_area).fill_x();
|
|
||||||
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
|
|
||||||
let content = lay!(content, playhead);
|
|
||||||
let mut upper_left = format!("[{}] Sequencer", if *entered {"■"} else {" "});
|
|
||||||
if let Some(phrase) = phrase {
|
|
||||||
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
|
|
||||||
}
|
|
||||||
let mut lower_right = format!(
|
|
||||||
"┤{}x{}├",
|
|
||||||
self.width.load(Ordering::Relaxed),
|
|
||||||
self.height.load(Ordering::Relaxed),
|
|
||||||
);
|
|
||||||
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
|
|
||||||
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
|
|
||||||
//pulses_to_name(time_scale),
|
|
||||||
//time_start, time_point.unwrap_or(0),
|
|
||||||
//time_scale, time_clamp.unwrap_or(0),
|
|
||||||
//);
|
|
||||||
if *focused && *entered {
|
|
||||||
lower_right = format!("┤Note: {} {}├─{lower_right}",
|
|
||||||
self.note_axis.read().unwrap().point.unwrap(),
|
|
||||||
pulses_to_name(*note_len));
|
|
||||||
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
|
|
||||||
//pulses_to_name(*note_len),
|
|
||||||
//note_start,
|
|
||||||
//note_point.unwrap_or(0),
|
|
||||||
//note_clamp.unwrap_or(0),
|
|
||||||
//);
|
|
||||||
}
|
|
||||||
let upper_right = if let Some(phrase) = phrase {
|
|
||||||
format!("┤Length: {}├", phrase.read().unwrap().length)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
lay!(
|
|
||||||
content,
|
|
||||||
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
|
|
||||||
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
|
|
||||||
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> PhraseEditor<E> {
|
|
||||||
pub fn put (&mut self) {
|
|
||||||
if let (Some(phrase), Some(time), Some(note)) = (
|
|
||||||
&self.phrase,
|
|
||||||
self.time_axis.read().unwrap().point,
|
|
||||||
self.note_axis.read().unwrap().point,
|
|
||||||
) {
|
|
||||||
let mut phrase = phrase.write().unwrap();
|
|
||||||
let key: u7 = u7::from((127 - note) as u8);
|
|
||||||
let vel: u7 = 100.into();
|
|
||||||
let start = time;
|
|
||||||
let end = (start + self.note_len) % phrase.length;
|
|
||||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
|
|
||||||
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
|
|
||||||
self.buffer = Self::redraw(&phrase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
|
||||||
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
|
||||||
if let Some(phrase) = phrase {
|
|
||||||
self.phrase = Some(phrase.clone());
|
|
||||||
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
|
|
||||||
self.buffer = Self::redraw(&*phrase.read().unwrap());
|
|
||||||
} else {
|
|
||||||
self.phrase = None;
|
|
||||||
self.time_axis.write().unwrap().clamp = Some(0);
|
|
||||||
self.buffer = Default::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn redraw (phrase: &Phrase) -> BigBuffer {
|
|
||||||
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
|
|
||||||
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
|
|
||||||
Self::fill_seq_fg(&mut buf, &phrase);
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
|
|
||||||
for x in 0..buf.width {
|
|
||||||
// Only fill as far as phrase length
|
|
||||||
if x as usize >= length { break }
|
|
||||||
// Fill each row with background characters
|
|
||||||
for y in 0 .. buf.height {
|
|
||||||
buf.get_mut(x, y).map(|cell|{
|
|
||||||
cell.set_char(if ppq == 0 {
|
|
||||||
'·'
|
|
||||||
} else if x % (4 * ppq) == 0 {
|
|
||||||
'│'
|
|
||||||
} else if x % ppq == 0 {
|
|
||||||
'╎'
|
|
||||||
} else {
|
|
||||||
'·'
|
|
||||||
});
|
|
||||||
cell.set_fg(Color::Rgb(48, 64, 56));
|
|
||||||
cell.modifier = Modifier::DIM;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
|
|
||||||
let mut notes_on = [false;128];
|
|
||||||
for x in 0..buf.width {
|
|
||||||
if x as usize >= phrase.length {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if let Some(notes) = phrase.notes.get(x as usize) {
|
|
||||||
if phrase.percussive {
|
|
||||||
for note in notes {
|
|
||||||
match note {
|
|
||||||
MidiMessage::NoteOn { key, .. } =>
|
|
||||||
notes_on[key.as_int() as usize] = true,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for note in notes {
|
|
||||||
match note {
|
|
||||||
MidiMessage::NoteOn { key, .. } =>
|
|
||||||
notes_on[key.as_int() as usize] = true,
|
|
||||||
MidiMessage::NoteOff { key, .. } =>
|
|
||||||
notes_on[key.as_int() as usize] = false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for y in 0..buf.height {
|
|
||||||
if y >= 64 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if let Some(block) = half_block(
|
|
||||||
notes_on[y as usize * 2],
|
|
||||||
notes_on[y as usize * 2 + 1],
|
|
||||||
) {
|
|
||||||
buf.get_mut(x, y).map(|cell|{
|
|
||||||
cell.set_char(block);
|
|
||||||
cell.set_fg(Color::White);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if phrase.percussive {
|
|
||||||
notes_on.fill(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Colors of piano keys
|
|
||||||
const KEY_COLORS: [(Color, Color);6] = [
|
|
||||||
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
|
||||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
||||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
||||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
|
||||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
||||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
|
||||||
];
|
|
||||||
pub(crate) fn keys_vert () -> Buffer {
|
|
||||||
let area = [0, 0, 5, 64];
|
|
||||||
let mut buffer = Buffer::empty(Rect {
|
|
||||||
x: area.x(), y: area.y(), width: area.w(), height: area.h()
|
|
||||||
});
|
|
||||||
buffer_update(&mut buffer, area, &|cell, x, y| {
|
|
||||||
let y = 63 - y;
|
|
||||||
match x {
|
|
||||||
0 => {
|
|
||||||
cell.set_char('▀');
|
|
||||||
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
|
|
||||||
cell.set_fg(fg);
|
|
||||||
cell.set_bg(bg);
|
|
||||||
},
|
|
||||||
1 => {
|
|
||||||
cell.set_char('▀');
|
|
||||||
cell.set_fg(Color::White);
|
|
||||||
cell.set_bg(Color::White);
|
|
||||||
},
|
|
||||||
2 => if y % 6 == 0 { cell.set_char('C'); },
|
|
||||||
3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); },
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
|
||||||
8
crates/tek_tui/src/tui_sequencer_bar.rs
Normal file
8
crates/tek_tui/src/tui_sequencer_bar.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Status bar for sequencer app
|
||||||
|
pub enum SequencerStatusBar {
|
||||||
|
Transport,
|
||||||
|
PhrasePool,
|
||||||
|
PhraseEditor,
|
||||||
|
}
|
||||||
|
|
@ -1,78 +1,35 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum SequencerCommand {
|
pub enum SequencerAppCommand {
|
||||||
Focus(FocusCommand),
|
Focus(FocusCommand),
|
||||||
Transport(TransportCommand),
|
Transport(TransportCommand),
|
||||||
Phrases(PhrasePoolCommand),
|
Phrases(PhrasePoolCommand),
|
||||||
Editor(PhraseEditorCommand),
|
Editor(PhraseEditorCommand),
|
||||||
}
|
}
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum PhrasePoolCommand {
|
impl Handle<Tui> for SequencerApp<Tui> {
|
||||||
Prev,
|
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||||
Next,
|
if let Some(entered) = self.entered() {
|
||||||
MoveUp,
|
use SequencerFocus::*;
|
||||||
MoveDown,
|
if let Some(true) = match entered {
|
||||||
Delete,
|
Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(),
|
||||||
Append,
|
PhrasePool => self.phrases.write().unwrap().handle(i)?,
|
||||||
Insert,
|
PhraseEditor => self.editor.handle(i)?,
|
||||||
Duplicate,
|
} {
|
||||||
RandomColor,
|
return Ok(Some(true))
|
||||||
Edit,
|
}
|
||||||
Import,
|
}
|
||||||
Export,
|
if let Some(command) = SequencerCommand::input_to_command(self, i) {
|
||||||
Rename(PhraseRenameCommand),
|
let _undo = command.execute(self)?;
|
||||||
Length(PhraseLengthCommand),
|
return Ok(Some(true))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum PhraseRenameCommand {
|
impl<E: Engine> Command<SequencerApp<E>> for SequencerAppCommand {
|
||||||
Begin,
|
fn execute (self, state: &mut SequencerApp<E>) -> Perhaps<Self> {
|
||||||
Backspace,
|
|
||||||
Append(char),
|
|
||||||
Set(String),
|
|
||||||
Confirm,
|
|
||||||
Cancel,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum PhraseLengthCommand {
|
|
||||||
Begin,
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Inc,
|
|
||||||
Dec,
|
|
||||||
Set(usize),
|
|
||||||
Confirm,
|
|
||||||
Cancel,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub enum PhraseEditorCommand {
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
|
||||||
ToggleDirection,
|
|
||||||
EnterEditMode,
|
|
||||||
ExitEditMode,
|
|
||||||
NoteAppend,
|
|
||||||
NoteCursorDec,
|
|
||||||
NoteCursorInc,
|
|
||||||
NoteLengthDec,
|
|
||||||
NoteLengthInc,
|
|
||||||
NotePageDown,
|
|
||||||
NotePageUp,
|
|
||||||
NoteScrollDec,
|
|
||||||
NoteScrollInc,
|
|
||||||
NoteSet,
|
|
||||||
TimeCursorDec,
|
|
||||||
TimeCursorInc,
|
|
||||||
TimeScrollDec,
|
|
||||||
TimeScrollInc,
|
|
||||||
TimeZoomIn,
|
|
||||||
TimeZoomOut,
|
|
||||||
GoUp,
|
|
||||||
GoDown,
|
|
||||||
GoLeft,
|
|
||||||
GoRight,
|
|
||||||
}
|
|
||||||
impl<E: Engine> Command<Sequencer<E>> for SequencerCommand {
|
|
||||||
fn execute (self, state: &mut Sequencer<E>) -> Perhaps<Self> {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Focus(cmd) => {
|
Self::Focus(cmd) => {
|
||||||
return delegate(cmd, Self::Focus, state)
|
return delegate(cmd, Self::Focus, state)
|
||||||
|
|
@ -90,197 +47,34 @@ impl<E: Engine> Command<Sequencer<E>> for SequencerCommand {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<E: Engine> Command<PhrasePool<E>> for PhrasePoolCommand {
|
|
||||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
impl InputToCommand<Tui, SequencerApp<Tui>> for SequencerCommand {
|
||||||
use PhrasePoolCommand::*;
|
fn input_to_command (state: &SequencerApp<Tui>, input: &TuiInput) -> Option<Self> {
|
||||||
use PhraseRenameCommand as Rename;
|
use SequencerCommand::*;
|
||||||
use PhraseLengthCommand as Length;
|
use FocusCommand::*;
|
||||||
match self {
|
match input.event() {
|
||||||
Rename(Rename::Begin) => { state.begin_rename() },
|
key!(KeyCode::Tab) => Some(Focus(Next)),
|
||||||
Length(Length::Begin) => { state.begin_length() },
|
key!(Shift-KeyCode::Tab) => Some(Focus(Prev)),
|
||||||
Prev => { state.select_prev() },
|
key!(KeyCode::BackTab) => Some(Focus(Prev)),
|
||||||
Next => { state.select_next() },
|
key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)),
|
||||||
Delete => { state.delete_selected() },
|
key!(KeyCode::Up) => Some(Focus(Up)),
|
||||||
Append => { state.append_new(None, None) },
|
key!(KeyCode::Down) => Some(Focus(Down)),
|
||||||
Insert => { state.insert_new(None, None) },
|
key!(KeyCode::Left) => Some(Focus(Left)),
|
||||||
Duplicate => { state.insert_dup() },
|
key!(KeyCode::Right) => Some(Focus(Right)),
|
||||||
RandomColor => { state.randomize_color() },
|
key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)),
|
||||||
MoveUp => { state.move_up() },
|
_ => match state.focused() {
|
||||||
MoveDown => { state.move_down() },
|
SequencerFocus::Transport => if let Some(t) = state.transport.as_ref() {
|
||||||
_ => unreachable!(),
|
TransportCommand::input_to_command(&*t.read().unwrap(), input).map(Transport)
|
||||||
}
|
} else {
|
||||||
Ok(None)
|
None
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> Command<PhrasePool<E>> for PhraseRenameCommand {
|
|
||||||
fn translate (self, state: &PhrasePool<E>) -> Self {
|
|
||||||
use PhraseRenameCommand::*;
|
|
||||||
if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode {
|
|
||||||
match self {
|
|
||||||
Backspace => {
|
|
||||||
let mut new_name = old_name.clone();
|
|
||||||
new_name.pop();
|
|
||||||
return Self::Set(new_name)
|
|
||||||
},
|
},
|
||||||
Append(c) => {
|
SequencerFocus::PhrasePool =>
|
||||||
let mut new_name = old_name.clone();
|
PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input)
|
||||||
new_name.push(c);
|
.map(Phrases),
|
||||||
return Self::Set(new_name)
|
SequencerFocus::PhraseEditor =>
|
||||||
},
|
PhraseEditorCommand::input_to_command(&state.editor, input)
|
||||||
_ => {}
|
.map(Editor),
|
||||||
}
|
}
|
||||||
} else if self != Begin {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
|
||||||
use PhraseRenameCommand::*;
|
|
||||||
if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode {
|
|
||||||
match self {
|
|
||||||
Set(s) => {
|
|
||||||
state.phrases[phrase].write().unwrap().name = s.into();
|
|
||||||
return Ok(Some(Self::Set(old_name.clone())))
|
|
||||||
},
|
|
||||||
Confirm => {
|
|
||||||
let old_name = old_name.clone();
|
|
||||||
state.mode = None;
|
|
||||||
return Ok(Some(Self::Set(old_name)))
|
|
||||||
},
|
|
||||||
Cancel => {
|
|
||||||
let mut phrase = state.phrases[phrase].write().unwrap();
|
|
||||||
phrase.name = old_name.clone();
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
};
|
|
||||||
Ok(None)
|
|
||||||
} else if self == Begin {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> Command<PhrasePool<E>> for PhraseLengthCommand {
|
|
||||||
fn translate (self, state: &PhrasePool<E>) -> Self {
|
|
||||||
use PhraseLengthCommand::*;
|
|
||||||
if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode {
|
|
||||||
match self {
|
|
||||||
Confirm => { return Self::Set(length) },
|
|
||||||
_ => self
|
|
||||||
}
|
|
||||||
} else if self == Begin {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn execute (self, state: &mut PhrasePool<E>) -> Perhaps<Self> {
|
|
||||||
use PhraseLengthFocus::*;
|
|
||||||
use PhraseLengthCommand::*;
|
|
||||||
if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode {
|
|
||||||
match self {
|
|
||||||
Cancel => { state.mode = None; },
|
|
||||||
Prev => { focus.prev() },
|
|
||||||
Next => { focus.next() },
|
|
||||||
Inc => match focus {
|
|
||||||
Bar => { *length += 4 * PPQ },
|
|
||||||
Beat => { *length += PPQ },
|
|
||||||
Tick => { *length += 1 },
|
|
||||||
},
|
|
||||||
Dec => match focus {
|
|
||||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
|
||||||
Beat => { *length = length.saturating_sub(PPQ) },
|
|
||||||
Tick => { *length = length.saturating_sub(1) },
|
|
||||||
},
|
|
||||||
Set(length) => {
|
|
||||||
let mut phrase = state.phrases[phrase].write().unwrap();
|
|
||||||
let old_length = phrase.length;
|
|
||||||
phrase.length = length;
|
|
||||||
state.mode = None;
|
|
||||||
return Ok(Some(Self::Set(old_length)))
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
} else if self == Begin {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> Command<PhraseEditor<E>> for PhraseEditorCommand {
|
|
||||||
fn translate (self, state: &PhraseEditor<E>) -> Self {
|
|
||||||
use PhraseEditorCommand::*;
|
|
||||||
match self {
|
|
||||||
GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, },
|
|
||||||
GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, },
|
|
||||||
GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, },
|
|
||||||
GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, },
|
|
||||||
_ => self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn execute (self, state: &mut PhraseEditor<E>) -> Perhaps<Self> {
|
|
||||||
use PhraseEditorCommand::*;
|
|
||||||
match self.translate(state) {
|
|
||||||
ToggleDirection => { state.mode = !state.mode; },
|
|
||||||
EnterEditMode => { state.entered = true; },
|
|
||||||
ExitEditMode => { state.entered = false; },
|
|
||||||
TimeZoomOut => { state.time_zoom_out() },
|
|
||||||
TimeZoomIn => { state.time_zoom_in() },
|
|
||||||
TimeCursorDec => { state.time_cursor_dec() },
|
|
||||||
TimeCursorInc => { state.time_cursor_inc() },
|
|
||||||
TimeScrollDec => { state.time_scroll_dec() },
|
|
||||||
TimeScrollInc => { state.time_scroll_inc() },
|
|
||||||
NoteCursorDec => { state.note_cursor_dec() },
|
|
||||||
NoteCursorInc => { state.note_cursor_inc() },
|
|
||||||
NoteScrollDec => { state.note_scroll_dec() },
|
|
||||||
NoteScrollInc => { state.note_scroll_inc() },
|
|
||||||
NoteLengthDec => { state.note_length_dec() },
|
|
||||||
NoteLengthInc => { state.note_length_inc() },
|
|
||||||
NotePageUp => { state.note_page_up() },
|
|
||||||
NotePageDown => { state.note_page_down() },
|
|
||||||
NoteAppend => {
|
|
||||||
if state.entered {
|
|
||||||
state.put();
|
|
||||||
state.time_cursor_advance();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
NoteSet => {
|
|
||||||
if state.entered { state.put(); }
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Handle<Tui> for PhraseEditor<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
PhraseEditorCommand::execute_with_state(self, from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl InputToCommand<Tui, PhraseEditor<Tui>> for PhraseEditorCommand {
|
|
||||||
fn input_to_command (_: &PhraseEditor<Tui>, from: &TuiInput) -> Option<Self> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Char('`')) => Some(Self::ToggleDirection),
|
|
||||||
key!(KeyCode::Enter) => Some(Self::EnterEditMode),
|
|
||||||
key!(KeyCode::Esc) => Some(Self::ExitEditMode),
|
|
||||||
key!(KeyCode::Char('[')) => Some(Self::NoteLengthDec),
|
|
||||||
key!(KeyCode::Char(']')) => Some(Self::NoteLengthInc),
|
|
||||||
key!(KeyCode::Char('a')) => Some(Self::NoteAppend),
|
|
||||||
key!(KeyCode::Char('s')) => Some(Self::NoteSet),
|
|
||||||
key!(KeyCode::Char('-')) => Some(Self::TimeZoomOut),
|
|
||||||
key!(KeyCode::Char('_')) => Some(Self::TimeZoomOut),
|
|
||||||
key!(KeyCode::Char('=')) => Some(Self::TimeZoomIn),
|
|
||||||
key!(KeyCode::Char('+')) => Some(Self::TimeZoomIn),
|
|
||||||
key!(KeyCode::PageUp) => Some(Self::NotePageUp),
|
|
||||||
key!(KeyCode::PageDown) => Some(Self::NotePageDown),
|
|
||||||
key!(KeyCode::Up) => Some(Self::GoUp),
|
|
||||||
key!(KeyCode::Down) => Some(Self::GoDown),
|
|
||||||
key!(KeyCode::Left) => Some(Self::GoLeft),
|
|
||||||
key!(KeyCode::Right) => Some(Self::GoRight),
|
|
||||||
_ => None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
crates/tek_tui/src/tui_sequencer_foc.rs
Normal file
43
crates/tek_tui/src/tui_sequencer_foc.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Sections in the sequencer app that may be focused
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus {
|
||||||
|
/// The transport (toolbar) is focused
|
||||||
|
Transport,
|
||||||
|
/// The phrase list (pool) is focused
|
||||||
|
PhrasePool,
|
||||||
|
/// The phrase editor (sequencer) is focused
|
||||||
|
PhraseEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus layout of sequencer app
|
||||||
|
impl<E: Engine> FocusGrid for Sequencer<E> {
|
||||||
|
type Item = SequencerFocus;
|
||||||
|
fn cursor (&self) -> (usize, usize) {
|
||||||
|
self.focus_cursor
|
||||||
|
}
|
||||||
|
fn cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||||
|
&mut self.focus_cursor
|
||||||
|
}
|
||||||
|
fn layout (&self) -> &[&[SequencerFocus]] { &[
|
||||||
|
&[SequencerFocus::Transport],
|
||||||
|
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
|
||||||
|
] }
|
||||||
|
fn focus_enter (&mut self) {
|
||||||
|
self.entered = true
|
||||||
|
}
|
||||||
|
fn focus_exit (&mut self) {
|
||||||
|
self.entered = false
|
||||||
|
}
|
||||||
|
fn entered (&self) -> Option<Self::Item> {
|
||||||
|
if self.entered { Some(self.focused()) } else { None }
|
||||||
|
}
|
||||||
|
fn update_focus (&mut self) {
|
||||||
|
let focused = self.focused();
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
transport.write().unwrap().focused = focused == SequencerFocus::Transport
|
||||||
|
}
|
||||||
|
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
|
||||||
|
self.editor.focused = focused == SequencerFocus::PhraseEditor;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue