mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
wip: refactor pt.4, reduce number of files
This commit is contained in:
parent
adf5b3f0f8
commit
8c37c95cc6
60 changed files with 2185 additions and 2187 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -2697,14 +2697,6 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tek_snd"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"tek_api",
|
|
||||||
"tek_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_tui"
|
name = "tek_tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/tek_core",
|
"crates/tek_core",
|
||||||
"crates/tek_snd",
|
|
||||||
"crates/tek_api",
|
"crates/tek_api",
|
||||||
"crates/tek_cli",
|
"crates/tek_cli",
|
||||||
"crates/tek_tui"
|
"crates/tek_tui"
|
||||||
|
|
|
||||||
0
crates/tek_api/src/api.rs
Normal file
0
crates/tek_api/src/api.rs
Normal file
|
|
@ -1,68 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ArrangerCommand {
|
|
||||||
Focus(FocusCommand),
|
|
||||||
Transport(TransportCommand),
|
|
||||||
Phrases(PhrasePoolCommand),
|
|
||||||
Editor(PhraseEditorCommand),
|
|
||||||
Arrangement(ArrangementCommand),
|
|
||||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ArrangementCommand {
|
|
||||||
New,
|
|
||||||
Load,
|
|
||||||
Save,
|
|
||||||
ToggleViewMode,
|
|
||||||
Delete,
|
|
||||||
Activate,
|
|
||||||
Increment,
|
|
||||||
Decrement,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut,
|
|
||||||
Go(Direction),
|
|
||||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
Scene(SceneCommand),
|
|
||||||
Track(TrackCommand),
|
|
||||||
Clip(ClipCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum SceneCommand {
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Add,
|
|
||||||
Delete,
|
|
||||||
MoveForward,
|
|
||||||
MoveBack,
|
|
||||||
RandomColor,
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum TrackCommand {
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Add,
|
|
||||||
Delete,
|
|
||||||
MoveForward,
|
|
||||||
MoveBack,
|
|
||||||
RandomColor,
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ClipCommand {
|
|
||||||
SetLoop(bool),
|
|
||||||
Get(usize, usize),
|
|
||||||
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
|
||||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum SequencerCommand {
|
pub enum SequencerCommand {
|
||||||
Focus(FocusCommand),
|
Focus(FocusCommand),
|
||||||
|
|
@ -127,17 +64,3 @@ pub enum PhraseEditorCommand {
|
||||||
TimeZoomSet(usize),
|
TimeZoomSet(usize),
|
||||||
Go(Direction),
|
Go(Direction),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
|
||||||
pub enum TransportCommand {
|
|
||||||
FocusNext,
|
|
||||||
FocusPrev,
|
|
||||||
Play(Option<usize>),
|
|
||||||
Pause(Option<usize>),
|
|
||||||
SeekUsec(f64),
|
|
||||||
SeekSample(f64),
|
|
||||||
SeekPulse(f64),
|
|
||||||
SetBpm(f64),
|
|
||||||
SetQuant(f64),
|
|
||||||
SetSync(f64),
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use crate::midly::num::u7;
|
|
||||||
|
|
||||||
impl Scene {
|
|
||||||
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
|
||||||
let mut name = None;
|
|
||||||
let mut clips = vec![];
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
let key = map.get(&Edn::Key(":name"));
|
|
||||||
if let Some(Edn::Str(n)) = key {
|
|
||||||
name = Some(*n);
|
|
||||||
} else {
|
|
||||||
panic!("unexpected key in scene '{name:?}': {key:?}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Edn::Symbol("_") => {
|
|
||||||
clips.push(None);
|
|
||||||
},
|
|
||||||
Edn::Int(i) => {
|
|
||||||
clips.push(Some(*i as usize));
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
|
||||||
});
|
|
||||||
Ok(Scene {
|
|
||||||
name: Arc::new(name.unwrap_or("").to_string().into()),
|
|
||||||
color: ItemColor::random(),
|
|
||||||
clips,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl MixerTrack {
|
|
||||||
const SYM_NAME: &'static str = ":name";
|
|
||||||
const SYM_GAIN: &'static str = ":gain";
|
|
||||||
const SYM_SAMPLER: &'static str = "sampler";
|
|
||||||
const SYM_LV2: &'static str = "lv2";
|
|
||||||
pub fn from_edn <'a, 'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
|
||||||
let mut _gain = 0.0f64;
|
|
||||||
let mut track = MixerTrack {
|
|
||||||
name: String::new(),
|
|
||||||
ports: JackPorts::default(),
|
|
||||||
devices: vec![],
|
|
||||||
};
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut devices: Vec<JackClient> = vec![];
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) {
|
|
||||||
track.name = n.to_string();
|
|
||||||
}
|
|
||||||
if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) {
|
|
||||||
_gain = f64::from(*g);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Edn::List(args) => match args.get(0) {
|
|
||||||
// Add a sampler device to the track
|
|
||||||
Some(Edn::Symbol(Self::SYM_SAMPLER)) => {
|
|
||||||
devices.push(Sampler::from_edn(jack, &args[1..])?);
|
|
||||||
panic!(
|
|
||||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
|
||||||
&track.name,
|
|
||||||
args.get(0).unwrap()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// Add a LV2 plugin to the track.
|
|
||||||
Some(Edn::Symbol(Self::SYM_LV2)) => {
|
|
||||||
devices.push(LV2Plugin::from_edn(jack, &args[1..])?);
|
|
||||||
panic!(
|
|
||||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
|
||||||
&track.name,
|
|
||||||
args.get(0).unwrap()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
None =>
|
|
||||||
panic!("empty list track {}", &track.name),
|
|
||||||
_ =>
|
|
||||||
panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap())
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
});
|
|
||||||
for device in devices {
|
|
||||||
track.add_device(device);
|
|
||||||
}
|
|
||||||
Ok(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LV2Plugin {
|
|
||||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<JackClient> {
|
|
||||||
let mut name = String::new();
|
|
||||||
let mut path = String::new();
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
|
||||||
name = String::from(*n);
|
|
||||||
}
|
|
||||||
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
|
|
||||||
path = String::from(*p);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in lv2 '{name}'"),
|
|
||||||
});
|
|
||||||
Plugin::new_lv2(jack, &name, &path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sampler {
|
|
||||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Sampler> {
|
|
||||||
let mut name = String::new();
|
|
||||||
let mut dir = String::new();
|
|
||||||
let mut samples = BTreeMap::new();
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
|
||||||
name = String::from(*n);
|
|
||||||
}
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
|
|
||||||
dir = String::from(*n);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Edn::List(args) => match args.get(0) {
|
|
||||||
Some(Edn::Symbol("sample")) => {
|
|
||||||
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
|
|
||||||
if let Some(midi) = midi {
|
|
||||||
samples.insert(midi, sample);
|
|
||||||
} else {
|
|
||||||
panic!("sample without midi binding: {}", sample.read().unwrap().name);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in sampler {name}: {args:?}")
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in sampler {name}: {edn:?}")
|
|
||||||
});
|
|
||||||
Ok(Sampler {
|
|
||||||
jack: jack.clone(),
|
|
||||||
name: name.into(),
|
|
||||||
mapped: samples,
|
|
||||||
unmapped: Default::default(),
|
|
||||||
voices: Default::default(),
|
|
||||||
ports: Default::default(),
|
|
||||||
buffer: Default::default(),
|
|
||||||
output_gain: 0.
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sample {
|
|
||||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
|
|
||||||
let mut name = String::new();
|
|
||||||
let mut file = String::new();
|
|
||||||
let mut midi = None;
|
|
||||||
let mut start = 0usize;
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
|
||||||
name = String::from(*n);
|
|
||||||
}
|
|
||||||
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
|
|
||||||
file = String::from(*f);
|
|
||||||
}
|
|
||||||
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
|
|
||||||
start = *i as usize;
|
|
||||||
}
|
|
||||||
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
|
|
||||||
midi = Some(u7::from(*m as u8));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in sample {name}"),
|
|
||||||
});
|
|
||||||
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
|
|
||||||
Ok((midi, Arc::new(RwLock::new(Self {
|
|
||||||
name: name.into(),
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
channels: data,
|
|
||||||
rate: None
|
|
||||||
}))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read WAV from file
|
|
||||||
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
|
||||||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
|
||||||
for channel in wavers::Wav::from_path(src)?.channels() {
|
|
||||||
channels.push(channel);
|
|
||||||
}
|
|
||||||
let mut end = 0;
|
|
||||||
let mut data: Vec<Vec<f32>> = vec![];
|
|
||||||
for samples in channels.iter() {
|
|
||||||
let channel = Vec::from(samples.as_ref());
|
|
||||||
end = end.max(channel.len());
|
|
||||||
data.push(channel);
|
|
||||||
}
|
|
||||||
Ok((end, data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
519
crates/tek_api/src/api_jack.rs
Normal file
519
crates/tek_api/src/api_jack.rs
Normal file
|
|
@ -0,0 +1,519 @@
|
||||||
|
use crate::*;
|
||||||
|
use tek_core::jack::*;
|
||||||
|
|
||||||
|
/// Trait for things that have a JACK process callback.
|
||||||
|
pub trait Audio: Send + Sync {
|
||||||
|
fn process(&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn callback(
|
||||||
|
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
||||||
|
) -> Control where Self: Sized {
|
||||||
|
if let Ok(mut state) = state.write() {
|
||||||
|
state.process(client, scope)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
||||||
|
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
|
||||||
|
/// Perform type erasure for collecting heterogeneous devices.
|
||||||
|
fn boxed(self) -> Box<dyn AudioComponent<E>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All things that implement the required traits can be treated as `AudioComponent`.
|
||||||
|
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
|
||||||
|
|
||||||
|
/// Trait for things that wrap a JACK client.
|
||||||
|
pub trait AudioEngine {
|
||||||
|
fn activate (
|
||||||
|
self,
|
||||||
|
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
||||||
|
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
||||||
|
fn client (&self) -> &Client;
|
||||||
|
fn transport (&self) -> Transport {
|
||||||
|
self.client().transport()
|
||||||
|
}
|
||||||
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||||
|
self.client().port_by_name(name)
|
||||||
|
}
|
||||||
|
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
|
||||||
|
Ok(self.client().register_port(name, spec)?)
|
||||||
|
}
|
||||||
|
fn thread_init (&self, _: &Client) {}
|
||||||
|
unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {}
|
||||||
|
fn freewheel (&mut self, _: &Client, enabled: bool) {}
|
||||||
|
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {}
|
||||||
|
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {}
|
||||||
|
fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {}
|
||||||
|
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn graph_reorder (&mut self, _: &Client) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn xrun (&mut self, _: &Client) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JackClient {
|
||||||
|
/// Before activation.
|
||||||
|
Inactive(Client),
|
||||||
|
/// During activation.
|
||||||
|
Activating,
|
||||||
|
/// After activation. Must not be dropped for JACK thread to persist.
|
||||||
|
Active(DynamicAsyncClient),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
||||||
|
|
||||||
|
pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>;
|
||||||
|
|
||||||
|
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
||||||
|
|
||||||
|
impl JackClient {
|
||||||
|
pub fn new (name: &str) -> Usually<Self> {
|
||||||
|
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||||
|
Ok(Self::Inactive(client))
|
||||||
|
}
|
||||||
|
pub fn activate_with <T: Audio + 'static> (
|
||||||
|
self,
|
||||||
|
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||||
|
)
|
||||||
|
-> Usually<Arc<RwLock<T>>>
|
||||||
|
{
|
||||||
|
let client = Arc::new(RwLock::new(self));
|
||||||
|
let target = Arc::new(RwLock::new(init(&client)?));
|
||||||
|
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||||
|
let events = Notifications(event);
|
||||||
|
let frame = Box::new({
|
||||||
|
let target = target.clone();
|
||||||
|
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
|
||||||
|
target.process(c, s)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||||
|
let mut buffer = Self::Activating;
|
||||||
|
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
|
||||||
|
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JackClient> for Client {
|
||||||
|
fn from (jack: JackClient) -> Client {
|
||||||
|
match jack {
|
||||||
|
JackClient::Inactive(client) => client,
|
||||||
|
JackClient::Activating => panic!("jack client still activating"),
|
||||||
|
JackClient::Active(_) => panic!("jack client already activated"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEngine for JackClient {
|
||||||
|
fn client(&self) -> &Client {
|
||||||
|
match self {
|
||||||
|
Self::Inactive(ref client) => client,
|
||||||
|
Self::Activating => panic!("jack client has not finished activation"),
|
||||||
|
Self::Active(ref client) => client.as_client(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn activate(
|
||||||
|
self,
|
||||||
|
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
|
||||||
|
) -> Usually<Arc<RwLock<Self>>>
|
||||||
|
where
|
||||||
|
Self: Send + Sync + 'static
|
||||||
|
{
|
||||||
|
let client = Client::from(self);
|
||||||
|
let state = Arc::new(RwLock::new(Self::Activating));
|
||||||
|
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||||
|
let events = Notifications(event);
|
||||||
|
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
|
||||||
|
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||||
|
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// Event enum for JACK events.
|
||||||
|
pub enum JackEvent {
|
||||||
|
ThreadInit,
|
||||||
|
Shutdown(ClientStatus, String),
|
||||||
|
Freewheel(bool),
|
||||||
|
SampleRate(Frames),
|
||||||
|
ClientRegistration(String, bool),
|
||||||
|
PortRegistration(PortId, bool),
|
||||||
|
PortRename(PortId, String, String),
|
||||||
|
PortsConnected(PortId, PortId, bool),
|
||||||
|
GraphReorder,
|
||||||
|
XRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for things that may expose JACK ports.
|
||||||
|
pub trait Ports {
|
||||||
|
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_ports<T: PortSpec + Copy>(
|
||||||
|
client: &Client,
|
||||||
|
names: Vec<String>,
|
||||||
|
spec: T,
|
||||||
|
) -> Usually<BTreeMap<String, Port<T>>> {
|
||||||
|
names
|
||||||
|
.into_iter()
|
||||||
|
.try_fold(BTreeMap::new(), |mut ports, name| {
|
||||||
|
let port = client.register_port(&name, spec)?;
|
||||||
|
ports.insert(name, port);
|
||||||
|
Ok(ports)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
|
||||||
|
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
|
||||||
|
let port = client.port_by_name(&name).unwrap();
|
||||||
|
ports.insert(name, port);
|
||||||
|
ports
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notification handler used by the [Jack] factory
|
||||||
|
/// when constructing [JackDevice]s.
|
||||||
|
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
||||||
|
|
||||||
|
/// Generic notification handler that emits [JackEvent]
|
||||||
|
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
||||||
|
|
||||||
|
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
||||||
|
fn thread_init(&self, _: &Client) {
|
||||||
|
self.0(JackEvent::ThreadInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
||||||
|
self.0(JackEvent::Shutdown(status, reason.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
||||||
|
self.0(JackEvent::Freewheel(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
||||||
|
self.0(JackEvent::SampleRate(frames));
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
||||||
|
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
||||||
|
self.0(JackEvent::PortRegistration(id, reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||||
|
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
||||||
|
self.0(JackEvent::PortsConnected(a, b, are));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_reorder(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::GraphReorder);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xrun(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::XRun);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///// A [AudioComponent] bound to a JACK client and a set of ports.
|
||||||
|
//pub struct JackDevice<E: Engine> {
|
||||||
|
///// The active JACK client of this device.
|
||||||
|
//pub client: DynamicAsyncClient,
|
||||||
|
///// The device state, encapsulated for sharing between threads.
|
||||||
|
//pub state: Arc<RwLock<Box<dyn AudioComponent<E>>>>,
|
||||||
|
///// Unowned copies of the device's JACK ports, for connecting to the device.
|
||||||
|
///// The "real" readable/writable `Port`s are owned by the `state`.
|
||||||
|
//pub ports: UnownedJackPorts,
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<E: Engine> std::fmt::Debug for JackDevice<E> {
|
||||||
|
//fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
//f.debug_struct("JackDevice")
|
||||||
|
//.field("ports", &self.ports)
|
||||||
|
//.finish()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<E: Engine> Widget for JackDevice<E> {
|
||||||
|
//type Engine = E;
|
||||||
|
//fn layout(&self, to: E::Size) -> Perhaps<E::Size> {
|
||||||
|
//self.state.read().unwrap().layout(to)
|
||||||
|
//}
|
||||||
|
//fn render(&self, to: &mut E::Output) -> Usually<()> {
|
||||||
|
//self.state.read().unwrap().render(to)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<E: Engine> Handle<E> for JackDevice<E> {
|
||||||
|
//fn handle(&mut self, from: &E::Input) -> Perhaps<E::Handled> {
|
||||||
|
//self.state.write().unwrap().handle(from)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<E: Engine> Ports for JackDevice<E> {
|
||||||
|
//fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
//Ok(self.ports.audio_ins.values().collect())
|
||||||
|
//}
|
||||||
|
//fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
//Ok(self.ports.audio_outs.values().collect())
|
||||||
|
//}
|
||||||
|
//fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
//Ok(self.ports.midi_ins.values().collect())
|
||||||
|
//}
|
||||||
|
//fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
//Ok(self.ports.midi_outs.values().collect())
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl<E: Engine> JackDevice<E> {
|
||||||
|
///// Returns a locked mutex of the state's contents.
|
||||||
|
//pub fn state(&self) -> LockResult<RwLockReadGuard<Box<dyn AudioComponent<E>>>> {
|
||||||
|
//self.state.read()
|
||||||
|
//}
|
||||||
|
///// Returns a locked mutex of the state's contents.
|
||||||
|
//pub fn state_mut(&self) -> LockResult<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||||
|
//self.state.write()
|
||||||
|
//}
|
||||||
|
//pub fn connect_midi_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||||
|
//Ok(self
|
||||||
|
//.client
|
||||||
|
//.as_client()
|
||||||
|
//.connect_ports(port, self.midi_ins()?[index])?)
|
||||||
|
//}
|
||||||
|
//pub fn connect_midi_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||||
|
//Ok(self
|
||||||
|
//.client
|
||||||
|
//.as_client()
|
||||||
|
//.connect_ports(self.midi_outs()?[index], port)?)
|
||||||
|
//}
|
||||||
|
//pub fn connect_audio_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||||
|
//Ok(self
|
||||||
|
//.client
|
||||||
|
//.as_client()
|
||||||
|
//.connect_ports(port, self.audio_ins()?[index])?)
|
||||||
|
//}
|
||||||
|
//pub fn connect_audio_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||||
|
//Ok(self
|
||||||
|
//.client
|
||||||
|
//.as_client()
|
||||||
|
//.connect_ports(self.audio_outs()?[index], port)?)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut].
|
||||||
|
//#[derive(Default, Debug)]
|
||||||
|
//pub struct JackPorts {
|
||||||
|
//pub audio_ins: BTreeMap<String, Port<AudioIn>>,
|
||||||
|
//pub midi_ins: BTreeMap<String, Port<MidiIn>>,
|
||||||
|
//pub audio_outs: BTreeMap<String, Port<AudioOut>>,
|
||||||
|
//pub midi_outs: BTreeMap<String, Port<MidiOut>>,
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// Collection of JACK ports as [Unowned].
|
||||||
|
//#[derive(Default, Debug)]
|
||||||
|
//pub struct UnownedJackPorts {
|
||||||
|
//pub audio_ins: BTreeMap<String, Port<Unowned>>,
|
||||||
|
//pub midi_ins: BTreeMap<String, Port<Unowned>>,
|
||||||
|
//pub audio_outs: BTreeMap<String, Port<Unowned>>,
|
||||||
|
//pub midi_outs: BTreeMap<String, Port<Unowned>>,
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl JackPorts {
|
||||||
|
//pub fn clone_unowned(&self) -> UnownedJackPorts {
|
||||||
|
//let mut unowned = UnownedJackPorts::default();
|
||||||
|
//for (name, port) in self.midi_ins.iter() {
|
||||||
|
//unowned.midi_ins.insert(name.clone(), port.clone_unowned());
|
||||||
|
//}
|
||||||
|
//for (name, port) in self.midi_outs.iter() {
|
||||||
|
//unowned.midi_outs.insert(name.clone(), port.clone_unowned());
|
||||||
|
//}
|
||||||
|
//for (name, port) in self.audio_ins.iter() {
|
||||||
|
//unowned.audio_ins.insert(name.clone(), port.clone_unowned());
|
||||||
|
//}
|
||||||
|
//for (name, port) in self.audio_outs.iter() {
|
||||||
|
//unowned
|
||||||
|
//.audio_outs
|
||||||
|
//.insert(name.clone(), port.clone_unowned());
|
||||||
|
//}
|
||||||
|
//unowned
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// Implement the `Ports` trait.
|
||||||
|
//#[macro_export]
|
||||||
|
//macro_rules! ports {
|
||||||
|
//($T:ty $({ $(audio: {
|
||||||
|
//$(ins: |$ai_arg:ident|$ai_impl:expr,)?
|
||||||
|
//$(outs: |$ao_arg:ident|$ao_impl:expr,)?
|
||||||
|
//})? $(midi: {
|
||||||
|
//$(ins: |$mi_arg:ident|$mi_impl:expr,)?
|
||||||
|
//$(outs: |$mo_arg:ident|$mo_impl:expr,)?
|
||||||
|
//})?})?) => {
|
||||||
|
//impl Ports for $T {$(
|
||||||
|
//$(
|
||||||
|
//$(fn audio_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||||
|
//let cb = |$ai_arg:&'a Self|$ai_impl;
|
||||||
|
//cb(self)
|
||||||
|
//})?
|
||||||
|
//)?
|
||||||
|
//$(
|
||||||
|
//$(fn audio_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||||
|
//let cb = (|$ao_arg:&'a Self|$ao_impl);
|
||||||
|
//cb(self)
|
||||||
|
//})?
|
||||||
|
//)?
|
||||||
|
//)? $(
|
||||||
|
//$(
|
||||||
|
//$(fn midi_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||||
|
//let cb = (|$mi_arg:&'a Self|$mi_impl);
|
||||||
|
//cb(self)
|
||||||
|
//})?
|
||||||
|
//)?
|
||||||
|
//$(
|
||||||
|
//$(fn midi_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||||
|
//let cb = (|$mo_arg:&'a Self|$mo_impl);
|
||||||
|
//cb(self)
|
||||||
|
//})?
|
||||||
|
//)?
|
||||||
|
//)?}
|
||||||
|
//};
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// `JackDevice` factory. Creates JACK `Client`s, performs port registration
|
||||||
|
///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`.
|
||||||
|
//pub struct Jack {
|
||||||
|
//pub client: Client,
|
||||||
|
//pub midi_ins: Vec<String>,
|
||||||
|
//pub audio_ins: Vec<String>,
|
||||||
|
//pub midi_outs: Vec<String>,
|
||||||
|
//pub audio_outs: Vec<String>,
|
||||||
|
//}
|
||||||
|
|
||||||
|
//impl Jack {
|
||||||
|
//pub fn new(name: &str) -> Usually<Self> {
|
||||||
|
//Ok(Self {
|
||||||
|
//midi_ins: vec![],
|
||||||
|
//audio_ins: vec![],
|
||||||
|
//midi_outs: vec![],
|
||||||
|
//audio_outs: vec![],
|
||||||
|
//client: Client::new(name, ClientOptions::NO_START_SERVER)?.0,
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
//pub fn run<'a: 'static, D, E>(
|
||||||
|
//self,
|
||||||
|
//state: impl FnOnce(JackPorts) -> Box<D>,
|
||||||
|
//) -> Usually<JackDevice<E>>
|
||||||
|
//where
|
||||||
|
//D: AudioComponent<E> + Sized + 'static,
|
||||||
|
//E: Engine + 'static,
|
||||||
|
//{
|
||||||
|
//let owned_ports = JackPorts {
|
||||||
|
//audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?,
|
||||||
|
//audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?,
|
||||||
|
//midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?,
|
||||||
|
//midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?,
|
||||||
|
//};
|
||||||
|
//let midi_outs = owned_ports
|
||||||
|
//.midi_outs
|
||||||
|
//.values()
|
||||||
|
//.map(|p| Ok(p.name()?))
|
||||||
|
//.collect::<Usually<Vec<_>>>()?;
|
||||||
|
//let midi_ins = owned_ports
|
||||||
|
//.midi_ins
|
||||||
|
//.values()
|
||||||
|
//.map(|p| Ok(p.name()?))
|
||||||
|
//.collect::<Usually<Vec<_>>>()?;
|
||||||
|
//let audio_outs = owned_ports
|
||||||
|
//.audio_outs
|
||||||
|
//.values()
|
||||||
|
//.map(|p| Ok(p.name()?))
|
||||||
|
//.collect::<Usually<Vec<_>>>()?;
|
||||||
|
//let audio_ins = owned_ports
|
||||||
|
//.audio_ins
|
||||||
|
//.values()
|
||||||
|
//.map(|p| Ok(p.name()?))
|
||||||
|
//.collect::<Usually<Vec<_>>>()?;
|
||||||
|
//let state = Arc::new(RwLock::new(state(owned_ports) as Box<dyn AudioComponent<E>>));
|
||||||
|
//let client = self.client.activate_async(
|
||||||
|
//Notifications(Box::new({
|
||||||
|
//let _state = state.clone();
|
||||||
|
//move |_event| {
|
||||||
|
//// FIXME: this deadlocks
|
||||||
|
////state.lock().unwrap().handle(&event).unwrap();
|
||||||
|
//}
|
||||||
|
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||||
|
//contrib::ClosureProcessHandler::new(Box::new({
|
||||||
|
//let state = state.clone();
|
||||||
|
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
|
||||||
|
//}) as BoxedAudioHandler),
|
||||||
|
//)?;
|
||||||
|
//Ok(JackDevice {
|
||||||
|
//ports: UnownedJackPorts {
|
||||||
|
//audio_ins: query_ports(&client.as_client(), audio_ins),
|
||||||
|
//audio_outs: query_ports(&client.as_client(), audio_outs),
|
||||||
|
//midi_ins: query_ports(&client.as_client(), midi_ins),
|
||||||
|
//midi_outs: query_ports(&client.as_client(), midi_outs),
|
||||||
|
//},
|
||||||
|
//client,
|
||||||
|
//state,
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
//pub fn audio_in(mut self, name: &str) -> Self {
|
||||||
|
//self.audio_ins.push(name.to_string());
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//pub fn audio_out(mut self, name: &str) -> Self {
|
||||||
|
//self.audio_outs.push(name.to_string());
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//pub fn midi_in(mut self, name: &str) -> Self {
|
||||||
|
//self.midi_ins.push(name.to_string());
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//pub fn midi_out(mut self, name: &str) -> Self {
|
||||||
|
//self.midi_outs.push(name.to_string());
|
||||||
|
//self
|
||||||
|
//}
|
||||||
|
//}
|
||||||
65
crates/tek_api/src/arrange.rs
Normal file
65
crates/tek_api/src/arrange.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use scene::*;
|
||||||
|
|
||||||
|
pub struct Arrangement {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
/// Global timebase
|
||||||
|
pub clock: Arc<Clock>,
|
||||||
|
/// Name of arranger
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Collection of phrases.
|
||||||
|
pub phrases: Arc<RwLock<Vec<Phrase>>>,
|
||||||
|
/// Collection of tracks.
|
||||||
|
pub tracks: Vec<SequencerTrack>,
|
||||||
|
/// Collection of scenes.
|
||||||
|
pub scenes: Vec<Scene>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for Arrangement {
|
||||||
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
for track in self.tracks.iter_mut() {
|
||||||
|
track.player.process(client, scope);
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangementCommand {
|
||||||
|
New,
|
||||||
|
Load,
|
||||||
|
Save,
|
||||||
|
ToggleViewMode,
|
||||||
|
Delete,
|
||||||
|
Activate,
|
||||||
|
Increment,
|
||||||
|
Decrement,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Go(Direction),
|
||||||
|
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
Scene(SceneCommand),
|
||||||
|
Track(TrackCommand),
|
||||||
|
Clip(ClipCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangementTrackCommand {
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Add,
|
||||||
|
Delete,
|
||||||
|
MoveForward,
|
||||||
|
MoveBack,
|
||||||
|
RandomColor,
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangementClipCommand {
|
||||||
|
SetLoop(bool),
|
||||||
|
Get(usize, usize),
|
||||||
|
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||||
|
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
}
|
||||||
36
crates/tek_api/src/clock.rs
Normal file
36
crates/tek_api/src/clock.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A timer with starting point, current time, and quantization
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Clock {
|
||||||
|
/// Playback state
|
||||||
|
pub playing: RwLock<Option<TransportState>>,
|
||||||
|
/// Global sample and usec at which playback started
|
||||||
|
pub started: RwLock<Option<(usize, usize)>>,
|
||||||
|
/// Current moment in time
|
||||||
|
pub current: Instant,
|
||||||
|
/// Note quantization factor
|
||||||
|
pub quant: Quantize,
|
||||||
|
/// Launch quantization factor
|
||||||
|
pub sync: LaunchSync,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clock {
|
||||||
|
#[inline] pub fn timebase (&self) -> &Arc<Timebase> {
|
||||||
|
&self.current.timebase
|
||||||
|
}
|
||||||
|
#[inline] pub fn pulse (&self) -> f64 {
|
||||||
|
self.current.pulse.get()
|
||||||
|
}
|
||||||
|
#[inline] pub fn quant (&self) -> f64 {
|
||||||
|
self.quant.get()
|
||||||
|
}
|
||||||
|
#[inline] pub fn sync (&self) -> f64 {
|
||||||
|
self.sync.get()
|
||||||
|
}
|
||||||
|
#[inline] pub fn next_launch_pulse (&self) -> usize {
|
||||||
|
let sync = self.sync.get() as usize;
|
||||||
|
let pulse = self.current.pulse.get() as usize;
|
||||||
|
if pulse % sync == 0 { pulse } else { (pulse / sync + 1) * sync }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,292 +1,23 @@
|
||||||
pub(crate) use tek_core::*;
|
pub(crate) use tek_core::*;
|
||||||
pub(crate) use tek_core::jack::{TransportState, Port, MidiIn, MidiOut};
|
pub(crate) use tek_core::jack::*;
|
||||||
pub(crate) use tek_core::midly::{MidiMessage, num::u7};
|
pub(crate) use tek_core::midly::{*, live::LiveEvent, num::u7};
|
||||||
pub(crate) use std::thread::JoinHandle;
|
pub(crate) use std::thread::JoinHandle;
|
||||||
|
pub(crate) use std::fmt::{Debug, Formatter, Error};
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
|
clock
|
||||||
|
mixer
|
||||||
|
phrase
|
||||||
|
plugin
|
||||||
|
pool
|
||||||
|
sampler
|
||||||
|
sample
|
||||||
|
scene
|
||||||
|
sequencer
|
||||||
|
track
|
||||||
|
transport
|
||||||
|
voice
|
||||||
|
|
||||||
api_cmd
|
api_cmd
|
||||||
api_edn
|
api_jack
|
||||||
}
|
|
||||||
|
|
||||||
/// A timer with starting point, current time, and quantization
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Clock {
|
|
||||||
/// Playback state
|
|
||||||
pub playing: RwLock<Option<TransportState>>,
|
|
||||||
/// Global sample and usec at which playback started
|
|
||||||
pub started: RwLock<Option<(usize, usize)>>,
|
|
||||||
/// Current moment in time
|
|
||||||
pub current: Instant,
|
|
||||||
/// Note quantization factor
|
|
||||||
pub quant: Quantize,
|
|
||||||
/// Launch quantization factor
|
|
||||||
pub sync: LaunchSync,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clock {
|
|
||||||
#[inline] pub fn timebase (&self) -> &Arc<Timebase> {
|
|
||||||
&self.current.timebase
|
|
||||||
}
|
|
||||||
#[inline] pub fn pulse (&self) -> f64 {
|
|
||||||
self.current.pulse.get()
|
|
||||||
}
|
|
||||||
#[inline] pub fn quant (&self) -> f64 {
|
|
||||||
self.quant.get()
|
|
||||||
}
|
|
||||||
#[inline] pub fn sync (&self) -> f64 {
|
|
||||||
self.sync.get()
|
|
||||||
}
|
|
||||||
#[inline] pub fn next_launch_pulse (&self) -> usize {
|
|
||||||
let sync = self.sync.get() as usize;
|
|
||||||
let pulse = self.current.pulse.get() as usize;
|
|
||||||
if pulse % sync == 0 { pulse } else { (pulse / sync + 1) * sync }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TransportToolbar {
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
/// JACK transport handle.
|
|
||||||
pub transport: jack::Transport,
|
|
||||||
/// Current sample rate, tempo, and PPQ.
|
|
||||||
pub clock: Arc<Clock>,
|
|
||||||
/// Enable metronome?
|
|
||||||
pub metronome: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Arrangement {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
/// Global timebase
|
|
||||||
pub clock: Arc<Clock>,
|
|
||||||
/// Name of arranger
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
/// Collection of phrases.
|
|
||||||
pub phrases: Arc<RwLock<Vec<Phrase>>>,
|
|
||||||
/// Collection of tracks.
|
|
||||||
pub tracks: Vec<Sequencer>,
|
|
||||||
/// Collection of scenes.
|
|
||||||
pub scenes: Vec<Scene>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Sequencer {
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phrase player.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PhrasePlayer {
|
|
||||||
/// Global timebase
|
|
||||||
pub clock: Arc<Clock>,
|
|
||||||
/// 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]>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A MIDI sequence.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct Phrase {
|
|
||||||
pub uuid: uuid::Uuid,
|
|
||||||
/// Name of phrase
|
|
||||||
pub name: String,
|
|
||||||
/// Temporal resolution in pulses per quarter note
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Length of phrase in pulses
|
|
||||||
pub length: usize,
|
|
||||||
/// Notes in phrase
|
|
||||||
pub notes: PhraseData,
|
|
||||||
/// Whether to loop the phrase or play it once
|
|
||||||
pub loop_on: bool,
|
|
||||||
/// Start of loop
|
|
||||||
pub loop_start: usize,
|
|
||||||
/// Length of loop
|
|
||||||
pub loop_length: usize,
|
|
||||||
/// All notes are displayed with minimum length
|
|
||||||
pub percussive: bool,
|
|
||||||
/// Identifying color of phrase
|
|
||||||
pub color: ItemColorTriplet,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI message structural
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Mixer {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
pub name: String,
|
|
||||||
pub tracks: Vec<MixerTrack>,
|
|
||||||
pub selected_track: usize,
|
|
||||||
pub selected_column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A mixer track.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MixerTrack {
|
|
||||||
pub name: String,
|
|
||||||
/// Inputs and outputs of 1st and last device
|
|
||||||
pub ports: JackPorts,
|
|
||||||
/// Device chain
|
|
||||||
pub devices: Vec<JackClient>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sampler plugin plays sounds.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Sampler {
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
pub name: String,
|
|
||||||
pub mapped: BTreeMap<u7, Arc<RwLock<Sample>>>,
|
|
||||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
|
||||||
pub ports: JackPorts,
|
|
||||||
pub buffer: Vec<Vec<f32>>,
|
|
||||||
pub output_gain: f32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sound sample.
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Sample {
|
|
||||||
pub name: String,
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
pub channels: Vec<Vec<f32>>,
|
|
||||||
pub rate: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A currently playing instance of a sample.
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
pub struct Voice {
|
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
|
||||||
pub after: usize,
|
|
||||||
pub position: usize,
|
|
||||||
pub velocity: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A plugin device.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Plugin {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
pub name: String,
|
|
||||||
pub path: Option<String>,
|
|
||||||
pub plugin: Option<PluginKind>,
|
|
||||||
pub selected: usize,
|
|
||||||
pub mapping: bool,
|
|
||||||
pub ports: JackPorts,
|
|
||||||
}
|
|
||||||
impl Plugin {
|
|
||||||
pub fn new_lv2 (
|
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
|
||||||
name: &str,
|
|
||||||
path: &str,
|
|
||||||
) -> Usually<JackDevice> {
|
|
||||||
let plugin = LV2Plugin::new(path)?;
|
|
||||||
jack_from_lv2(name, &plugin.plugin)?.run(|ports|Box::new(Self {
|
|
||||||
jack: jack.clone(),
|
|
||||||
name: name.into(),
|
|
||||||
path: Some(String::from(path)),
|
|
||||||
plugin: Some(PluginKind::LV2(plugin)),
|
|
||||||
selected: 0,
|
|
||||||
mapping: false,
|
|
||||||
ports
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supported plugin formats.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum PluginKind {
|
|
||||||
#[default] None,
|
|
||||||
LV2(LV2Plugin),
|
|
||||||
VST2 { instance: ::vst::host::PluginInstance },
|
|
||||||
VST3,
|
|
||||||
}
|
|
||||||
impl std::fmt::Debug for PluginKind {
|
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
||||||
write!(f, "{}", match self {
|
|
||||||
Self::None => "(none)",
|
|
||||||
Self::LV2(_) => "LV2",
|
|
||||||
Self::VST2{..} => "VST2",
|
|
||||||
Self::VST3 => "VST3",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A LV2 plugin.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct LV2Plugin {
|
|
||||||
pub world: livi::World,
|
|
||||||
pub instance: livi::Instance,
|
|
||||||
pub plugin: livi::Plugin,
|
|
||||||
pub features: Arc<livi::Features>,
|
|
||||||
pub port_list: Vec<livi::Port>,
|
|
||||||
pub input_buffer: Vec<livi::event::LV2AtomSequence>,
|
|
||||||
pub ui_thread: Option<JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LV2Plugin {
|
|
||||||
const INPUT_BUFFER: usize = 1024;
|
|
||||||
pub fn new (uri: &str) -> Usually<Self> {
|
|
||||||
let world = livi::World::with_load_bundle(&uri);
|
|
||||||
let features = world
|
|
||||||
.build_features(livi::FeaturesBuilder {
|
|
||||||
min_block_length: 1,
|
|
||||||
max_block_length: 65536,
|
|
||||||
});
|
|
||||||
let plugin = world
|
|
||||||
.iter_plugins()
|
|
||||||
.nth(0)
|
|
||||||
.expect(&format!("plugin not found: {uri}"));
|
|
||||||
Ok(Self {
|
|
||||||
instance: unsafe {
|
|
||||||
plugin
|
|
||||||
.instantiate(features.clone(), 48000.0)
|
|
||||||
.expect(&format!("instantiate failed: {uri}"))
|
|
||||||
},
|
|
||||||
port_list: plugin.ports().collect::<Vec<_>>(),
|
|
||||||
input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
|
|
||||||
ui_thread: None,
|
|
||||||
world,
|
|
||||||
features,
|
|
||||||
plugin,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
crates/tek_api/src/mixer.rs
Normal file
17
crates/tek_api/src/mixer.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Mixer {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
pub name: String,
|
||||||
|
pub tracks: Vec<MixerTrack>,
|
||||||
|
pub selected_track: usize,
|
||||||
|
pub selected_column: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for Mixer {
|
||||||
|
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
78
crates/tek_api/src/phrase.rs
Normal file
78
crates/tek_api/src/phrase.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A MIDI sequence.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Phrase {
|
||||||
|
pub uuid: uuid::Uuid,
|
||||||
|
/// Name of phrase
|
||||||
|
pub name: String,
|
||||||
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of phrase in pulses
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in phrase
|
||||||
|
pub notes: PhraseData,
|
||||||
|
/// Whether to loop the phrase or play it once
|
||||||
|
pub loop_on: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
|
/// Identifying color of phrase
|
||||||
|
pub color: ItemColorTriplet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI message structural
|
||||||
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
impl Phrase {
|
||||||
|
pub fn new (
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
loop_on: bool,
|
||||||
|
length: usize,
|
||||||
|
notes: Option<PhraseData>,
|
||||||
|
color: Option<ItemColorTriplet>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
uuid: uuid::Uuid::new_v4(),
|
||||||
|
name: name.as_ref().to_string(),
|
||||||
|
ppq: PPQ,
|
||||||
|
length,
|
||||||
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
|
loop_on,
|
||||||
|
loop_start: 0,
|
||||||
|
loop_length: length,
|
||||||
|
percussive: true,
|
||||||
|
color: color.unwrap_or_else(ItemColorTriplet::random)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn duplicate (&self) -> Self {
|
||||||
|
let mut clone = self.clone();
|
||||||
|
clone.uuid = uuid::Uuid::new_v4();
|
||||||
|
clone
|
||||||
|
}
|
||||||
|
pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; }
|
||||||
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||||
|
if pulse >= self.length { panic!("extend phrase first") }
|
||||||
|
self.notes[pulse].push(message);
|
||||||
|
}
|
||||||
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||||
|
//panic!("{:?} {start} {end}", &self);
|
||||||
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||||
|
for event in events.iter() {
|
||||||
|
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for Phrase {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
|
||||||
|
impl Eq for Phrase {}
|
||||||
190
crates/tek_api/src/plugin.rs
Normal file
190
crates/tek_api/src/plugin.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A plugin device.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Plugin {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
pub name: String,
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub plugin: Option<PluginKind>,
|
||||||
|
pub selected: usize,
|
||||||
|
pub mapping: bool,
|
||||||
|
}
|
||||||
|
impl Plugin {
|
||||||
|
pub fn new_lv2 (
|
||||||
|
jack: &Arc<RwLock<JackClient>>,
|
||||||
|
name: &str,
|
||||||
|
path: &str,
|
||||||
|
) -> Usually<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.into(),
|
||||||
|
path: Some(String::from(path)),
|
||||||
|
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
||||||
|
selected: 0,
|
||||||
|
mapping: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||||
|
//let counts = plugin.port_counts();
|
||||||
|
//let mut jack = Jack::new(name)?;
|
||||||
|
//for i in 0..counts.atom_sequence_inputs {
|
||||||
|
//jack = jack.midi_in(&format!("midi-in-{i}"))
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.atom_sequence_outputs {
|
||||||
|
//jack = jack.midi_out(&format!("midi-out-{i}"));
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.audio_inputs {
|
||||||
|
//jack = jack.audio_in(&format!("audio-in-{i}"));
|
||||||
|
//}
|
||||||
|
//for i in 0..counts.audio_outputs {
|
||||||
|
//jack = jack.audio_out(&format!("audio-out-{i}"));
|
||||||
|
//}
|
||||||
|
//Ok(jack)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported plugin formats.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum PluginKind {
|
||||||
|
#[default] None,
|
||||||
|
LV2(LV2Plugin),
|
||||||
|
VST2 { instance: ::vst::host::PluginInstance },
|
||||||
|
VST3,
|
||||||
|
}
|
||||||
|
impl Debug for PluginKind {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> Result<(), Error> {
|
||||||
|
write!(f, "{}", match self {
|
||||||
|
Self::None => "(none)",
|
||||||
|
Self::LV2(_) => "LV2",
|
||||||
|
Self::VST2{..} => "VST2",
|
||||||
|
Self::VST3 => "VST3",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A LV2 plugin.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LV2Plugin {
|
||||||
|
pub world: livi::World,
|
||||||
|
pub instance: livi::Instance,
|
||||||
|
pub plugin: livi::Plugin,
|
||||||
|
pub features: Arc<livi::Features>,
|
||||||
|
pub port_list: Vec<livi::Port>,
|
||||||
|
pub input_buffer: Vec<livi::event::LV2AtomSequence>,
|
||||||
|
pub ui_thread: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LV2Plugin {
|
||||||
|
const INPUT_BUFFER: usize = 1024;
|
||||||
|
pub fn new (uri: &str) -> Usually<Self> {
|
||||||
|
let world = livi::World::with_load_bundle(&uri);
|
||||||
|
let features = world
|
||||||
|
.build_features(livi::FeaturesBuilder {
|
||||||
|
min_block_length: 1,
|
||||||
|
max_block_length: 65536,
|
||||||
|
});
|
||||||
|
let plugin = world
|
||||||
|
.iter_plugins()
|
||||||
|
.nth(0)
|
||||||
|
.expect(&format!("plugin not found: {uri}"));
|
||||||
|
Ok(Self {
|
||||||
|
instance: unsafe {
|
||||||
|
plugin
|
||||||
|
.instantiate(features.clone(), 48000.0)
|
||||||
|
.expect(&format!("instantiate failed: {uri}"))
|
||||||
|
},
|
||||||
|
port_list: plugin.ports().collect::<Vec<_>>(),
|
||||||
|
input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
|
||||||
|
ui_thread: None,
|
||||||
|
world,
|
||||||
|
features,
|
||||||
|
plugin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LV2Plugin {
|
||||||
|
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut path = String::new();
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||||
|
name = String::from(*n);
|
||||||
|
}
|
||||||
|
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
|
||||||
|
path = String::from(*p);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in lv2 '{name}'"),
|
||||||
|
});
|
||||||
|
Plugin::new_lv2(jack, &name, &path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for LV2Plugin {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for Plugin {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
match self.plugin.as_mut() {
|
||||||
|
Some(PluginKind::LV2(LV2Plugin {
|
||||||
|
features,
|
||||||
|
ref mut instance,
|
||||||
|
ref mut input_buffer,
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
let urid = features.midi_urid();
|
||||||
|
input_buffer.clear();
|
||||||
|
for port in self.ports.midi_ins.values() {
|
||||||
|
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||||
|
&features,
|
||||||
|
scope.n_frames() as usize
|
||||||
|
);
|
||||||
|
for event in port.iter(scope) {
|
||||||
|
match event.bytes.len() {
|
||||||
|
3 => atom.push_midi_event::<3>(
|
||||||
|
event.time as i64,
|
||||||
|
urid,
|
||||||
|
&event.bytes[0..3]
|
||||||
|
).unwrap(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input_buffer.push(atom);
|
||||||
|
}
|
||||||
|
let mut outputs = vec![];
|
||||||
|
for _ in self.ports.midi_outs.iter() {
|
||||||
|
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||||
|
&features,
|
||||||
|
scope.n_frames() as usize
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let ports = ::livi::EmptyPortConnections::new()
|
||||||
|
.with_atom_sequence_inputs(
|
||||||
|
input_buffer.iter()
|
||||||
|
)
|
||||||
|
.with_atom_sequence_outputs(
|
||||||
|
outputs.iter_mut()
|
||||||
|
)
|
||||||
|
.with_audio_inputs(
|
||||||
|
self.ports.audio_ins.values().map(|o|o.as_slice(scope))
|
||||||
|
)
|
||||||
|
.with_audio_outputs(
|
||||||
|
self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope))
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
17
crates/tek_api/src/pool.rs
Normal file
17
crates/tek_api/src/pool.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Contains all phrases in a project
|
||||||
|
pub struct PhrasePool {
|
||||||
|
/// Scroll offset
|
||||||
|
pub scroll: usize,
|
||||||
|
/// Highlighted phrase
|
||||||
|
pub phrase: usize,
|
||||||
|
/// Phrases in the pool
|
||||||
|
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
||||||
|
/// Mode switch
|
||||||
|
pub mode: Option<PhrasePoolMode>,
|
||||||
|
/// Whether this widget is focused
|
||||||
|
pub focused: bool,
|
||||||
|
/// Whether this widget is entered
|
||||||
|
pub entered: bool,
|
||||||
|
}
|
||||||
72
crates/tek_api/src/sample.rs
Normal file
72
crates/tek_api/src/sample.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A sound sample.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Sample {
|
||||||
|
pub name: String,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub channels: Vec<Vec<f32>>,
|
||||||
|
pub rate: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sample {
|
||||||
|
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||||
|
Self { name: name.to_string(), start, end, channels, rate: None }
|
||||||
|
}
|
||||||
|
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||||
|
Voice {
|
||||||
|
sample: sample.clone(),
|
||||||
|
after,
|
||||||
|
position: sample.read().unwrap().start,
|
||||||
|
velocity: velocity.as_int() as f32 / 127.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut file = String::new();
|
||||||
|
let mut midi = None;
|
||||||
|
let mut start = 0usize;
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||||
|
name = String::from(*n);
|
||||||
|
}
|
||||||
|
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
|
||||||
|
file = String::from(*f);
|
||||||
|
}
|
||||||
|
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
|
||||||
|
start = *i as usize;
|
||||||
|
}
|
||||||
|
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
|
||||||
|
midi = Some(u7::from(*m as u8));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in sample {name}"),
|
||||||
|
});
|
||||||
|
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
|
||||||
|
Ok((midi, Arc::new(RwLock::new(Self {
|
||||||
|
name: name.into(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
channels: data,
|
||||||
|
rate: None
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read WAV from file
|
||||||
|
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||||
|
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||||
|
channels.push(channel);
|
||||||
|
}
|
||||||
|
let mut end = 0;
|
||||||
|
let mut data: Vec<Vec<f32>> = vec![];
|
||||||
|
for samples in channels.iter() {
|
||||||
|
let channel = Vec::from(samples.as_ref());
|
||||||
|
end = end.max(channel.len());
|
||||||
|
data.push(channel);
|
||||||
|
}
|
||||||
|
Ok((end, data))
|
||||||
|
}
|
||||||
|
}
|
||||||
74
crates/tek_api/src/sampler.rs
Normal file
74
crates/tek_api/src/sampler.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// The sampler plugin plays sounds.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Sampler {
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
pub name: String,
|
||||||
|
pub mapped: BTreeMap<u7, Arc<RwLock<Sample>>>,
|
||||||
|
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
pub midi_in: Port<MidiIn>,
|
||||||
|
pub audio_outs: Vec<Port<MidiOut>>,
|
||||||
|
pub buffer: Vec<Vec<f32>>,
|
||||||
|
pub output_gain: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for Sampler {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
self.process_midi_in(scope);
|
||||||
|
self.clear_output_buffer();
|
||||||
|
self.process_audio_out(scope);
|
||||||
|
self.write_output_buffer(scope);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sampler {
|
||||||
|
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut dir = String::new();
|
||||||
|
let mut samples = BTreeMap::new();
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||||
|
name = String::from(*n);
|
||||||
|
}
|
||||||
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
|
||||||
|
dir = String::from(*n);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Edn::List(args) => match args.get(0) {
|
||||||
|
Some(Edn::Symbol("sample")) => {
|
||||||
|
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
|
||||||
|
if let Some(midi) = midi {
|
||||||
|
samples.insert(midi, sample);
|
||||||
|
} else {
|
||||||
|
panic!("sample without midi binding: {}", sample.read().unwrap().name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in sampler {name}: {args:?}")
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in sampler {name}: {edn:?}")
|
||||||
|
});
|
||||||
|
Ok(Sampler {
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.into(),
|
||||||
|
mapped: samples,
|
||||||
|
unmapped: Default::default(),
|
||||||
|
voices: Default::default(),
|
||||||
|
ports: Default::default(),
|
||||||
|
buffer: Default::default(),
|
||||||
|
output_gain: 0.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A currently playing instance of a sample.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Voice {
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub after: usize,
|
||||||
|
pub position: usize,
|
||||||
|
pub velocity: f32,
|
||||||
|
}
|
||||||
53
crates/tek_api/src/scene.rs
Normal file
53
crates/tek_api/src/scene.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut name = None;
|
||||||
|
let mut clips = vec![];
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
let key = map.get(&Edn::Key(":name"));
|
||||||
|
if let Some(Edn::Str(n)) = key {
|
||||||
|
name = Some(*n);
|
||||||
|
} else {
|
||||||
|
panic!("unexpected key in scene '{name:?}': {key:?}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Edn::Symbol("_") => {
|
||||||
|
clips.push(None);
|
||||||
|
},
|
||||||
|
Edn::Int(i) => {
|
||||||
|
clips.push(Some(*i as usize));
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||||||
|
});
|
||||||
|
Ok(Scene {
|
||||||
|
name: Arc::new(name.unwrap_or("").to_string().into()),
|
||||||
|
color: ItemColor::random(),
|
||||||
|
clips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SceneCommand {
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Add,
|
||||||
|
Delete,
|
||||||
|
MoveForward,
|
||||||
|
MoveBack,
|
||||||
|
RandomColor,
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,45 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// JACK process callback for sequencer app
|
pub enum SequencerCommand {}
|
||||||
impl Audio for Sequencer {
|
|
||||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
#[derive(Debug)]
|
||||||
if let Some(ref transport) = self.transport {
|
pub struct SequencerTrack {
|
||||||
transport.write().unwrap().process(client, scope);
|
/// Name of track
|
||||||
}
|
pub name: Arc<RwLock<String>>,
|
||||||
self.player.process(client, scope);
|
/// Preferred width of track column
|
||||||
Control::Continue
|
pub width: usize,
|
||||||
}
|
/// Identifying color of track
|
||||||
|
pub color: ItemColor,
|
||||||
|
/// Global timebase
|
||||||
|
pub clock: Arc<Clock>,
|
||||||
|
/// 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]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JACK process callback for a sequencer's phrase player/recorder.
|
/// JACK process callback for a sequencer's phrase player/recorder.
|
||||||
impl Audio for PhrasePlayer {
|
impl Audio for SequencerTrack {
|
||||||
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();
|
||||||
|
|
@ -38,7 +65,7 @@ impl Audio for PhrasePlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Methods used primarily by the process callback
|
/// Methods used primarily by the process callback
|
||||||
impl PhrasePlayer {
|
impl SequencerTrack {
|
||||||
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)
|
||||||
}
|
}
|
||||||
86
crates/tek_api/src/track.rs
Normal file
86
crates/tek_api/src/track.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub enum MixerTrackCommand {}
|
||||||
|
|
||||||
|
/// A mixer track.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MixerTrack {
|
||||||
|
pub name: String,
|
||||||
|
/// Inputs of 1st device
|
||||||
|
pub audio_ins: Vec<Port<AudioIn>>,
|
||||||
|
/// Outputs of last device
|
||||||
|
pub audio_outs: Vec<Port<AudioOut>>,
|
||||||
|
/// Device chain
|
||||||
|
pub devices: Vec<Box<dyn MixerTrackDevice>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MixerTrackDevice: Audio + Debug {
|
||||||
|
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MixerTrackDevice for Sampler {}
|
||||||
|
|
||||||
|
impl MixerTrackDevice for LV2Plugin {}
|
||||||
|
|
||||||
|
impl MixerTrack {
|
||||||
|
const SYM_NAME: &'static str = ":name";
|
||||||
|
const SYM_GAIN: &'static str = ":gain";
|
||||||
|
const SYM_SAMPLER: &'static str = "sampler";
|
||||||
|
const SYM_LV2: &'static str = "lv2";
|
||||||
|
pub fn from_edn <'a, 'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut _gain = 0.0f64;
|
||||||
|
let mut track = MixerTrack {
|
||||||
|
name: String::new(),
|
||||||
|
audio_ins: vec![],
|
||||||
|
audio_outs: vec![],
|
||||||
|
devices: vec![],
|
||||||
|
};
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut devices: Vec<JackClient> = vec![];
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) {
|
||||||
|
track.name = n.to_string();
|
||||||
|
}
|
||||||
|
if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) {
|
||||||
|
_gain = f64::from(*g);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Edn::List(args) => match args.get(0) {
|
||||||
|
// Add a sampler device to the track
|
||||||
|
Some(Edn::Symbol(Self::SYM_SAMPLER)) => {
|
||||||
|
track.add_device(
|
||||||
|
Sampler::from_edn(jack, &args[1..])?
|
||||||
|
);
|
||||||
|
panic!(
|
||||||
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
||||||
|
&track.name,
|
||||||
|
args.get(0).unwrap()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// Add a LV2 plugin to the track.
|
||||||
|
Some(Edn::Symbol(Self::SYM_LV2)) => {
|
||||||
|
track.add_device(
|
||||||
|
LV2Plugin::from_edn(jack, &args[1..])?
|
||||||
|
);
|
||||||
|
panic!(
|
||||||
|
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
||||||
|
&track.name,
|
||||||
|
args.get(0).unwrap()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
None =>
|
||||||
|
panic!("empty list track {}", &track.name),
|
||||||
|
_ =>
|
||||||
|
panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap())
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
Ok(track)
|
||||||
|
}
|
||||||
|
pub fn add_device (&mut self, device: impl MixerTrackDevice) {
|
||||||
|
self.devices.push(device.boxed())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
impl Audio for TransportToolbar {
|
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum TransportCommand {
|
||||||
|
FocusNext,
|
||||||
|
FocusPrev,
|
||||||
|
Play(Option<usize>),
|
||||||
|
Pause(Option<usize>),
|
||||||
|
SeekUsec(f64),
|
||||||
|
SeekSample(f64),
|
||||||
|
SeekPulse(f64),
|
||||||
|
SetBpm(f64),
|
||||||
|
SetQuant(f64),
|
||||||
|
SetSync(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Transport {
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: jack::Transport,
|
||||||
|
/// Current sample rate, tempo, and PPQ.
|
||||||
|
pub clock: Arc<Clock>,
|
||||||
|
/// Enable metronome?
|
||||||
|
pub metronome: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio for Transport {
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
let times = scope.cycle_times().unwrap();
|
let times = scope.cycle_times().unwrap();
|
||||||
let CycleTimes { current_frames, current_usecs, next_usecs: _, period_usecs: _ } = times;
|
let CycleTimes { current_frames, current_usecs, next_usecs: _, period_usecs: _ } = times;
|
||||||
0
crates/tek_api/src/voice.rs
Normal file
0
crates/tek_api/src/voice.rs
Normal file
|
|
@ -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(Arranger::new(
|
Ok(ArrangerView::new(
|
||||||
jack,
|
jack,
|
||||||
self.transport.then_some(transport),
|
self.transport.then_some(transport),
|
||||||
arrangement,
|
arrangement,
|
||||||
|
|
@ -53,3 +53,33 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,6 @@ impl SequencerCli {
|
||||||
fn run (&self) -> Usually<()> {
|
fn run (&self) -> Usually<()> {
|
||||||
Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{
|
Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{
|
||||||
let transport = TransportToolbar::new(jack, None);
|
let transport = TransportToolbar::new(jack, None);
|
||||||
let sequencer = Sequencer {
|
|
||||||
jack: jack.clone(),
|
|
||||||
focus_cursor: (1, 1),
|
|
||||||
entered: false,
|
|
||||||
phrases: Arc::new(RwLock::new(PhrasePool::new())),
|
|
||||||
editor: PhraseEditor::new(),
|
|
||||||
clock: transport.clock.clone(),
|
|
||||||
player: PhrasePlayer::new(jack, &transport.clock, "tek_sequencer")?,
|
|
||||||
transport: self.transport.then_some(Arc::new(RwLock::new(transport))),
|
|
||||||
};
|
|
||||||
if let Some(_) = self.name.as_ref() {
|
if let Some(_) = self.name.as_ref() {
|
||||||
// TODO: sequencer.name = Arc::new(RwLock::new(name.clone()));
|
// TODO: sequencer.name = Arc::new(RwLock::new(name.clone()));
|
||||||
}
|
}
|
||||||
|
|
@ -41,8 +31,28 @@ impl SequencerCli {
|
||||||
//phrase.write().unwrap().length = length;
|
//phrase.write().unwrap().length = length;
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
Ok(sequencer)
|
Ok(SequencerView {
|
||||||
|
jack: jack.clone(),
|
||||||
|
focus_cursor: (1, 1),
|
||||||
|
entered: false,
|
||||||
|
phrases: Arc::new(RwLock::new(PhrasePool::new())),
|
||||||
|
editor: PhraseEditor::new(),
|
||||||
|
clock: transport.clock.clone(),
|
||||||
|
player: PhrasePlayer::new(jack, &transport.clock, "tek_sequencer")?,
|
||||||
|
transport: self.transport.then_some(Arc::new(RwLock::new(transport))),
|
||||||
|
})
|
||||||
})?)?;
|
})?)?;
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,510 +1,2 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use jack::*;
|
use jack::*;
|
||||||
/// Trait for things that wrap a JACK client.
|
|
||||||
pub trait AudioEngine {
|
|
||||||
fn activate (
|
|
||||||
self,
|
|
||||||
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
|
||||||
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
|
||||||
fn client (&self) -> &Client;
|
|
||||||
fn transport (&self) -> Transport {
|
|
||||||
self.client().transport()
|
|
||||||
}
|
|
||||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
|
||||||
self.client().port_by_name(name)
|
|
||||||
}
|
|
||||||
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
|
|
||||||
Ok(self.client().register_port(name, spec)?)
|
|
||||||
}
|
|
||||||
fn thread_init (&self, _: &Client) {}
|
|
||||||
unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {}
|
|
||||||
fn freewheel (&mut self, _: &Client, enabled: bool) {}
|
|
||||||
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {}
|
|
||||||
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {}
|
|
||||||
fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {}
|
|
||||||
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn graph_reorder (&mut self, _: &Client) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn xrun (&mut self, _: &Client) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum JackClient {
|
|
||||||
/// Before activation.
|
|
||||||
Inactive(Client),
|
|
||||||
/// During activation.
|
|
||||||
Activating,
|
|
||||||
/// After activation. Must not be dropped for JACK thread to persist.
|
|
||||||
Active(DynamicAsyncClient),
|
|
||||||
}
|
|
||||||
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
|
||||||
pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>;
|
|
||||||
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
|
||||||
impl JackClient {
|
|
||||||
pub fn new (name: &str) -> Usually<Self> {
|
|
||||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
|
||||||
Ok(Self::Inactive(client))
|
|
||||||
}
|
|
||||||
pub fn activate_with <T: Audio + 'static> (
|
|
||||||
self,
|
|
||||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
|
||||||
)
|
|
||||||
-> Usually<Arc<RwLock<T>>>
|
|
||||||
{
|
|
||||||
let client = Arc::new(RwLock::new(self));
|
|
||||||
let target = Arc::new(RwLock::new(init(&client)?));
|
|
||||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
|
||||||
let events = Notifications(event);
|
|
||||||
let frame = Box::new({
|
|
||||||
let target = target.clone();
|
|
||||||
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
|
|
||||||
target.process(c, s)
|
|
||||||
} else {
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
|
||||||
let mut buffer = Self::Activating;
|
|
||||||
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
|
|
||||||
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
|
|
||||||
Ok(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<JackClient> for Client {
|
|
||||||
fn from (jack: JackClient) -> Client {
|
|
||||||
match jack {
|
|
||||||
JackClient::Inactive(client) => client,
|
|
||||||
JackClient::Activating => panic!("jack client still activating"),
|
|
||||||
JackClient::Active(_) => panic!("jack client already activated"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl AudioEngine for JackClient {
|
|
||||||
fn client(&self) -> &Client {
|
|
||||||
match self {
|
|
||||||
Self::Inactive(ref client) => client,
|
|
||||||
Self::Activating => panic!("jack client has not finished activation"),
|
|
||||||
Self::Active(ref client) => client.as_client(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn activate(
|
|
||||||
self,
|
|
||||||
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
|
|
||||||
) -> Usually<Arc<RwLock<Self>>>
|
|
||||||
where
|
|
||||||
Self: Send + Sync + 'static
|
|
||||||
{
|
|
||||||
let client = Client::from(self);
|
|
||||||
let state = Arc::new(RwLock::new(Self::Activating));
|
|
||||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
|
||||||
let events = Notifications(event);
|
|
||||||
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
|
|
||||||
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
|
||||||
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
|
||||||
Ok(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Trait for things that have a JACK process callback.
|
|
||||||
pub trait Audio: Send + Sync {
|
|
||||||
fn process(&mut self, _: &Client, _: &ProcessScope) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn callback(
|
|
||||||
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
|
||||||
) -> Control where Self: Sized {
|
|
||||||
if let Ok(mut state) = state.write() {
|
|
||||||
state.process(client, scope)
|
|
||||||
} else {
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that may expose JACK ports.
|
|
||||||
pub trait Ports {
|
|
||||||
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
|
||||||
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
|
|
||||||
/// Perform type erasure for collecting heterogeneous devices.
|
|
||||||
fn boxed(self) -> Box<dyn AudioComponent<E>>
|
|
||||||
where
|
|
||||||
Self: Sized + 'static,
|
|
||||||
{
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All things that implement the required traits can be treated as `AudioComponent`.
|
|
||||||
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
|
|
||||||
|
|
||||||
/// `JackDevice` factory. Creates JACK `Client`s, performs port registration
|
|
||||||
/// and activation, and encapsulates a `AudioComponent` into a `JackDevice`.
|
|
||||||
pub struct Jack {
|
|
||||||
pub client: Client,
|
|
||||||
pub midi_ins: Vec<String>,
|
|
||||||
pub audio_ins: Vec<String>,
|
|
||||||
pub midi_outs: Vec<String>,
|
|
||||||
pub audio_outs: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Jack {
|
|
||||||
pub fn new(name: &str) -> Usually<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
midi_ins: vec![],
|
|
||||||
audio_ins: vec![],
|
|
||||||
midi_outs: vec![],
|
|
||||||
audio_outs: vec![],
|
|
||||||
client: Client::new(name, ClientOptions::NO_START_SERVER)?.0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn run<'a: 'static, D, E>(
|
|
||||||
self,
|
|
||||||
state: impl FnOnce(JackPorts) -> Box<D>,
|
|
||||||
) -> Usually<JackDevice<E>>
|
|
||||||
where
|
|
||||||
D: AudioComponent<E> + Sized + 'static,
|
|
||||||
E: Engine + 'static,
|
|
||||||
{
|
|
||||||
let owned_ports = JackPorts {
|
|
||||||
audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?,
|
|
||||||
audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?,
|
|
||||||
midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?,
|
|
||||||
midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?,
|
|
||||||
};
|
|
||||||
let midi_outs = owned_ports
|
|
||||||
.midi_outs
|
|
||||||
.values()
|
|
||||||
.map(|p| Ok(p.name()?))
|
|
||||||
.collect::<Usually<Vec<_>>>()?;
|
|
||||||
let midi_ins = owned_ports
|
|
||||||
.midi_ins
|
|
||||||
.values()
|
|
||||||
.map(|p| Ok(p.name()?))
|
|
||||||
.collect::<Usually<Vec<_>>>()?;
|
|
||||||
let audio_outs = owned_ports
|
|
||||||
.audio_outs
|
|
||||||
.values()
|
|
||||||
.map(|p| Ok(p.name()?))
|
|
||||||
.collect::<Usually<Vec<_>>>()?;
|
|
||||||
let audio_ins = owned_ports
|
|
||||||
.audio_ins
|
|
||||||
.values()
|
|
||||||
.map(|p| Ok(p.name()?))
|
|
||||||
.collect::<Usually<Vec<_>>>()?;
|
|
||||||
let state = Arc::new(RwLock::new(state(owned_ports) as Box<dyn AudioComponent<E>>));
|
|
||||||
let client = self.client.activate_async(
|
|
||||||
Notifications(Box::new({
|
|
||||||
let _state = state.clone();
|
|
||||||
move |_event| {
|
|
||||||
// FIXME: this deadlocks
|
|
||||||
//state.lock().unwrap().handle(&event).unwrap();
|
|
||||||
}
|
|
||||||
}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
|
||||||
contrib::ClosureProcessHandler::new(Box::new({
|
|
||||||
let state = state.clone();
|
|
||||||
move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
|
|
||||||
}) as BoxedAudioHandler),
|
|
||||||
)?;
|
|
||||||
Ok(JackDevice {
|
|
||||||
ports: UnownedJackPorts {
|
|
||||||
audio_ins: query_ports(&client.as_client(), audio_ins),
|
|
||||||
audio_outs: query_ports(&client.as_client(), audio_outs),
|
|
||||||
midi_ins: query_ports(&client.as_client(), midi_ins),
|
|
||||||
midi_outs: query_ports(&client.as_client(), midi_outs),
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn audio_in(mut self, name: &str) -> Self {
|
|
||||||
self.audio_ins.push(name.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn audio_out(mut self, name: &str) -> Self {
|
|
||||||
self.audio_outs.push(name.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn midi_in(mut self, name: &str) -> Self {
|
|
||||||
self.midi_ins.push(name.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn midi_out(mut self, name: &str) -> Self {
|
|
||||||
self.midi_outs.push(name.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_ports<T: PortSpec + Copy>(
|
|
||||||
client: &Client,
|
|
||||||
names: Vec<String>,
|
|
||||||
spec: T,
|
|
||||||
) -> Usually<BTreeMap<String, Port<T>>> {
|
|
||||||
names
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(BTreeMap::new(), |mut ports, name| {
|
|
||||||
let port = client.register_port(&name, spec)?;
|
|
||||||
ports.insert(name, port);
|
|
||||||
Ok(ports)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
|
|
||||||
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
|
|
||||||
let port = client.port_by_name(&name).unwrap();
|
|
||||||
ports.insert(name, port);
|
|
||||||
ports
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notification handler used by the [Jack] factory
|
|
||||||
/// when constructing [JackDevice]s.
|
|
||||||
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// Event enum for JACK events.
|
|
||||||
pub enum JackEvent {
|
|
||||||
ThreadInit,
|
|
||||||
Shutdown(ClientStatus, String),
|
|
||||||
Freewheel(bool),
|
|
||||||
SampleRate(Frames),
|
|
||||||
ClientRegistration(String, bool),
|
|
||||||
PortRegistration(PortId, bool),
|
|
||||||
PortRename(PortId, String, String),
|
|
||||||
PortsConnected(PortId, PortId, bool),
|
|
||||||
GraphReorder,
|
|
||||||
XRun,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic notification handler that emits [JackEvent]
|
|
||||||
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
|
||||||
|
|
||||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
|
||||||
fn thread_init(&self, _: &Client) {
|
|
||||||
self.0(JackEvent::ThreadInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
|
||||||
self.0(JackEvent::Shutdown(status, reason.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
|
||||||
self.0(JackEvent::Freewheel(enabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
|
||||||
self.0(JackEvent::SampleRate(frames));
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
|
||||||
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
|
||||||
self.0(JackEvent::PortRegistration(id, reg));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
|
||||||
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
|
||||||
self.0(JackEvent::PortsConnected(a, b, are));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn graph_reorder(&mut self, _: &Client) -> Control {
|
|
||||||
self.0(JackEvent::GraphReorder);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xrun(&mut self, _: &Client) -> Control {
|
|
||||||
self.0(JackEvent::XRun);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [AudioComponent] bound to a JACK client and a set of ports.
|
|
||||||
pub struct JackDevice<E: Engine> {
|
|
||||||
/// The active JACK client of this device.
|
|
||||||
pub client: DynamicAsyncClient,
|
|
||||||
/// The device state, encapsulated for sharing between threads.
|
|
||||||
pub state: Arc<RwLock<Box<dyn AudioComponent<E>>>>,
|
|
||||||
/// Unowned copies of the device's JACK ports, for connecting to the device.
|
|
||||||
/// The "real" readable/writable `Port`s are owned by the `state`.
|
|
||||||
pub ports: UnownedJackPorts,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> std::fmt::Debug for JackDevice<E> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("JackDevice")
|
|
||||||
.field("ports", &self.ports)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Widget for JackDevice<E> {
|
|
||||||
type Engine = E;
|
|
||||||
fn layout(&self, to: E::Size) -> Perhaps<E::Size> {
|
|
||||||
self.state.read().unwrap().layout(to)
|
|
||||||
}
|
|
||||||
fn render(&self, to: &mut E::Output) -> Usually<()> {
|
|
||||||
self.state.read().unwrap().render(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Handle<E> for JackDevice<E> {
|
|
||||||
fn handle(&mut self, from: &E::Input) -> Perhaps<E::Handled> {
|
|
||||||
self.state.write().unwrap().handle(from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Ports for JackDevice<E> {
|
|
||||||
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(self.ports.audio_ins.values().collect())
|
|
||||||
}
|
|
||||||
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(self.ports.audio_outs.values().collect())
|
|
||||||
}
|
|
||||||
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(self.ports.midi_ins.values().collect())
|
|
||||||
}
|
|
||||||
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(self.ports.midi_outs.values().collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> JackDevice<E> {
|
|
||||||
/// Returns a locked mutex of the state's contents.
|
|
||||||
pub fn state(&self) -> LockResult<RwLockReadGuard<Box<dyn AudioComponent<E>>>> {
|
|
||||||
self.state.read()
|
|
||||||
}
|
|
||||||
/// Returns a locked mutex of the state's contents.
|
|
||||||
pub fn state_mut(&self) -> LockResult<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
|
||||||
self.state.write()
|
|
||||||
}
|
|
||||||
pub fn connect_midi_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.as_client()
|
|
||||||
.connect_ports(port, self.midi_ins()?[index])?)
|
|
||||||
}
|
|
||||||
pub fn connect_midi_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.as_client()
|
|
||||||
.connect_ports(self.midi_outs()?[index], port)?)
|
|
||||||
}
|
|
||||||
pub fn connect_audio_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.as_client()
|
|
||||||
.connect_ports(port, self.audio_ins()?[index])?)
|
|
||||||
}
|
|
||||||
pub fn connect_audio_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.as_client()
|
|
||||||
.connect_ports(self.audio_outs()?[index], port)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut].
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct JackPorts {
|
|
||||||
pub audio_ins: BTreeMap<String, Port<AudioIn>>,
|
|
||||||
pub midi_ins: BTreeMap<String, Port<MidiIn>>,
|
|
||||||
pub audio_outs: BTreeMap<String, Port<AudioOut>>,
|
|
||||||
pub midi_outs: BTreeMap<String, Port<MidiOut>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection of JACK ports as [Unowned].
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct UnownedJackPorts {
|
|
||||||
pub audio_ins: BTreeMap<String, Port<Unowned>>,
|
|
||||||
pub midi_ins: BTreeMap<String, Port<Unowned>>,
|
|
||||||
pub audio_outs: BTreeMap<String, Port<Unowned>>,
|
|
||||||
pub midi_outs: BTreeMap<String, Port<Unowned>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JackPorts {
|
|
||||||
pub fn clone_unowned(&self) -> UnownedJackPorts {
|
|
||||||
let mut unowned = UnownedJackPorts::default();
|
|
||||||
for (name, port) in self.midi_ins.iter() {
|
|
||||||
unowned.midi_ins.insert(name.clone(), port.clone_unowned());
|
|
||||||
}
|
|
||||||
for (name, port) in self.midi_outs.iter() {
|
|
||||||
unowned.midi_outs.insert(name.clone(), port.clone_unowned());
|
|
||||||
}
|
|
||||||
for (name, port) in self.audio_ins.iter() {
|
|
||||||
unowned.audio_ins.insert(name.clone(), port.clone_unowned());
|
|
||||||
}
|
|
||||||
for (name, port) in self.audio_outs.iter() {
|
|
||||||
unowned
|
|
||||||
.audio_outs
|
|
||||||
.insert(name.clone(), port.clone_unowned());
|
|
||||||
}
|
|
||||||
unowned
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement the `Ports` trait.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! ports {
|
|
||||||
($T:ty $({ $(audio: {
|
|
||||||
$(ins: |$ai_arg:ident|$ai_impl:expr,)?
|
|
||||||
$(outs: |$ao_arg:ident|$ao_impl:expr,)?
|
|
||||||
})? $(midi: {
|
|
||||||
$(ins: |$mi_arg:ident|$mi_impl:expr,)?
|
|
||||||
$(outs: |$mo_arg:ident|$mo_impl:expr,)?
|
|
||||||
})?})?) => {
|
|
||||||
impl Ports for $T {$(
|
|
||||||
$(
|
|
||||||
$(fn audio_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
|
||||||
let cb = |$ai_arg:&'a Self|$ai_impl;
|
|
||||||
cb(self)
|
|
||||||
})?
|
|
||||||
)?
|
|
||||||
$(
|
|
||||||
$(fn audio_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
|
||||||
let cb = (|$ao_arg:&'a Self|$ao_impl);
|
|
||||||
cb(self)
|
|
||||||
})?
|
|
||||||
)?
|
|
||||||
)? $(
|
|
||||||
$(
|
|
||||||
$(fn midi_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
|
||||||
let cb = (|$mi_arg:&'a Self|$mi_impl);
|
|
||||||
cb(self)
|
|
||||||
})?
|
|
||||||
)?
|
|
||||||
$(
|
|
||||||
$(fn midi_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
|
||||||
let cb = (|$mo_arg:&'a Self|$mo_impl);
|
|
||||||
cb(self)
|
|
||||||
})?
|
|
||||||
)?
|
|
||||||
)?}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ use std::fmt::{Debug, Display};
|
||||||
}
|
}
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
audio
|
//audio
|
||||||
color
|
color
|
||||||
command
|
command
|
||||||
edn
|
edn
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_snd"
|
|
||||||
edition = "2021"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tek_core = { path = "../tek_core" }
|
|
||||||
tek_api = { path = "../tek_api" }
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
jack-based audio engine
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
pub(crate) use tek_core::*;
|
|
||||||
submod! {
|
|
||||||
snd_arranger
|
|
||||||
snd_mixer
|
|
||||||
snd_plugin
|
|
||||||
snd_sampler
|
|
||||||
snd_sequencer
|
|
||||||
snd_transport
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Audio for Arranger {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Audio for Mixer {
|
|
||||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Audio for Plugin {
|
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
match self.plugin.as_mut() {
|
|
||||||
Some(PluginKind::LV2(LV2Plugin {
|
|
||||||
features,
|
|
||||||
ref mut instance,
|
|
||||||
ref mut input_buffer,
|
|
||||||
..
|
|
||||||
})) => {
|
|
||||||
let urid = features.midi_urid();
|
|
||||||
input_buffer.clear();
|
|
||||||
for port in self.ports.midi_ins.values() {
|
|
||||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
|
||||||
&features,
|
|
||||||
scope.n_frames() as usize
|
|
||||||
);
|
|
||||||
for event in port.iter(scope) {
|
|
||||||
match event.bytes.len() {
|
|
||||||
3 => atom.push_midi_event::<3>(
|
|
||||||
event.time as i64,
|
|
||||||
urid,
|
|
||||||
&event.bytes[0..3]
|
|
||||||
).unwrap(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input_buffer.push(atom);
|
|
||||||
}
|
|
||||||
let mut outputs = vec![];
|
|
||||||
for _ in self.ports.midi_outs.iter() {
|
|
||||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
|
||||||
&features,
|
|
||||||
scope.n_frames() as usize
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let ports = ::livi::EmptyPortConnections::new()
|
|
||||||
.with_atom_sequence_inputs(
|
|
||||||
input_buffer.iter()
|
|
||||||
)
|
|
||||||
.with_atom_sequence_outputs(
|
|
||||||
outputs.iter_mut()
|
|
||||||
)
|
|
||||||
.with_audio_inputs(
|
|
||||||
self.ports.audio_ins.values().map(|o|o.as_slice(scope))
|
|
||||||
)
|
|
||||||
.with_audio_outputs(
|
|
||||||
self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope))
|
|
||||||
);
|
|
||||||
unsafe {
|
|
||||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
|
||||||
};
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Audio for Sampler {
|
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
self.process_midi_in(scope);
|
|
||||||
self.clear_output_buffer();
|
|
||||||
self.process_audio_out(scope);
|
|
||||||
self.write_output_buffer(scope);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ArrangerCommand {
|
|
||||||
Focus(FocusCommand),
|
|
||||||
Transport(TransportCommand),
|
|
||||||
Phrases(PhrasePoolCommand),
|
|
||||||
Editor(PhraseEditorCommand),
|
|
||||||
Arrangement(ArrangementCommand),
|
|
||||||
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ArrangementCommand {
|
|
||||||
New,
|
|
||||||
Load,
|
|
||||||
Save,
|
|
||||||
ToggleViewMode,
|
|
||||||
Delete,
|
|
||||||
Activate,
|
|
||||||
Increment,
|
|
||||||
Decrement,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut,
|
|
||||||
MoveBack,
|
|
||||||
MoveForward,
|
|
||||||
RandomColor,
|
|
||||||
Put,
|
|
||||||
Get,
|
|
||||||
AddScene,
|
|
||||||
AddTrack,
|
|
||||||
ToggleLoop,
|
|
||||||
GoUp,
|
|
||||||
GoDown,
|
|
||||||
GoLeft,
|
|
||||||
GoRight,
|
|
||||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
|
|
||||||
fn execute (self, state: &mut Arranger<E>) -> Perhaps<Self> {
|
|
||||||
let undo = match self {
|
|
||||||
Self::Focus(cmd) => {
|
|
||||||
delegate(cmd, Self::Focus, state)
|
|
||||||
},
|
|
||||||
Self::Phrases(cmd) => {
|
|
||||||
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
|
|
||||||
},
|
|
||||||
Self::Editor(cmd) => {
|
|
||||||
delegate(cmd, Self::Editor, &mut state.editor)
|
|
||||||
},
|
|
||||||
Self::Arrangement(cmd) => {
|
|
||||||
delegate(cmd, Self::Arrangement, &mut state.arrangement)
|
|
||||||
},
|
|
||||||
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
|
|
||||||
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
Self::EditPhrase(phrase) => {
|
|
||||||
state.editor.phrase = phrase.clone();
|
|
||||||
state.focus(ArrangerFocus::PhraseEditor);
|
|
||||||
state.focus_enter();
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
state.show_phrase();
|
|
||||||
state.update_status();
|
|
||||||
return Ok(undo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
|
|
||||||
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
|
|
||||||
use ArrangementCommand::*;
|
|
||||||
match self {
|
|
||||||
New => todo!(),
|
|
||||||
Load => todo!(),
|
|
||||||
Save => todo!(),
|
|
||||||
Edit(phrase) => { state.phrase = phrase.clone() },
|
|
||||||
ToggleViewMode => { state.mode.to_next(); },
|
|
||||||
Delete => { state.delete(); },
|
|
||||||
Activate => { state.activate(); },
|
|
||||||
Increment => { state.increment(); },
|
|
||||||
Decrement => { state.decrement(); },
|
|
||||||
ZoomIn => { state.zoom_in(); },
|
|
||||||
ZoomOut => { state.zoom_out(); },
|
|
||||||
MoveBack => { state.move_back(); },
|
|
||||||
MoveForward => { state.move_forward(); },
|
|
||||||
RandomColor => { state.randomize_color(); },
|
|
||||||
Put => { state.phrase_put(); },
|
|
||||||
Get => { state.phrase_get(); },
|
|
||||||
AddScene => { state.scene_add(None, None)?; },
|
|
||||||
AddTrack => { state.track_add(None, None)?; },
|
|
||||||
ToggleLoop => { state.toggle_loop() },
|
|
||||||
GoUp => { state.go_up() },
|
|
||||||
GoDown => { state.go_down() },
|
|
||||||
GoLeft => { state.go_left() },
|
|
||||||
GoRight => { state.go_right() },
|
|
||||||
};
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,36 +10,24 @@ pub(crate) use std::ffi::OsString;
|
||||||
pub(crate) use std::fs::read_dir;
|
pub(crate) use std::fs::read_dir;
|
||||||
|
|
||||||
submod! {
|
submod! {
|
||||||
arranger
|
tui_arranger
|
||||||
arranger_cmd
|
tui_arranger_bar
|
||||||
arranger_snd
|
tui_arranger_cmd
|
||||||
arranger_tui
|
tui_arranger_col
|
||||||
arranger_tui_bar
|
tui_arranger_hor
|
||||||
arranger_tui_cmd
|
tui_arranger_ver
|
||||||
arranger_tui_col
|
tui_sequencer
|
||||||
arranger_tui_hor
|
tui_sequencer_cmd
|
||||||
arranger_tui_ver
|
tui_transport
|
||||||
sequencer
|
tui_transport_cmd
|
||||||
sequencer_cmd
|
tui_mixer
|
||||||
sequencer_snd
|
tui_mixer_cmd
|
||||||
sequencer_tui
|
tui_sampler
|
||||||
transport
|
tui_sampler_cmd
|
||||||
transport_cmd
|
tui_plugin
|
||||||
transport_snd
|
tui_plugin_cmd
|
||||||
transport_tui
|
tui_plugin_lv2
|
||||||
mixer
|
tui_plugin_lv2_gui
|
||||||
mixer_snd
|
tui_plugin_vst2
|
||||||
mixer_cmd
|
tui_plugin_vst3
|
||||||
mixer_tui
|
|
||||||
sampler
|
|
||||||
sampler_snd
|
|
||||||
sampler_cmd
|
|
||||||
plugin
|
|
||||||
plugin_snd
|
|
||||||
plugin_cmd
|
|
||||||
plugin_tui
|
|
||||||
plugin_lv2
|
|
||||||
plugin_lv2_gui
|
|
||||||
plugin_vst2
|
|
||||||
plugin_vst3
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Content for Mixer<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Stack::right(|add| {
|
|
||||||
for channel in self.tracks.iter() {
|
|
||||||
add(channel)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for Track<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
TrackView {
|
|
||||||
chain: Some(&self),
|
|
||||||
direction: tek_core::Direction::Right,
|
|
||||||
focused: true,
|
|
||||||
entered: true,
|
|
||||||
//pub channels: u8,
|
|
||||||
//pub input_ports: Vec<Port<AudioIn>>,
|
|
||||||
//pub pre_gain_meter: f64,
|
|
||||||
//pub gain: f64,
|
|
||||||
//pub insert_ports: Vec<Port<AudioOut>>,
|
|
||||||
//pub return_ports: Vec<Port<AudioIn>>,
|
|
||||||
//pub post_gain_meter: f64,
|
|
||||||
//pub post_insert_meter: f64,
|
|
||||||
//pub level: f64,
|
|
||||||
//pub pan: f64,
|
|
||||||
//pub output_ports: Vec<Port<AudioOut>>,
|
|
||||||
//pub post_fader_meter: f64,
|
|
||||||
//pub route: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// A plugin device.
|
|
||||||
pub struct Plugin<E> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
pub name: String,
|
|
||||||
pub path: Option<String>,
|
|
||||||
pub plugin: Option<PluginKind>,
|
|
||||||
pub selected: usize,
|
|
||||||
pub mapping: bool,
|
|
||||||
pub ports: JackPorts,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Plugin<E> {
|
|
||||||
/// Create a plugin host device.
|
|
||||||
pub fn new (
|
|
||||||
jack: &Arc<RwLock<JackClient>>,
|
|
||||||
name: &str,
|
|
||||||
) -> Usually<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
jack: jack.clone(),
|
|
||||||
name: name.into(),
|
|
||||||
path: None,
|
|
||||||
plugin: None,
|
|
||||||
selected: 0,
|
|
||||||
mapping: false,
|
|
||||||
ports: JackPorts::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Widget for Sampler<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
tui_render_sampler(self, to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let [x, y, _, height] = to.area();
|
|
||||||
let style = Style::default().gray();
|
|
||||||
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
|
|
||||||
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
|
|
||||||
let mut width = title.len() + 2;
|
|
||||||
let mut y1 = 1;
|
|
||||||
let mut j = 0;
|
|
||||||
for (note, sample) in sampler.mapped.iter()
|
|
||||||
.map(|(note, sample)|(Some(note), sample))
|
|
||||||
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
|
|
||||||
{
|
|
||||||
if y1 >= height {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let active = j == sampler.cursor.0;
|
|
||||||
width = width.max(
|
|
||||||
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
|
|
||||||
);
|
|
||||||
y1 = y1 + 1;
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
let height = ((2 + y1) as u16).min(height);
|
|
||||||
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_sample (
|
|
||||||
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
|
||||||
) -> Usually<usize> {
|
|
||||||
let style = if focus { Style::default().green() } else { Style::default() };
|
|
||||||
if focus {
|
|
||||||
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
|
||||||
}
|
|
||||||
let label1 = format!("{:3} {:12}",
|
|
||||||
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
|
||||||
sample.name);
|
|
||||||
let label2 = format!("{:>6} {:>6} +0.0",
|
|
||||||
sample.start,
|
|
||||||
sample.end);
|
|
||||||
to.blit(&label1, x+2, y, Some(style.bold()));
|
|
||||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
|
||||||
Ok(label1.len() + label2.len() + 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for AddSampleModal {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
|
||||||
todo!()
|
|
||||||
//Align::Center(()).layout(to)
|
|
||||||
}
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
todo!()
|
|
||||||
//let area = to.area();
|
|
||||||
//to.make_dim();
|
|
||||||
//let area = center_box(
|
|
||||||
//area,
|
|
||||||
//64.max(area.w().saturating_sub(8)),
|
|
||||||
//20.max(area.w().saturating_sub(8)),
|
|
||||||
//);
|
|
||||||
//to.fill_fg(area, Color::Reset);
|
|
||||||
//to.fill_bg(area, Nord::bg_lo(true, true));
|
|
||||||
//to.fill_char(area, ' ');
|
|
||||||
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
|
|
||||||
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
|
|
||||||
//for (i, (is_dir, name)) in self.subdirs.iter()
|
|
||||||
//.map(|path|(true, path))
|
|
||||||
//.chain(self.files.iter().map(|path|(false, path)))
|
|
||||||
//.enumerate()
|
|
||||||
//.skip(self.offset)
|
|
||||||
//{
|
|
||||||
//if i >= area.h() as usize - 4 {
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//let t = if is_dir { "" } else { "" };
|
|
||||||
//let line = format!("{t} {}", name.to_string_lossy());
|
|
||||||
//let line = &line[..line.len().min(area.w() as usize - 4)];
|
|
||||||
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
|
|
||||||
//Style::default().green()
|
|
||||||
//} else {
|
|
||||||
//Style::default().white()
|
|
||||||
//}))?;
|
|
||||||
//}
|
|
||||||
//Lozenge(Style::default()).draw(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,481 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
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`
|
|
||||||
pub struct Sequencer<E: Engine> {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Arc<RwLock<JackClient>>,
|
|
||||||
/// Controls the JACK transport.
|
|
||||||
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
|
||||||
/// Global timebase
|
|
||||||
pub clock: Arc<TransportTime>,
|
|
||||||
/// Pool of all phrases available to the sequencer
|
|
||||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
|
||||||
/// Phrase editor view
|
|
||||||
pub editor: PhraseEditor<E>,
|
|
||||||
/// Phrase player
|
|
||||||
pub player: PhrasePlayer,
|
|
||||||
/// Which view is focused
|
|
||||||
pub focus_cursor: (usize, usize),
|
|
||||||
/// Whether the currently focused item 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,
|
|
||||||
}
|
|
||||||
/// Contains all phrases in a project
|
|
||||||
pub struct PhrasePool<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Scroll offset
|
|
||||||
pub scroll: usize,
|
|
||||||
/// Highlighted phrase
|
|
||||||
pub phrase: usize,
|
|
||||||
/// Phrases in the pool
|
|
||||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
|
||||||
/// 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),
|
|
||||||
}
|
|
||||||
/// A MIDI sequence.
|
|
||||||
#[derive(Debug, Clone)] pub struct Phrase {
|
|
||||||
pub uuid: uuid::Uuid,
|
|
||||||
/// Name of phrase
|
|
||||||
pub name: String,
|
|
||||||
/// Temporal resolution in pulses per quarter note
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Length of phrase in pulses
|
|
||||||
pub length: usize,
|
|
||||||
/// Notes in phrase
|
|
||||||
pub notes: PhraseData,
|
|
||||||
/// Whether to loop the phrase or play it once
|
|
||||||
pub loop_on: bool,
|
|
||||||
/// Start of loop
|
|
||||||
pub loop_start: usize,
|
|
||||||
/// Length of loop
|
|
||||||
pub loop_length: usize,
|
|
||||||
/// All notes are displayed with minimum length
|
|
||||||
pub percussive: bool,
|
|
||||||
/// Identifying color of phrase
|
|
||||||
pub color: ItemColorTriplet,
|
|
||||||
}
|
|
||||||
/// 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 Phrase {
|
|
||||||
pub fn new (
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
loop_on: bool,
|
|
||||||
length: usize,
|
|
||||||
notes: Option<PhraseData>,
|
|
||||||
color: Option<ItemColorTriplet>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
uuid: uuid::Uuid::new_v4(),
|
|
||||||
name: name.as_ref().to_string(),
|
|
||||||
ppq: PPQ,
|
|
||||||
length,
|
|
||||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
||||||
loop_on,
|
|
||||||
loop_start: 0,
|
|
||||||
loop_length: length,
|
|
||||||
percussive: true,
|
|
||||||
color: color.unwrap_or_else(ItemColorTriplet::random)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn duplicate (&self) -> Self {
|
|
||||||
let mut clone = self.clone();
|
|
||||||
clone.uuid = uuid::Uuid::new_v4();
|
|
||||||
clone
|
|
||||||
}
|
|
||||||
pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; }
|
|
||||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
||||||
if pulse >= self.length { panic!("extend phrase first") }
|
|
||||||
self.notes[pulse].push(message);
|
|
||||||
}
|
|
||||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
||||||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
||||||
//panic!("{:?} {start} {end}", &self);
|
|
||||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
||||||
for event in events.iter() {
|
|
||||||
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for Phrase {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for Phrase { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
|
|
||||||
impl Eq for Phrase {}
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
//! Multi-track mixer
|
|
||||||
include!("lib.rs");
|
|
||||||
pub fn main () -> Usually<()> {
|
|
||||||
Tui::run(Arc::new(RwLock::new(crate::Track::new("")?)))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
/// Stores and displays time-related state.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TransportView<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
state: TransportToolbar,
|
|
||||||
focused: bool,
|
|
||||||
focus: TransportFocus,
|
|
||||||
}
|
|
||||||
/// Which item of the transport toolbar is focused
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
pub enum TransportFocus {
|
|
||||||
Bpm,
|
|
||||||
Sync,
|
|
||||||
PlayPause,
|
|
||||||
Clock,
|
|
||||||
Quant,
|
|
||||||
}
|
|
||||||
impl<E: Engine> TransportView<E> {
|
|
||||||
pub fn new (jack: &Arc<RwLock<JackClient>>, clock: Option<&Arc<TransportTime>>) -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
focused: false,
|
|
||||||
focus: TransportFocus::PlayPause,
|
|
||||||
state: TransportToolbar {
|
|
||||||
metronome: false,
|
|
||||||
transport: jack.read().unwrap().transport(),
|
|
||||||
jack: jack.clone(),
|
|
||||||
clock: match clock {
|
|
||||||
Some(clock) => clock.clone(),
|
|
||||||
None => {
|
|
||||||
let timebase = Arc::new(Timebase::default());
|
|
||||||
Arc::new(TransportTime {
|
|
||||||
playing: Some(TransportState::Stopped).into(),
|
|
||||||
quant: 24.into(),
|
|
||||||
sync: (timebase.ppq.get() * 4.).into(),
|
|
||||||
current: Instant::default(),
|
|
||||||
started: None.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn toggle_play (&mut self) -> Usually<()> {
|
|
||||||
let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet");
|
|
||||||
let playing = match playing {
|
|
||||||
TransportState::Stopped => {
|
|
||||||
self.transport.start()?;
|
|
||||||
Some(TransportState::Starting)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
self.transport.stop()?;
|
|
||||||
self.transport.locate(0)?;
|
|
||||||
Some(TransportState::Stopped)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
*self.clock.playing.write().unwrap() = playing;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TransportFocus {
|
|
||||||
pub fn next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::PlayPause => Self::Bpm,
|
|
||||||
Self::Bpm => Self::Quant,
|
|
||||||
Self::Quant => Self::Sync,
|
|
||||||
Self::Sync => Self::Clock,
|
|
||||||
Self::Clock => Self::PlayPause,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::PlayPause => Self::Clock,
|
|
||||||
Self::Bpm => Self::PlayPause,
|
|
||||||
Self::Quant => Self::Bpm,
|
|
||||||
Self::Sync => Self::Quant,
|
|
||||||
Self::Clock => Self::Sync,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -811,3 +811,56 @@ impl Scene {
|
||||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
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,15 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangerCommand {
|
||||||
|
Focus(FocusCommand),
|
||||||
|
Transport(TransportCommand),
|
||||||
|
Phrases(PhrasePoolCommand),
|
||||||
|
Editor(PhraseEditorCommand),
|
||||||
|
Arrangement(ArrangementCommand),
|
||||||
|
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle top-level events in standalone arranger.
|
/// Handle top-level events in standalone arranger.
|
||||||
impl Handle<Tui> for Arranger<Tui> {
|
impl Handle<Tui> for Arranger<Tui> {
|
||||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||||
|
|
@ -159,3 +169,103 @@ impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
||||||
//Ok(Some(true))
|
//Ok(Some(true))
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangerCommand {
|
||||||
|
Focus(FocusCommand),
|
||||||
|
Transport(TransportCommand),
|
||||||
|
Phrases(PhrasePoolCommand),
|
||||||
|
Editor(PhraseEditorCommand),
|
||||||
|
Arrangement(ArrangementCommand),
|
||||||
|
EditPhrase(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArrangementCommand {
|
||||||
|
New,
|
||||||
|
Load,
|
||||||
|
Save,
|
||||||
|
ToggleViewMode,
|
||||||
|
Delete,
|
||||||
|
Activate,
|
||||||
|
Increment,
|
||||||
|
Decrement,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
MoveBack,
|
||||||
|
MoveForward,
|
||||||
|
RandomColor,
|
||||||
|
Put,
|
||||||
|
Get,
|
||||||
|
AddScene,
|
||||||
|
AddTrack,
|
||||||
|
ToggleLoop,
|
||||||
|
GoUp,
|
||||||
|
GoDown,
|
||||||
|
GoLeft,
|
||||||
|
GoRight,
|
||||||
|
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
|
||||||
|
fn execute (self, state: &mut Arranger<E>) -> Perhaps<Self> {
|
||||||
|
let undo = match self {
|
||||||
|
Self::Focus(cmd) => {
|
||||||
|
delegate(cmd, Self::Focus, state)
|
||||||
|
},
|
||||||
|
Self::Phrases(cmd) => {
|
||||||
|
delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap())
|
||||||
|
},
|
||||||
|
Self::Editor(cmd) => {
|
||||||
|
delegate(cmd, Self::Editor, &mut state.editor)
|
||||||
|
},
|
||||||
|
Self::Arrangement(cmd) => {
|
||||||
|
delegate(cmd, Self::Arrangement, &mut state.arrangement)
|
||||||
|
},
|
||||||
|
Self::Transport(cmd) => if let Some(ref transport) = state.transport {
|
||||||
|
delegate(cmd, Self::Transport, &mut*transport.write().unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Self::EditPhrase(phrase) => {
|
||||||
|
state.editor.phrase = phrase.clone();
|
||||||
|
state.focus(ArrangerFocus::PhraseEditor);
|
||||||
|
state.focus_enter();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
state.show_phrase();
|
||||||
|
state.update_status();
|
||||||
|
return Ok(undo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
|
||||||
|
fn execute (self, state: &mut Arrangement<E>) -> Perhaps<Self> {
|
||||||
|
use ArrangementCommand::*;
|
||||||
|
match self {
|
||||||
|
New => todo!(),
|
||||||
|
Load => todo!(),
|
||||||
|
Save => todo!(),
|
||||||
|
Edit(phrase) => { state.phrase = phrase.clone() },
|
||||||
|
ToggleViewMode => { state.mode.to_next(); },
|
||||||
|
Delete => { state.delete(); },
|
||||||
|
Activate => { state.activate(); },
|
||||||
|
Increment => { state.increment(); },
|
||||||
|
Decrement => { state.decrement(); },
|
||||||
|
ZoomIn => { state.zoom_in(); },
|
||||||
|
ZoomOut => { state.zoom_out(); },
|
||||||
|
MoveBack => { state.move_back(); },
|
||||||
|
MoveForward => { state.move_forward(); },
|
||||||
|
RandomColor => { state.randomize_color(); },
|
||||||
|
Put => { state.phrase_put(); },
|
||||||
|
Get => { state.phrase_get(); },
|
||||||
|
AddScene => { state.scene_add(None, None)?; },
|
||||||
|
AddTrack => { state.track_add(None, None)?; },
|
||||||
|
ToggleLoop => { state.toggle_loop() },
|
||||||
|
GoUp => { state.go_up() },
|
||||||
|
GoDown => { state.go_down() },
|
||||||
|
GoLeft => { state.go_left() },
|
||||||
|
GoRight => { state.go_right() },
|
||||||
|
};
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -88,8 +88,6 @@ impl<E: Engine> Track<E> {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct TrackView<'a, E: Engine> {
|
pub struct TrackView<'a, E: Engine> {
|
||||||
pub chain: Option<&'a Track<E>>,
|
pub chain: Option<&'a Track<E>>,
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
|
|
@ -131,3 +129,40 @@ impl<'a> Widget for TrackView<'a, Tui> {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Content for Mixer<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Stack::right(|add| {
|
||||||
|
for channel in self.tracks.iter() {
|
||||||
|
add(channel)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for Track<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
TrackView {
|
||||||
|
chain: Some(&self),
|
||||||
|
direction: tek_core::Direction::Right,
|
||||||
|
focused: true,
|
||||||
|
entered: true,
|
||||||
|
//pub channels: u8,
|
||||||
|
//pub input_ports: Vec<Port<AudioIn>>,
|
||||||
|
//pub pre_gain_meter: f64,
|
||||||
|
//pub gain: f64,
|
||||||
|
//pub insert_ports: Vec<Port<AudioOut>>,
|
||||||
|
//pub return_ports: Vec<Port<AudioIn>>,
|
||||||
|
//pub post_gain_meter: f64,
|
||||||
|
//pub post_insert_meter: f64,
|
||||||
|
//pub level: f64,
|
||||||
|
//pub pan: f64,
|
||||||
|
//pub output_ports: Vec<Port<AudioOut>>,
|
||||||
|
//pub post_fader_meter: f64,
|
||||||
|
//pub route: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// A plugin device.
|
||||||
|
pub struct Plugin<E> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
pub name: String,
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub plugin: Option<PluginKind>,
|
||||||
|
pub selected: usize,
|
||||||
|
pub mapping: bool,
|
||||||
|
pub ports: JackPorts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Plugin<E> {
|
||||||
|
/// Create a plugin host device.
|
||||||
|
pub fn new (
|
||||||
|
jack: &Arc<RwLock<JackClient>>,
|
||||||
|
name: &str,
|
||||||
|
) -> Usually<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.into(),
|
||||||
|
path: None,
|
||||||
|
plugin: None,
|
||||||
|
selected: 0,
|
||||||
|
mapping: false,
|
||||||
|
ports: JackPorts::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
impl Widget for Plugin<Tui> {
|
impl Widget for Plugin<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]> {
|
||||||
|
|
@ -56,21 +56,3 @@ impl ApplicationHandler for LV2PluginUI {
|
||||||
fn lv2_ui_instantiate (kind: &str) {
|
fn lv2_ui_instantiate (kind: &str) {
|
||||||
//let host = Suil
|
//let host = Suil
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
|
||||||
let counts = plugin.port_counts();
|
|
||||||
let mut jack = Jack::new(name)?;
|
|
||||||
for i in 0..counts.atom_sequence_inputs {
|
|
||||||
jack = jack.midi_in(&format!("midi-in-{i}"))
|
|
||||||
}
|
|
||||||
for i in 0..counts.atom_sequence_outputs {
|
|
||||||
jack = jack.midi_out(&format!("midi-out-{i}"));
|
|
||||||
}
|
|
||||||
for i in 0..counts.audio_inputs {
|
|
||||||
jack = jack.audio_in(&format!("audio-in-{i}"));
|
|
||||||
}
|
|
||||||
for i in 0..counts.audio_outputs {
|
|
||||||
jack = jack.audio_out(&format!("audio-out-{i}"));
|
|
||||||
}
|
|
||||||
Ok(jack)
|
|
||||||
}
|
|
||||||
|
|
@ -117,20 +117,6 @@ pub struct Sample {
|
||||||
pub rate: Option<usize>,
|
pub rate: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sample {
|
|
||||||
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
|
||||||
Self { name: name.to_string(), start, end, channels, rate: None }
|
|
||||||
}
|
|
||||||
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
|
||||||
Voice {
|
|
||||||
sample: sample.clone(),
|
|
||||||
after,
|
|
||||||
position: sample.read().unwrap().start,
|
|
||||||
velocity: velocity.as_int() as f32 / 127.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 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 {
|
||||||
|
|
@ -412,3 +398,100 @@ impl Iterator for Voice {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Widget for Sampler<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
tui_render_sampler(self, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tui_render_sampler (sampler: &Sampler<Tui>, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
let [x, y, _, height] = to.area();
|
||||||
|
let style = Style::default().gray();
|
||||||
|
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
|
||||||
|
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
|
||||||
|
let mut width = title.len() + 2;
|
||||||
|
let mut y1 = 1;
|
||||||
|
let mut j = 0;
|
||||||
|
for (note, sample) in sampler.mapped.iter()
|
||||||
|
.map(|(note, sample)|(Some(note), sample))
|
||||||
|
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
|
||||||
|
{
|
||||||
|
if y1 >= height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let active = j == sampler.cursor.0;
|
||||||
|
width = width.max(
|
||||||
|
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
|
||||||
|
);
|
||||||
|
y1 = y1 + 1;
|
||||||
|
j = j + 1;
|
||||||
|
}
|
||||||
|
let height = ((2 + y1) as u16).min(height);
|
||||||
|
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_sample (
|
||||||
|
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||||
|
) -> Usually<usize> {
|
||||||
|
let style = if focus { Style::default().green() } else { Style::default() };
|
||||||
|
if focus {
|
||||||
|
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||||
|
}
|
||||||
|
let label1 = format!("{:3} {:12}",
|
||||||
|
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||||
|
sample.name);
|
||||||
|
let label2 = format!("{:>6} {:>6} +0.0",
|
||||||
|
sample.start,
|
||||||
|
sample.end);
|
||||||
|
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||||
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
|
Ok(label1.len() + label2.len() + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for AddSampleModal {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||||
|
todo!()
|
||||||
|
//Align::Center(()).layout(to)
|
||||||
|
}
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
todo!()
|
||||||
|
//let area = to.area();
|
||||||
|
//to.make_dim();
|
||||||
|
//let area = center_box(
|
||||||
|
//area,
|
||||||
|
//64.max(area.w().saturating_sub(8)),
|
||||||
|
//20.max(area.w().saturating_sub(8)),
|
||||||
|
//);
|
||||||
|
//to.fill_fg(area, Color::Reset);
|
||||||
|
//to.fill_bg(area, Nord::bg_lo(true, true));
|
||||||
|
//to.fill_char(area, ' ');
|
||||||
|
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
|
||||||
|
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
|
||||||
|
//for (i, (is_dir, name)) in self.subdirs.iter()
|
||||||
|
//.map(|path|(true, path))
|
||||||
|
//.chain(self.files.iter().map(|path|(false, path)))
|
||||||
|
//.enumerate()
|
||||||
|
//.skip(self.offset)
|
||||||
|
//{
|
||||||
|
//if i >= area.h() as usize - 4 {
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//let t = if is_dir { "" } else { "" };
|
||||||
|
//let line = format!("{t} {}", name.to_string_lossy());
|
||||||
|
//let line = &line[..line.len().min(area.w() as usize - 4)];
|
||||||
|
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
|
||||||
|
//Style::default().green()
|
||||||
|
//} else {
|
||||||
|
//Style::default().white()
|
||||||
|
//}))?;
|
||||||
|
//}
|
||||||
|
//Lozenge(Style::default()).draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,419 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
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`
|
||||||
|
pub struct Sequencer<E: Engine> {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Arc<RwLock<JackClient>>,
|
||||||
|
/// Controls the JACK transport.
|
||||||
|
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||||||
|
/// Global timebase
|
||||||
|
pub clock: Arc<TransportTime>,
|
||||||
|
/// Pool of all phrases available to the sequencer
|
||||||
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
|
/// Phrase editor view
|
||||||
|
pub editor: PhraseEditor<E>,
|
||||||
|
/// Phrase player
|
||||||
|
pub player: PhrasePlayer,
|
||||||
|
/// Which view is focused
|
||||||
|
pub focus_cursor: (usize, usize),
|
||||||
|
/// Whether the currently focused item 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),
|
||||||
|
}
|
||||||
|
/// A MIDI sequence.
|
||||||
|
#[derive(Debug, Clone)] pub struct Phrase {
|
||||||
|
pub uuid: uuid::Uuid,
|
||||||
|
/// Name of phrase
|
||||||
|
pub name: String,
|
||||||
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of phrase in pulses
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in phrase
|
||||||
|
pub notes: PhraseData,
|
||||||
|
/// Whether to loop the phrase or play it once
|
||||||
|
pub loop_on: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
|
/// Identifying color of phrase
|
||||||
|
pub color: ItemColorTriplet,
|
||||||
|
}
|
||||||
|
/// 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> {
|
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> {
|
||||||
|
|
@ -1,4 +1,84 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
/// Stores and displays time-related state.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TransportView<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
state: TransportToolbar,
|
||||||
|
focused: bool,
|
||||||
|
focus: TransportFocus,
|
||||||
|
}
|
||||||
|
/// Which item of the transport toolbar is focused
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum TransportFocus {
|
||||||
|
Bpm,
|
||||||
|
Sync,
|
||||||
|
PlayPause,
|
||||||
|
Clock,
|
||||||
|
Quant,
|
||||||
|
}
|
||||||
|
impl<E: Engine> TransportView<E> {
|
||||||
|
pub fn new (jack: &Arc<RwLock<JackClient>>, clock: Option<&Arc<TransportTime>>) -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
focused: false,
|
||||||
|
focus: TransportFocus::PlayPause,
|
||||||
|
state: TransportToolbar {
|
||||||
|
metronome: false,
|
||||||
|
transport: jack.read().unwrap().transport(),
|
||||||
|
jack: jack.clone(),
|
||||||
|
clock: match clock {
|
||||||
|
Some(clock) => clock.clone(),
|
||||||
|
None => {
|
||||||
|
let timebase = Arc::new(Timebase::default());
|
||||||
|
Arc::new(TransportTime {
|
||||||
|
playing: Some(TransportState::Stopped).into(),
|
||||||
|
quant: 24.into(),
|
||||||
|
sync: (timebase.ppq.get() * 4.).into(),
|
||||||
|
current: Instant::default(),
|
||||||
|
started: None.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn toggle_play (&mut self) -> Usually<()> {
|
||||||
|
let playing = self.clock.playing.read().unwrap().expect("1st sample has not been processed yet");
|
||||||
|
let playing = match playing {
|
||||||
|
TransportState::Stopped => {
|
||||||
|
self.transport.start()?;
|
||||||
|
Some(TransportState::Starting)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
self.transport.stop()?;
|
||||||
|
self.transport.locate(0)?;
|
||||||
|
Some(TransportState::Stopped)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
*self.clock.playing.write().unwrap() = playing;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TransportFocus {
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::PlayPause => Self::Bpm,
|
||||||
|
Self::Bpm => Self::Quant,
|
||||||
|
Self::Quant => Self::Sync,
|
||||||
|
Self::Sync => Self::Clock,
|
||||||
|
Self::Clock => Self::PlayPause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::PlayPause => Self::Clock,
|
||||||
|
Self::Bpm => Self::PlayPause,
|
||||||
|
Self::Quant => Self::Bpm,
|
||||||
|
Self::Sync => Self::Quant,
|
||||||
|
Self::Clock => Self::Sync,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl Content for TransportToolbar<Tui> {
|
impl Content for TransportToolbar<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
Loading…
Add table
Add a link
Reference in a new issue