mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-12 02:56:42 +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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_snd"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tek_api",
|
||||
"tek_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_tui"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
resolver = "2"
|
||||
members = [
|
||||
"crates/tek_core",
|
||||
"crates/tek_snd",
|
||||
"crates/tek_api",
|
||||
"crates/tek_cli",
|
||||
"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::*;
|
||||
|
||||
#[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)]
|
||||
pub enum SequencerCommand {
|
||||
Focus(FocusCommand),
|
||||
|
|
@ -127,17 +64,3 @@ pub enum PhraseEditorCommand {
|
|||
TimeZoomSet(usize),
|
||||
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::jack::{TransportState, Port, MidiIn, MidiOut};
|
||||
pub(crate) use tek_core::midly::{MidiMessage, num::u7};
|
||||
pub(crate) use tek_core::jack::*;
|
||||
pub(crate) use tek_core::midly::{*, live::LiveEvent, num::u7};
|
||||
pub(crate) use std::thread::JoinHandle;
|
||||
pub(crate) use std::fmt::{Debug, Formatter, Error};
|
||||
|
||||
submod! {
|
||||
clock
|
||||
mixer
|
||||
phrase
|
||||
plugin
|
||||
pool
|
||||
sampler
|
||||
sample
|
||||
scene
|
||||
sequencer
|
||||
track
|
||||
transport
|
||||
voice
|
||||
|
||||
api_cmd
|
||||
api_edn
|
||||
}
|
||||
|
||||
/// 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,
|
||||
})
|
||||
}
|
||||
api_jack
|
||||
}
|
||||
|
|
|
|||
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::*;
|
||||
|
||||
/// JACK process callback for sequencer app
|
||||
impl Audio for Sequencer {
|
||||
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
|
||||
}
|
||||
pub enum SequencerCommand {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SequencerTrack {
|
||||
/// Name of track
|
||||
pub name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// Identifying color of track
|
||||
pub color: ItemColor,
|
||||
/// Global timebase
|
||||
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.
|
||||
impl Audio for PhrasePlayer {
|
||||
impl Audio for SequencerTrack {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let has_midi_outputs = self.has_midi_outputs();
|
||||
let has_midi_inputs = self.has_midi_inputs();
|
||||
|
|
@ -38,7 +65,7 @@ impl Audio for PhrasePlayer {
|
|||
}
|
||||
|
||||
/// Methods used primarily by the process callback
|
||||
impl PhrasePlayer {
|
||||
impl SequencerTrack {
|
||||
fn is_rolling (&self) -> bool {
|
||||
*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::*;
|
||||
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 {
|
||||
let times = scope.cycle_times().unwrap();
|
||||
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))
|
||||
)?;
|
||||
}
|
||||
Ok(Arranger::new(
|
||||
Ok(ArrangerView::new(
|
||||
jack,
|
||||
self.transport.then_some(transport),
|
||||
arrangement,
|
||||
|
|
@ -53,3 +53,33 @@ impl ArrangerCli {
|
|||
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<()> {
|
||||
Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{
|
||||
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() {
|
||||
// TODO: sequencer.name = Arc::new(RwLock::new(name.clone()));
|
||||
}
|
||||
|
|
@ -41,8 +31,28 @@ impl SequencerCli {
|
|||
//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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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! {
|
||||
audio
|
||||
//audio
|
||||
color
|
||||
command
|
||||
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;
|
||||
|
||||
submod! {
|
||||
arranger
|
||||
arranger_cmd
|
||||
arranger_snd
|
||||
arranger_tui
|
||||
arranger_tui_bar
|
||||
arranger_tui_cmd
|
||||
arranger_tui_col
|
||||
arranger_tui_hor
|
||||
arranger_tui_ver
|
||||
sequencer
|
||||
sequencer_cmd
|
||||
sequencer_snd
|
||||
sequencer_tui
|
||||
transport
|
||||
transport_cmd
|
||||
transport_snd
|
||||
transport_tui
|
||||
mixer
|
||||
mixer_snd
|
||||
mixer_cmd
|
||||
mixer_tui
|
||||
sampler
|
||||
sampler_snd
|
||||
sampler_cmd
|
||||
plugin
|
||||
plugin_snd
|
||||
plugin_cmd
|
||||
plugin_tui
|
||||
plugin_lv2
|
||||
plugin_lv2_gui
|
||||
plugin_vst2
|
||||
plugin_vst3
|
||||
tui_arranger
|
||||
tui_arranger_bar
|
||||
tui_arranger_cmd
|
||||
tui_arranger_col
|
||||
tui_arranger_hor
|
||||
tui_arranger_ver
|
||||
tui_sequencer
|
||||
tui_sequencer_cmd
|
||||
tui_transport
|
||||
tui_transport_cmd
|
||||
tui_mixer
|
||||
tui_mixer_cmd
|
||||
tui_sampler
|
||||
tui_sampler_cmd
|
||||
tui_plugin
|
||||
tui_plugin_cmd
|
||||
tui_plugin_lv2
|
||||
tui_plugin_lv2_gui
|
||||
tui_plugin_vst2
|
||||
tui_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 }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::*;
|
||||
|
||||
#[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.
|
||||
impl Handle<Tui> for Arranger<Tui> {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
|
|
@ -159,3 +169,103 @@ impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
|
|||
//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 chain: Option<&'a Track<E>>,
|
||||
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::*;
|
||||
|
||||
/// 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> {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
|
|
@ -56,21 +56,3 @@ impl ApplicationHandler for LV2PluginUI {
|
|||
fn lv2_ui_instantiate (kind: &str) {
|
||||
//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>,
|
||||
}
|
||||
|
||||
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.
|
||||
#[macro_export] macro_rules! sample {
|
||||
|
|
@ -412,3 +398,100 @@ impl Iterator for Voice {
|
|||
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 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> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
|
|
@ -1,4 +1,84 @@
|
|||
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> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Loading…
Add table
Add a link
Reference in a new issue