mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 00:36:40 +01:00
flatten workspace into 1 crate
This commit is contained in:
parent
7c4e1e2166
commit
d926422c67
147 changed files with 66 additions and 126 deletions
|
|
@ -1,40 +0,0 @@
|
|||
[package]
|
||||
name = "tek_cli"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek = { path = "../tek" }
|
||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||
|
||||
[[bin]]
|
||||
name = "tek_arranger"
|
||||
path = "src/cli_arranger.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_sequencer"
|
||||
path = "src/cli_sequencer.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_groovebox"
|
||||
path = "src/cli_groovebox.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_transport"
|
||||
path = "src/cli_transport.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_sampler"
|
||||
path = "src/cli_sampler.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_mixer"
|
||||
#path = "src/cli_mixer.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_track"
|
||||
#path = "src/cli_track.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_plugin"
|
||||
#path = "src/cli_plugin.rs"
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
use tek::ArrangerTui;
|
||||
pub fn main () -> Usually<()> { ArrangerCli::parse().run() }
|
||||
|
||||
/// Launches an interactive MIDI arranger.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct ArrangerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
transport: bool,
|
||||
|
||||
/// Number of tracks
|
||||
#[arg(short = 'x', long, default_value_t = 4)]
|
||||
tracks: usize,
|
||||
|
||||
/// Number of scenes
|
||||
#[arg(short, long, default_value_t = 8)]
|
||||
scenes: usize,
|
||||
|
||||
/// MIDI outs to connect each track to.
|
||||
#[arg(short='i', long)]
|
||||
midi_from: Vec<String>,
|
||||
|
||||
/// MIDI ins to connect each track to.
|
||||
#[arg(short='o', long)]
|
||||
midi_to: Vec<String>,
|
||||
}
|
||||
|
||||
impl ArrangerCli {
|
||||
/// Run the arranger TUI from CLI arguments.
|
||||
fn run (&self) -> Usually<()> {
|
||||
let mut client_name = String::from("tek_arranger");
|
||||
if let Some(name) = self.name.as_ref() {
|
||||
client_name = name.clone();
|
||||
}
|
||||
Tui::run(JackClient::new(client_name.as_str())?.activate_with(|jack|{
|
||||
let mut app = ArrangerTui::try_from(jack)?;
|
||||
let jack = jack.read().unwrap();
|
||||
app.color = ItemPalette::random();
|
||||
add_tracks(&jack, &mut app, self)?;
|
||||
add_scenes(&mut app, self.scenes)?;
|
||||
Ok(app)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn add_tracks (jack: &JackClient, app: &mut ArrangerTui, cli: &ArrangerCli) -> Usually<()> {
|
||||
let n = cli.tracks;
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let track = app.track_add(None, Some(
|
||||
track_color_1.mix(track_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
track.width = 8;
|
||||
let name = track.name.read().unwrap();
|
||||
track.player.midi_ins.push(
|
||||
jack.register_port(&format!("{}I", &name), MidiIn::default())?
|
||||
);
|
||||
track.player.midi_outs.push(
|
||||
jack.register_port(&format!("{}O", &name), MidiOut::default())?
|
||||
);
|
||||
}
|
||||
for connection in cli.midi_from.iter() {
|
||||
let mut split = connection.split("=");
|
||||
let number = split.next().unwrap().trim();
|
||||
if let Ok(track) = number.parse::<usize>() {
|
||||
if track < 1 {
|
||||
panic!("Tracks are zero-indexed")
|
||||
}
|
||||
if track > n {
|
||||
panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.")
|
||||
}
|
||||
if let Some(port) = split.next() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(port, &app.tracks[track-1].player.midi_ins[0])?;
|
||||
} else {
|
||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
} else {
|
||||
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||
}
|
||||
} else {
|
||||
panic!("Failed to parse track number: {number}")
|
||||
}
|
||||
}
|
||||
for connection in cli.midi_to.iter() {
|
||||
let mut split = connection.split("=");
|
||||
let number = split.next().unwrap().trim();
|
||||
if let Ok(track) = number.parse::<usize>() {
|
||||
if track < 1 {
|
||||
panic!("Tracks are zero-indexed")
|
||||
}
|
||||
if track > n {
|
||||
panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.")
|
||||
}
|
||||
if let Some(port) = split.next() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(&app.tracks[track-1].player.midi_outs[0], port)?;
|
||||
} else {
|
||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
} else {
|
||||
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
|
||||
}
|
||||
} else {
|
||||
panic!("Failed to parse track number: {number}")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_scenes (app: &mut ArrangerTui, n: usize) -> Usually<()> {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _scene = app.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test] fn verify_arranger_cli () {
|
||||
use clap::CommandFactory;
|
||||
ArrangerCli::command().debug_assert();
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
pub fn main () -> Usually<()> { GrooveboxCli::parse().run() }
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct GrooveboxCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
transport: bool,
|
||||
/// MIDI outs to connect to MIDI input
|
||||
#[arg(short='i', long)]
|
||||
midi_from: Vec<String>,
|
||||
/// MIDI ins to connect from MIDI output
|
||||
#[arg(short='o', long)]
|
||||
midi_to: Vec<String>,
|
||||
/// Audio outs to connect to left input
|
||||
#[arg(short='l', long)]
|
||||
l_from: Vec<String>,
|
||||
/// Audio outs to connect to right input
|
||||
#[arg(short='r', long)]
|
||||
r_from: Vec<String>,
|
||||
/// Audio ins to connect from left output
|
||||
#[arg(short='L', long)]
|
||||
l_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)]
|
||||
r_to: Vec<String>,
|
||||
}
|
||||
impl GrooveboxCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_groovebox")?.activate_with(|jack|{
|
||||
let app = tek::GrooveboxTui::try_from(jack)?;
|
||||
jack.read().unwrap().client().connect_ports(&app.player.midi_outs[0], &app.sampler.midi_in)?;
|
||||
jack.connect_midi_from(&app.player.midi_ins[0], &self.midi_from)?;
|
||||
jack.connect_midi_from(&app.sampler.midi_in, &self.midi_from)?;
|
||||
jack.connect_midi_to(&app.player.midi_outs[0], &self.midi_to)?;
|
||||
jack.connect_audio_from(&app.sampler.audio_ins[0], &self.l_from)?;
|
||||
jack.connect_audio_from(&app.sampler.audio_ins[1], &self.r_from)?;
|
||||
jack.connect_audio_to(&app.sampler.audio_outs[0], &self.l_to)?;
|
||||
jack.connect_audio_to(&app.sampler.audio_outs[1], &self.r_to)?;
|
||||
Ok(app)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn verify_groovebox_cli () {
|
||||
use clap::CommandFactory;
|
||||
GrooveboxCli::command().debug_assert();
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
pub fn main () -> Usually<()> { SamplerCli::parse().run() }
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Path to plugin
|
||||
#[arg(short, long)] path: Option<String>,
|
||||
}
|
||||
impl SamplerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_sampler")?.activate_with(|x|{
|
||||
let sampler = tek::SamplerTui::try_from(x)?;
|
||||
Ok(sampler)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
SequencerCli::parse().run()
|
||||
}
|
||||
|
||||
/// Launches a single interactive MIDI sequencer.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct SequencerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
transport: bool,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='i', long)]
|
||||
midi_from: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='o', long)]
|
||||
midi_to: Vec<String>,
|
||||
}
|
||||
|
||||
impl SequencerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
let name = self.name.as_deref().unwrap_or("tek_sequencer");
|
||||
Tui::run(JackClient::new(name)?.activate_with(|jack|{
|
||||
let mut app = SequencerTui::try_from(jack)?;
|
||||
let jack = jack.read().unwrap();
|
||||
let midi_in = jack.register_port("i", MidiIn::default())?;
|
||||
let midi_out = jack.register_port("o", MidiOut::default())?;
|
||||
connect_from(&jack, &midi_in, &self.midi_from)?;
|
||||
connect_to(&jack, &midi_out, &self.midi_to)?;
|
||||
app.player.midi_ins.push(midi_in);
|
||||
app.player.midi_outs.push(midi_out);
|
||||
Ok(app)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn verify_sequencer_cli () {
|
||||
use clap::CommandFactory;
|
||||
SequencerCli::command().debug_assert();
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_transport")?.activate_with(|jack|{
|
||||
TransportTui::try_from(jack)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#[allow(unused_imports)] use std::sync::Arc;
|
||||
#[allow(unused_imports)] use clap::{self, Parser};
|
||||
#[allow(unused_imports)] use tek::{*, jack::*};
|
||||
|
||||
#[allow(unused)]
|
||||
fn connect_from (jack: &JackClient, input: &Port<MidiIn>, ports: &[String]) -> Usually<()> {
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(port, input)?;
|
||||
} else {
|
||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn connect_to (jack: &JackClient, output: &Port<MidiOut>, ports: &[String]) -> Usually<()> {
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(output, port)?;
|
||||
} else {
|
||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn connect_audio_from (jack: &JackClient, input: &Port<AudioIn>, ports: &[String]) -> Usually<()> {
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(port, input)?;
|
||||
} else {
|
||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn connect_audio_to (jack: &JackClient, output: &Port<AudioOut>, ports: &[String]) -> Usually<()> {
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(output, port)?;
|
||||
} else {
|
||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
MixerCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct MixerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Number of tracks
|
||||
#[arg(short, long)] channels: Option<usize>,
|
||||
}
|
||||
|
||||
impl MixerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_mixer")?.activate_with(|jack|{
|
||||
let mut mixer = Mixer::new(jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"))?;
|
||||
for channel in 0..self.channels.unwrap_or(8) {
|
||||
mixer.track_add(&format!("Track {}", channel + 1), 1)?;
|
||||
}
|
||||
Ok(mixer)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
PluginCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct PluginCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Path to plugin
|
||||
#[arg(short, long)] path: Option<String>,
|
||||
}
|
||||
|
||||
impl PluginCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_plugin")?.activate_with(|jack|{
|
||||
let mut plugin = Plugin::new_lv2(
|
||||
jack,
|
||||
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
|
||||
self.path.as_ref().expect("pass --path /to/lv2/plugin.so")
|
||||
)?;
|
||||
Ok(plugin)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
include!("./lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
SamplerCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Path to plugin
|
||||
#[arg(short, long)] path: Option<String>,
|
||||
}
|
||||
|
||||
impl SamplerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_sampler")?.activate_with(|jack|{
|
||||
let mut plugin = Sampler::new(
|
||||
jack,
|
||||
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
|
||||
None,
|
||||
)?;
|
||||
Ok(plugin)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "tek_edn"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek = { path = "../tek" }
|
||||
clojure-reader = "0.1.0"
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
#[allow(unused_imports)] use tek::{*, jack::*, plugin::*};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use clojure_reader::edn::Edn;
|
||||
//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
||||
|
||||
/// EDN parsing helper.
|
||||
#[macro_export] macro_rules! edn {
|
||||
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
match $edn { $($pat => $expr),* }
|
||||
};
|
||||
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
for $edn in $args {
|
||||
edn!($edn { $($pat => $expr),* })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait FromEdn<C>: Sized {
|
||||
const ID: &'static str;
|
||||
fn from_edn (context: C, expr: &[Edn<'_>]) -> Usually<Self>;
|
||||
}
|
||||
|
||||
/// Implements the [FromEdn] trait.
|
||||
#[macro_export] macro_rules! from_edn {
|
||||
($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => {
|
||||
impl FromEdn<$Context> for $T {
|
||||
const ID: &'static str = $id;
|
||||
fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually<Self> {
|
||||
$body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from_edn!("sampler" => |jack: &Arc<RwLock<JackClient>>, args| -> crate::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.first() {
|
||||
Some(Edn::Symbol("sample")) => {
|
||||
let (midi, sample) = MidiSample::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:?}")
|
||||
});
|
||||
Self::new(jack, &name)
|
||||
});
|
||||
|
||||
type MidiSample = (Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||
|
||||
from_edn!("sample" => |(_jack, dir): (&Arc<RwLock<JackClient>>, &str), args| -> MidiSample {
|
||||
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(crate::Sample {
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
channels: data,
|
||||
rate: None,
|
||||
gain: 1.0
|
||||
}))))
|
||||
});
|
||||
|
||||
from_edn!("plugin/lv2" => |jack: &Arc<RwLock<JackClient>>, args| -> Plugin {
|
||||
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)
|
||||
});
|
||||
|
||||
const SYM_NAME: &str = ":name";
|
||||
const SYM_GAIN: &str = ":gain";
|
||||
const SYM_SAMPLER: &str = "sampler";
|
||||
const SYM_LV2: &str = "lv2";
|
||||
|
||||
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackClient>>, args| -> MixerTrack {
|
||||
let mut _gain = 0.0f64;
|
||||
let mut track = MixerTrack {
|
||||
name: String::new(),
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
devices: vec![],
|
||||
};
|
||||
edn!(edn in args {
|
||||
Edn::Map(map) => {
|
||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
|
||||
track.name = n.to_string();
|
||||
}
|
||||
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
|
||||
_gain = f64::from(*g);
|
||||
}
|
||||
},
|
||||
Edn::List(args) => match args.first() {
|
||||
// Add a sampler device to the track
|
||||
Some(Edn::Symbol(SYM_SAMPLER)) => {
|
||||
track.devices.push(
|
||||
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||
);
|
||||
panic!(
|
||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
||||
&track.name,
|
||||
args.first().unwrap()
|
||||
)
|
||||
},
|
||||
// Add a LV2 plugin to the track.
|
||||
Some(Edn::Symbol(SYM_LV2)) => {
|
||||
track.devices.push(
|
||||
Box::new(Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||
);
|
||||
panic!(
|
||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
||||
&track.name,
|
||||
args.first().unwrap()
|
||||
)
|
||||
},
|
||||
None =>
|
||||
panic!("empty list track {}", &track.name),
|
||||
_ =>
|
||||
panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap())
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
Ok(track)
|
||||
});
|
||||
|
||||
//impl ArrangerScene {
|
||||
|
||||
////TODO
|
||||
////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(ArrangerScene {
|
||||
////name: Arc::new(name.unwrap_or("").to_string().into()),
|
||||
////color: ItemColor::random(),
|
||||
////clips,
|
||||
////})
|
||||
////}
|
||||
//}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "tek_suil"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gtk = "0.18.1"
|
||||
livi = "0.7.4"
|
||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.69.4"
|
||||
|
||||
[dev-dependencies]
|
||||
winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
use std::env::var;
|
||||
use bindgen::{Builder, CargoCallbacks};
|
||||
fn main() {
|
||||
//println!("cargo:rustc-link-lib=lv2");
|
||||
println!("cargo:rustc-link-lib=suil-0");
|
||||
let bindings = Builder::default()
|
||||
.header("wrapper.h")
|
||||
//.clang_arg("-Ilv2/include/lv2")
|
||||
.clang_arg("-Isuil/include/suil")
|
||||
.parse_callbacks(Box::new(CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
let out_path = PathBuf::from(var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
struct SuilX11Wrapper {
|
||||
socket: GtkSocket,
|
||||
plug: GtkPlug,
|
||||
wrapper: SuilWrapper,
|
||||
instance: SuilInstance,
|
||||
idle_iface: LV2UI_Idle_Interface,
|
||||
idle_id: usize,
|
||||
idle_ms: usize,
|
||||
idle_size_req_id: usize,
|
||||
initial_width: usize,
|
||||
initial_height: usize,
|
||||
req_width: usize,
|
||||
req_height: usize,
|
||||
}
|
||||
|
||||
struct SuilX11WrapperClass {
|
||||
parent_class: GtkSocketClass,
|
||||
}
|
||||
|
||||
impl SuilX11Wrapper {
|
||||
fn x_window_is_valid (&self) {
|
||||
let window: GdkWindow = gtk_widget_get_window(self.plug);
|
||||
let root: X11Window = Some(0);
|
||||
let parent: X11Window = Some(0);
|
||||
let children: [X11Window] = None;
|
||||
let child_count: usize = 0;
|
||||
x_query_tree(window.xdisplay, window.xid, root, parent, children, &mut childcount);
|
||||
for i in 0..child_count {
|
||||
if children[i] == self.instance.ui_widget {
|
||||
x_free(children);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
x_free(children);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
use std::ffi::{CString, c_void};
|
||||
|
||||
pub mod bound;
|
||||
#[cfg(test)] mod test;
|
||||
|
||||
pub struct Host(*mut self::bound::SuilHost);
|
||||
|
||||
impl Drop for Host {
|
||||
fn drop (&mut self) {
|
||||
unsafe {
|
||||
bound::suil_host_free(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn new () -> Self {
|
||||
Self(unsafe {
|
||||
let mut argv = [];
|
||||
bound::suil_init(
|
||||
&mut 0,
|
||||
&mut argv as *mut *mut *mut i8,
|
||||
0
|
||||
);
|
||||
bound::suil_host_new(
|
||||
Some(write),
|
||||
Some(index),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
})
|
||||
}
|
||||
fn set_touch_func (&self) {
|
||||
unimplemented!();
|
||||
}
|
||||
fn instance <T> (
|
||||
&self,
|
||||
controller: &mut T,
|
||||
container_type_uri: &str,
|
||||
plugin_uri: &str,
|
||||
ui_uri: &str,
|
||||
ui_type_uri: &str,
|
||||
ui_bundle_path: &str,
|
||||
ui_binary_path: &str,
|
||||
features: &[*const bound::LV2_Feature],
|
||||
) -> Result<Instance, Box<dyn std::error::Error>> {
|
||||
let container_type_uri = CString::new(container_type_uri)?;
|
||||
let plugin_uri = CString::new(plugin_uri)?;
|
||||
let ui_uri = CString::new(ui_uri)?;
|
||||
let ui_type_uri = CString::new(ui_type_uri)?;
|
||||
let ui_bundle_path = CString::new(ui_bundle_path)?;
|
||||
let ui_binary_path = CString::new(ui_binary_path)?;
|
||||
Ok(Instance(unsafe {
|
||||
bound::suil_instance_new(
|
||||
self.0,
|
||||
controller as *mut _ as *mut std::ffi::c_void,
|
||||
container_type_uri.into_raw(),
|
||||
plugin_uri.into_raw(),
|
||||
ui_uri.into_raw(),
|
||||
ui_type_uri.into_raw(),
|
||||
ui_bundle_path.into_raw(),
|
||||
ui_binary_path.into_raw(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn write (
|
||||
_controller: *mut c_void, _: u32, _: u32, _: u32, _: *const c_void
|
||||
) {
|
||||
panic!("write")
|
||||
}
|
||||
|
||||
unsafe extern "C" fn index (
|
||||
_controller: *mut c_void, _: *const i8
|
||||
) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
//unsafe extern "C" fn subscribe (
|
||||
//_: *mut c_void, _: u32, _: u32, _: *const *const bound::LV2_Feature
|
||||
//) -> u32 {
|
||||
//panic!("subscribe");
|
||||
//0
|
||||
//}
|
||||
|
||||
//unsafe extern "C" fn unsubscribe (
|
||||
//_: *mut c_void, _: u32, _: u32, _: *const *const bound::LV2_Feature
|
||||
//) -> u32 {
|
||||
//panic!("unsubscribe");
|
||||
//0
|
||||
//}
|
||||
|
||||
pub struct Instance(*mut self::bound::SuilInstance);
|
||||
|
||||
impl Drop for Instance {
|
||||
fn drop (&mut self) -> () {
|
||||
unsafe {
|
||||
bound::suil_instance_free(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Instance {}
|
||||
|
||||
unsafe impl Sync for Instance {}
|
||||
|
||||
impl Instance {
|
||||
fn get_widget (&self) -> *mut c_void {
|
||||
unsafe { bound::suil_instance_get_widget(self.0) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
use crate::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_lv2_ui () {
|
||||
let mut features = [];
|
||||
std::mem::forget(features);
|
||||
//gtk::init().unwrap();
|
||||
use gtk::prelude::{ApplicationExt, ApplicationExtManual};
|
||||
let app = gtk::Application::builder().application_id("lol.tek").build();
|
||||
app.connect_activate(move |app| {
|
||||
let host = Host::new();
|
||||
let mut plugin = plugin::LV2Plugin::new("file:///home/user/.lv2/Odin2.lv2").unwrap();
|
||||
//let mut plugin = plugin::LV2Plugin::new("file:///home/user/.lv2/ChowKick.lv2").unwrap();
|
||||
for ui in plugin.plugin.raw().uis().unwrap().iter() {
|
||||
println!("{:?}", ui.uri());
|
||||
println!("{:?}", ui.classes());
|
||||
}
|
||||
let handle = plugin.instance.raw_mut().instance_mut().handle();
|
||||
let instance_access = crate::bound::LV2_Feature {
|
||||
URI: std::ffi::CString::new("http://lv2plug.in/ns/ext/instance-access").unwrap().into_raw(),
|
||||
data: handle as *mut _ as *mut std::ffi::c_void
|
||||
};
|
||||
let ui_instance = Arc::new(host.instance(
|
||||
&mut plugin,
|
||||
"http://lv2plug.in/ns/extensions/ui#Gtk3UI",
|
||||
"https://thewavewarden.com/odin2",
|
||||
"https://thewavewarden.com/odin2#ParentUI",
|
||||
"http://lv2plug.in/ns/extensions/ui#X11UI",
|
||||
"/home/user/.lv2/Odin2.lv2/Odin2.so",
|
||||
"/home/user/.lv2/Odin2.lv2/Odin2.so",
|
||||
&features,
|
||||
).unwrap());
|
||||
//let instance = Arc::new(host.instance(
|
||||
//&mut plugin,
|
||||
//"http://lv2plug.in/ns/extensions/ui#Gtk3UI",
|
||||
//"http://tytel.org/helm",
|
||||
//"http://tytel.org/helm#ParentUI",
|
||||
//"http://lv2plug.in/ns/extensions/ui#X11UI",
|
||||
//"/home/user/.lv2/Helm.lv2/helm.so",
|
||||
//"/home/user/.lv2/Helm.lv2/helm.so",
|
||||
//&features,
|
||||
//).unwrap());
|
||||
//let instance = Arc::new(host.instance(
|
||||
//&mut plugin,
|
||||
//"http://lv2plug.in/ns/extensions/ui#Gtk3UI",
|
||||
//"http://github.com/Chowdhury-DSP/ChowKick",
|
||||
//"http://github.com/Chowdhury-DSP/ChowKick:UI",
|
||||
//"http://lv2plug.in/ns/extensions/ui#X11UI",
|
||||
//"/home/user/.lv2/ChowKick.lv2/libChowKick.so",
|
||||
//"/home/user/.lv2/ChowKick.lv2/libChowKick.so",
|
||||
//&features,
|
||||
//).unwrap());
|
||||
//println!("{:?}", instance.get_widget());
|
||||
});
|
||||
app.run();
|
||||
//self::ui::UI::run(&instance).join();
|
||||
}
|
||||
|
||||
mod ui {
|
||||
use std::sync::Arc;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
use ::winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
platform::x11::EventLoopBuilderExtX11
|
||||
};
|
||||
use crate::{Host, Instance};
|
||||
pub struct UI {
|
||||
host: Arc<Instance>,
|
||||
window: Option<Window>
|
||||
}
|
||||
impl UI {
|
||||
pub fn new (host: &Arc<Instance>) -> Self {
|
||||
Self {
|
||||
host: host.clone(),
|
||||
window: None
|
||||
}
|
||||
}
|
||||
pub fn run (host: &Arc<Instance>) -> JoinHandle<()> {
|
||||
let mut ui = Self::new(host);
|
||||
spawn(move||{
|
||||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_loop.run_app(&mut ui).unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ApplicationHandler for UI {
|
||||
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
|
||||
}
|
||||
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.window.as_ref().unwrap().set_visible(false);
|
||||
event_loop.exit();
|
||||
},
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.window.as_ref().unwrap().request_redraw();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod plugin {
|
||||
use std::thread::JoinHandle;
|
||||
use std::sync::Arc;
|
||||
use ::livi::{
|
||||
World,
|
||||
Instance,
|
||||
Plugin as LiviPlugin,
|
||||
Features,
|
||||
FeaturesBuilder,
|
||||
Port,
|
||||
event::LV2AtomSequence,
|
||||
};
|
||||
/// A LV2 plugin.
|
||||
pub struct LV2Plugin {
|
||||
pub world: World,
|
||||
pub instance: Instance,
|
||||
pub plugin: LiviPlugin,
|
||||
pub features: Arc<Features>,
|
||||
pub port_list: Vec<Port>,
|
||||
pub input_buffer: Vec<LV2AtomSequence>,
|
||||
pub ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LV2Plugin {
|
||||
pub fn new (uri: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
// Get 1st plugin at URI
|
||||
let world = World::with_load_bundle(&uri);
|
||||
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
|
||||
let features = world.build_features(features);
|
||||
let mut plugin = None;
|
||||
#[allow(clippy::never_loop)]
|
||||
for p in world.iter_plugins() {
|
||||
plugin = Some(p);
|
||||
break
|
||||
}
|
||||
if plugin.is_none() {
|
||||
return Err(format!("plugin not found: {uri}").into())
|
||||
}
|
||||
let plugin = plugin.unwrap();
|
||||
let err = &format!("init {uri}");
|
||||
|
||||
// Instantiate
|
||||
Ok(Self {
|
||||
world,
|
||||
instance: unsafe {
|
||||
plugin
|
||||
.instantiate(features.clone(), 48000.0)
|
||||
.expect(&err)
|
||||
},
|
||||
port_list: {
|
||||
let mut port_list = vec![];
|
||||
for port in plugin.ports() {
|
||||
port_list.push(port);
|
||||
}
|
||||
port_list
|
||||
},
|
||||
plugin,
|
||||
features,
|
||||
input_buffer: Vec::with_capacity(1024),
|
||||
ui_thread: None
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*===---- stdbool.h - Standard header for booleans -------------------------===
|
||||
*
|
||||
* Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
* See https://llvm.org/LICENSE.txt for license information.
|
||||
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
*
|
||||
*===-----------------------------------------------------------------------===
|
||||
*/
|
||||
|
||||
#ifndef __STDBOOL_H
|
||||
#define __STDBOOL_H
|
||||
|
||||
#define __bool_true_false_are_defined 1
|
||||
|
||||
#if defined(__MVS__) && __has_include_next(<stdbool.h>)
|
||||
#include_next <stdbool.h>
|
||||
#else
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ > 201710L
|
||||
/* FIXME: We should be issuing a deprecation warning here, but cannot yet due
|
||||
* to system headers which include this header file unconditionally.
|
||||
*/
|
||||
#elif !defined(__cplusplus)
|
||||
#define bool _Bool
|
||||
#define true 1
|
||||
#define false 0
|
||||
#elif defined(__GNUC__) && !defined(__STRICT_ANSI__)
|
||||
/* Define _Bool as a GNU extension. */
|
||||
#define _Bool bool
|
||||
#if defined(__cplusplus) && __cplusplus < 201103L
|
||||
/* For C++98, define bool, false, true as a GNU extension. */
|
||||
#define bool bool
|
||||
#define false false
|
||||
#define true true
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif /* __MVS__ */
|
||||
#endif /* __STDBOOL_H */
|
||||
|
|
@ -1,955 +0,0 @@
|
|||
/*===---- stdint.h - Standard header for sized integer types --------------===*\
|
||||
*
|
||||
* Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
* See https://llvm.org/LICENSE.txt for license information.
|
||||
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
*
|
||||
\*===----------------------------------------------------------------------===*/
|
||||
|
||||
#ifndef __CLANG_STDINT_H
|
||||
// AIX system headers need stdint.h to be re-enterable while _STD_TYPES_T
|
||||
// is defined until an inclusion of it without _STD_TYPES_T occurs, in which
|
||||
// case the header guard macro is defined.
|
||||
#if !defined(_AIX) || !defined(_STD_TYPES_T) || !defined(__STDC_HOSTED__)
|
||||
#define __CLANG_STDINT_H
|
||||
#endif
|
||||
|
||||
#if defined(__MVS__) && __has_include_next(<stdint.h>)
|
||||
#include_next <stdint.h>
|
||||
#else
|
||||
|
||||
/* If we're hosted, fall back to the system's stdint.h, which might have
|
||||
* additional definitions.
|
||||
*/
|
||||
#if __STDC_HOSTED__ && __has_include_next(<stdint.h>)
|
||||
|
||||
// C99 7.18.3 Limits of other integer types
|
||||
//
|
||||
// Footnote 219, 220: C++ implementations should define these macros only when
|
||||
// __STDC_LIMIT_MACROS is defined before <stdint.h> is included.
|
||||
//
|
||||
// Footnote 222: C++ implementations should define these macros only when
|
||||
// __STDC_CONSTANT_MACROS is defined before <stdint.h> is included.
|
||||
//
|
||||
// C++11 [cstdint.syn]p2:
|
||||
//
|
||||
// The macros defined by <cstdint> are provided unconditionally. In particular,
|
||||
// the symbols __STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS (mentioned in
|
||||
// footnotes 219, 220, and 222 in the C standard) play no role in C++.
|
||||
//
|
||||
// C11 removed the problematic footnotes.
|
||||
//
|
||||
// Work around this inconsistency by always defining those macros in C++ mode,
|
||||
// so that a C library implementation which follows the C99 standard can be
|
||||
// used in C++.
|
||||
# ifdef __cplusplus
|
||||
# if !defined(__STDC_LIMIT_MACROS)
|
||||
# define __STDC_LIMIT_MACROS
|
||||
# define __STDC_LIMIT_MACROS_DEFINED_BY_CLANG
|
||||
# endif
|
||||
# if !defined(__STDC_CONSTANT_MACROS)
|
||||
# define __STDC_CONSTANT_MACROS
|
||||
# define __STDC_CONSTANT_MACROS_DEFINED_BY_CLANG
|
||||
# endif
|
||||
# endif
|
||||
|
||||
# include_next <stdint.h>
|
||||
|
||||
# ifdef __STDC_LIMIT_MACROS_DEFINED_BY_CLANG
|
||||
# undef __STDC_LIMIT_MACROS
|
||||
# undef __STDC_LIMIT_MACROS_DEFINED_BY_CLANG
|
||||
# endif
|
||||
# ifdef __STDC_CONSTANT_MACROS_DEFINED_BY_CLANG
|
||||
# undef __STDC_CONSTANT_MACROS
|
||||
# undef __STDC_CONSTANT_MACROS_DEFINED_BY_CLANG
|
||||
# endif
|
||||
|
||||
#else
|
||||
|
||||
/* C99 7.18.1.1 Exact-width integer types.
|
||||
* C99 7.18.1.2 Minimum-width integer types.
|
||||
* C99 7.18.1.3 Fastest minimum-width integer types.
|
||||
*
|
||||
* The standard requires that exact-width type be defined for 8-, 16-, 32-, and
|
||||
* 64-bit types if they are implemented. Other exact width types are optional.
|
||||
* This implementation defines an exact-width types for every integer width
|
||||
* that is represented in the standard integer types.
|
||||
*
|
||||
* The standard also requires minimum-width types be defined for 8-, 16-, 32-,
|
||||
* and 64-bit widths regardless of whether there are corresponding exact-width
|
||||
* types.
|
||||
*
|
||||
* To accommodate targets that are missing types that are exactly 8, 16, 32, or
|
||||
* 64 bits wide, this implementation takes an approach of cascading
|
||||
* redefinitions, redefining __int_leastN_t to successively smaller exact-width
|
||||
* types. It is therefore important that the types are defined in order of
|
||||
* descending widths.
|
||||
*
|
||||
* We currently assume that the minimum-width types and the fastest
|
||||
* minimum-width types are the same. This is allowed by the standard, but is
|
||||
* suboptimal.
|
||||
*
|
||||
* In violation of the standard, some targets do not implement a type that is
|
||||
* wide enough to represent all of the required widths (8-, 16-, 32-, 64-bit).
|
||||
* To accommodate these targets, a required minimum-width type is only
|
||||
* defined if there exists an exact-width type of equal or greater width.
|
||||
*/
|
||||
|
||||
#ifdef __INT64_TYPE__
|
||||
# ifndef __int8_t_defined /* glibc sys/types.h also defines int64_t*/
|
||||
typedef __INT64_TYPE__ int64_t;
|
||||
# endif /* __int8_t_defined */
|
||||
typedef __UINT64_TYPE__ uint64_t;
|
||||
# undef __int_least64_t
|
||||
# define __int_least64_t int64_t
|
||||
# undef __uint_least64_t
|
||||
# define __uint_least64_t uint64_t
|
||||
# undef __int_least32_t
|
||||
# define __int_least32_t int64_t
|
||||
# undef __uint_least32_t
|
||||
# define __uint_least32_t uint64_t
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int64_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint64_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int64_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint64_t
|
||||
#endif /* __INT64_TYPE__ */
|
||||
|
||||
#ifdef __int_least64_t
|
||||
typedef __int_least64_t int_least64_t;
|
||||
typedef __uint_least64_t uint_least64_t;
|
||||
typedef __int_least64_t int_fast64_t;
|
||||
typedef __uint_least64_t uint_fast64_t;
|
||||
#endif /* __int_least64_t */
|
||||
|
||||
#ifdef __INT56_TYPE__
|
||||
typedef __INT56_TYPE__ int56_t;
|
||||
typedef __UINT56_TYPE__ uint56_t;
|
||||
typedef int56_t int_least56_t;
|
||||
typedef uint56_t uint_least56_t;
|
||||
typedef int56_t int_fast56_t;
|
||||
typedef uint56_t uint_fast56_t;
|
||||
# undef __int_least32_t
|
||||
# define __int_least32_t int56_t
|
||||
# undef __uint_least32_t
|
||||
# define __uint_least32_t uint56_t
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int56_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint56_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int56_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint56_t
|
||||
#endif /* __INT56_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT48_TYPE__
|
||||
typedef __INT48_TYPE__ int48_t;
|
||||
typedef __UINT48_TYPE__ uint48_t;
|
||||
typedef int48_t int_least48_t;
|
||||
typedef uint48_t uint_least48_t;
|
||||
typedef int48_t int_fast48_t;
|
||||
typedef uint48_t uint_fast48_t;
|
||||
# undef __int_least32_t
|
||||
# define __int_least32_t int48_t
|
||||
# undef __uint_least32_t
|
||||
# define __uint_least32_t uint48_t
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int48_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint48_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int48_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint48_t
|
||||
#endif /* __INT48_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT40_TYPE__
|
||||
typedef __INT40_TYPE__ int40_t;
|
||||
typedef __UINT40_TYPE__ uint40_t;
|
||||
typedef int40_t int_least40_t;
|
||||
typedef uint40_t uint_least40_t;
|
||||
typedef int40_t int_fast40_t;
|
||||
typedef uint40_t uint_fast40_t;
|
||||
# undef __int_least32_t
|
||||
# define __int_least32_t int40_t
|
||||
# undef __uint_least32_t
|
||||
# define __uint_least32_t uint40_t
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int40_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint40_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int40_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint40_t
|
||||
#endif /* __INT40_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT32_TYPE__
|
||||
|
||||
# ifndef __int8_t_defined /* glibc sys/types.h also defines int32_t*/
|
||||
typedef __INT32_TYPE__ int32_t;
|
||||
# endif /* __int8_t_defined */
|
||||
|
||||
# ifndef __uint32_t_defined /* more glibc compatibility */
|
||||
# define __uint32_t_defined
|
||||
typedef __UINT32_TYPE__ uint32_t;
|
||||
# endif /* __uint32_t_defined */
|
||||
|
||||
# undef __int_least32_t
|
||||
# define __int_least32_t int32_t
|
||||
# undef __uint_least32_t
|
||||
# define __uint_least32_t uint32_t
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int32_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint32_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int32_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint32_t
|
||||
#endif /* __INT32_TYPE__ */
|
||||
|
||||
#ifdef __int_least32_t
|
||||
typedef __int_least32_t int_least32_t;
|
||||
typedef __uint_least32_t uint_least32_t;
|
||||
typedef __int_least32_t int_fast32_t;
|
||||
typedef __uint_least32_t uint_fast32_t;
|
||||
#endif /* __int_least32_t */
|
||||
|
||||
#ifdef __INT24_TYPE__
|
||||
typedef __INT24_TYPE__ int24_t;
|
||||
typedef __UINT24_TYPE__ uint24_t;
|
||||
typedef int24_t int_least24_t;
|
||||
typedef uint24_t uint_least24_t;
|
||||
typedef int24_t int_fast24_t;
|
||||
typedef uint24_t uint_fast24_t;
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int24_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint24_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int24_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint24_t
|
||||
#endif /* __INT24_TYPE__ */
|
||||
|
||||
#ifdef __INT16_TYPE__
|
||||
#ifndef __int8_t_defined /* glibc sys/types.h also defines int16_t*/
|
||||
typedef __INT16_TYPE__ int16_t;
|
||||
#endif /* __int8_t_defined */
|
||||
typedef __UINT16_TYPE__ uint16_t;
|
||||
# undef __int_least16_t
|
||||
# define __int_least16_t int16_t
|
||||
# undef __uint_least16_t
|
||||
# define __uint_least16_t uint16_t
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int16_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint16_t
|
||||
#endif /* __INT16_TYPE__ */
|
||||
|
||||
#ifdef __int_least16_t
|
||||
typedef __int_least16_t int_least16_t;
|
||||
typedef __uint_least16_t uint_least16_t;
|
||||
typedef __int_least16_t int_fast16_t;
|
||||
typedef __uint_least16_t uint_fast16_t;
|
||||
#endif /* __int_least16_t */
|
||||
|
||||
|
||||
#ifdef __INT8_TYPE__
|
||||
#ifndef __int8_t_defined /* glibc sys/types.h also defines int8_t*/
|
||||
typedef __INT8_TYPE__ int8_t;
|
||||
#endif /* __int8_t_defined */
|
||||
typedef __UINT8_TYPE__ uint8_t;
|
||||
# undef __int_least8_t
|
||||
# define __int_least8_t int8_t
|
||||
# undef __uint_least8_t
|
||||
# define __uint_least8_t uint8_t
|
||||
#endif /* __INT8_TYPE__ */
|
||||
|
||||
#ifdef __int_least8_t
|
||||
typedef __int_least8_t int_least8_t;
|
||||
typedef __uint_least8_t uint_least8_t;
|
||||
typedef __int_least8_t int_fast8_t;
|
||||
typedef __uint_least8_t uint_fast8_t;
|
||||
#endif /* __int_least8_t */
|
||||
|
||||
/* prevent glibc sys/types.h from defining conflicting types */
|
||||
#ifndef __int8_t_defined
|
||||
# define __int8_t_defined
|
||||
#endif /* __int8_t_defined */
|
||||
|
||||
/* C99 7.18.1.4 Integer types capable of holding object pointers.
|
||||
*/
|
||||
#define __stdint_join3(a,b,c) a ## b ## c
|
||||
|
||||
#ifndef _INTPTR_T
|
||||
#ifndef __intptr_t_defined
|
||||
typedef __INTPTR_TYPE__ intptr_t;
|
||||
#define __intptr_t_defined
|
||||
#define _INTPTR_T
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef _UINTPTR_T
|
||||
typedef __UINTPTR_TYPE__ uintptr_t;
|
||||
#define _UINTPTR_T
|
||||
#endif
|
||||
|
||||
/* C99 7.18.1.5 Greatest-width integer types.
|
||||
*/
|
||||
typedef __INTMAX_TYPE__ intmax_t;
|
||||
typedef __UINTMAX_TYPE__ uintmax_t;
|
||||
|
||||
/* C99 7.18.4 Macros for minimum-width integer constants.
|
||||
*
|
||||
* The standard requires that integer constant macros be defined for all the
|
||||
* minimum-width types defined above. As 8-, 16-, 32-, and 64-bit minimum-width
|
||||
* types are required, the corresponding integer constant macros are defined
|
||||
* here. This implementation also defines minimum-width types for every other
|
||||
* integer width that the target implements, so corresponding macros are
|
||||
* defined below, too.
|
||||
*
|
||||
* These macros are defined using the same successive-shrinking approach as
|
||||
* the type definitions above. It is likewise important that macros are defined
|
||||
* in order of decending width.
|
||||
*
|
||||
* Note that C++ should not check __STDC_CONSTANT_MACROS here, contrary to the
|
||||
* claims of the C standard (see C++ 18.3.1p2, [cstdint.syn]).
|
||||
*/
|
||||
|
||||
#define __int_c_join(a, b) a ## b
|
||||
#define __int_c(v, suffix) __int_c_join(v, suffix)
|
||||
#define __uint_c(v, suffix) __int_c_join(v##U, suffix)
|
||||
|
||||
|
||||
#ifdef __INT64_TYPE__
|
||||
# undef __int64_c_suffix
|
||||
# undef __int32_c_suffix
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT64_C_SUFFIX__
|
||||
# define __int64_c_suffix __INT64_C_SUFFIX__
|
||||
# define __int32_c_suffix __INT64_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT64_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT64_C_SUFFIX__
|
||||
# endif /* __INT64_C_SUFFIX__ */
|
||||
#endif /* __INT64_TYPE__ */
|
||||
|
||||
#ifdef __int_least64_t
|
||||
# ifdef __int64_c_suffix
|
||||
# define INT64_C(v) __int_c(v, __int64_c_suffix)
|
||||
# define UINT64_C(v) __uint_c(v, __int64_c_suffix)
|
||||
# else
|
||||
# define INT64_C(v) v
|
||||
# define UINT64_C(v) v ## U
|
||||
# endif /* __int64_c_suffix */
|
||||
#endif /* __int_least64_t */
|
||||
|
||||
|
||||
#ifdef __INT56_TYPE__
|
||||
# undef __int32_c_suffix
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT56_C_SUFFIX__
|
||||
# define INT56_C(v) __int_c(v, __INT56_C_SUFFIX__)
|
||||
# define UINT56_C(v) __uint_c(v, __INT56_C_SUFFIX__)
|
||||
# define __int32_c_suffix __INT56_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT56_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT56_C_SUFFIX__
|
||||
# else
|
||||
# define INT56_C(v) v
|
||||
# define UINT56_C(v) v ## U
|
||||
# endif /* __INT56_C_SUFFIX__ */
|
||||
#endif /* __INT56_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT48_TYPE__
|
||||
# undef __int32_c_suffix
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT48_C_SUFFIX__
|
||||
# define INT48_C(v) __int_c(v, __INT48_C_SUFFIX__)
|
||||
# define UINT48_C(v) __uint_c(v, __INT48_C_SUFFIX__)
|
||||
# define __int32_c_suffix __INT48_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT48_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT48_C_SUFFIX__
|
||||
# else
|
||||
# define INT48_C(v) v
|
||||
# define UINT48_C(v) v ## U
|
||||
# endif /* __INT48_C_SUFFIX__ */
|
||||
#endif /* __INT48_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT40_TYPE__
|
||||
# undef __int32_c_suffix
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT40_C_SUFFIX__
|
||||
# define INT40_C(v) __int_c(v, __INT40_C_SUFFIX__)
|
||||
# define UINT40_C(v) __uint_c(v, __INT40_C_SUFFIX__)
|
||||
# define __int32_c_suffix __INT40_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT40_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT40_C_SUFFIX__
|
||||
# else
|
||||
# define INT40_C(v) v
|
||||
# define UINT40_C(v) v ## U
|
||||
# endif /* __INT40_C_SUFFIX__ */
|
||||
#endif /* __INT40_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT32_TYPE__
|
||||
# undef __int32_c_suffix
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT32_C_SUFFIX__
|
||||
# define __int32_c_suffix __INT32_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT32_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT32_C_SUFFIX__
|
||||
# endif /* __INT32_C_SUFFIX__ */
|
||||
#endif /* __INT32_TYPE__ */
|
||||
|
||||
#ifdef __int_least32_t
|
||||
# ifdef __int32_c_suffix
|
||||
# define INT32_C(v) __int_c(v, __int32_c_suffix)
|
||||
# define UINT32_C(v) __uint_c(v, __int32_c_suffix)
|
||||
# else
|
||||
# define INT32_C(v) v
|
||||
# define UINT32_C(v) v ## U
|
||||
# endif /* __int32_c_suffix */
|
||||
#endif /* __int_least32_t */
|
||||
|
||||
|
||||
#ifdef __INT24_TYPE__
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT24_C_SUFFIX__
|
||||
# define INT24_C(v) __int_c(v, __INT24_C_SUFFIX__)
|
||||
# define UINT24_C(v) __uint_c(v, __INT24_C_SUFFIX__)
|
||||
# define __int16_c_suffix __INT24_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT24_C_SUFFIX__
|
||||
# else
|
||||
# define INT24_C(v) v
|
||||
# define UINT24_C(v) v ## U
|
||||
# endif /* __INT24_C_SUFFIX__ */
|
||||
#endif /* __INT24_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT16_TYPE__
|
||||
# undef __int16_c_suffix
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT16_C_SUFFIX__
|
||||
# define __int16_c_suffix __INT16_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT16_C_SUFFIX__
|
||||
# endif /* __INT16_C_SUFFIX__ */
|
||||
#endif /* __INT16_TYPE__ */
|
||||
|
||||
#ifdef __int_least16_t
|
||||
# ifdef __int16_c_suffix
|
||||
# define INT16_C(v) __int_c(v, __int16_c_suffix)
|
||||
# define UINT16_C(v) __uint_c(v, __int16_c_suffix)
|
||||
# else
|
||||
# define INT16_C(v) v
|
||||
# define UINT16_C(v) v ## U
|
||||
# endif /* __int16_c_suffix */
|
||||
#endif /* __int_least16_t */
|
||||
|
||||
|
||||
#ifdef __INT8_TYPE__
|
||||
# undef __int8_c_suffix
|
||||
# ifdef __INT8_C_SUFFIX__
|
||||
# define __int8_c_suffix __INT8_C_SUFFIX__
|
||||
# endif /* __INT8_C_SUFFIX__ */
|
||||
#endif /* __INT8_TYPE__ */
|
||||
|
||||
#ifdef __int_least8_t
|
||||
# ifdef __int8_c_suffix
|
||||
# define INT8_C(v) __int_c(v, __int8_c_suffix)
|
||||
# define UINT8_C(v) __uint_c(v, __int8_c_suffix)
|
||||
# else
|
||||
# define INT8_C(v) v
|
||||
# define UINT8_C(v) v ## U
|
||||
# endif /* __int8_c_suffix */
|
||||
#endif /* __int_least8_t */
|
||||
|
||||
|
||||
/* C99 7.18.2.1 Limits of exact-width integer types.
|
||||
* C99 7.18.2.2 Limits of minimum-width integer types.
|
||||
* C99 7.18.2.3 Limits of fastest minimum-width integer types.
|
||||
*
|
||||
* The presence of limit macros are completely optional in C99. This
|
||||
* implementation defines limits for all of the types (exact- and
|
||||
* minimum-width) that it defines above, using the limits of the minimum-width
|
||||
* type for any types that do not have exact-width representations.
|
||||
*
|
||||
* As in the type definitions, this section takes an approach of
|
||||
* successive-shrinking to determine which limits to use for the standard (8,
|
||||
* 16, 32, 64) bit widths when they don't have exact representations. It is
|
||||
* therefore important that the definitions be kept in order of decending
|
||||
* widths.
|
||||
*
|
||||
* Note that C++ should not check __STDC_LIMIT_MACROS here, contrary to the
|
||||
* claims of the C standard (see C++ 18.3.1p2, [cstdint.syn]).
|
||||
*/
|
||||
|
||||
#ifdef __INT64_TYPE__
|
||||
# define INT64_MAX INT64_C( 9223372036854775807)
|
||||
# define INT64_MIN (-INT64_C( 9223372036854775807)-1)
|
||||
# define UINT64_MAX UINT64_C(18446744073709551615)
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT64_WIDTH 64
|
||||
# define INT64_WIDTH UINT64_WIDTH
|
||||
|
||||
# define __UINT_LEAST64_WIDTH UINT64_WIDTH
|
||||
# undef __UINT_LEAST32_WIDTH
|
||||
# define __UINT_LEAST32_WIDTH UINT64_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT64_WIDTH
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT64_MAX
|
||||
#endif /* __STDC_VERSION__ */
|
||||
|
||||
# define __INT_LEAST64_MIN INT64_MIN
|
||||
# define __INT_LEAST64_MAX INT64_MAX
|
||||
# define __UINT_LEAST64_MAX UINT64_MAX
|
||||
# undef __INT_LEAST32_MIN
|
||||
# define __INT_LEAST32_MIN INT64_MIN
|
||||
# undef __INT_LEAST32_MAX
|
||||
# define __INT_LEAST32_MAX INT64_MAX
|
||||
# undef __UINT_LEAST32_MAX
|
||||
# define __UINT_LEAST32_MAX UINT64_MAX
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT64_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT64_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT64_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT64_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT64_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT64_MAX
|
||||
#endif /* __INT64_TYPE__ */
|
||||
|
||||
#ifdef __INT_LEAST64_MIN
|
||||
# define INT_LEAST64_MIN __INT_LEAST64_MIN
|
||||
# define INT_LEAST64_MAX __INT_LEAST64_MAX
|
||||
# define UINT_LEAST64_MAX __UINT_LEAST64_MAX
|
||||
# define INT_FAST64_MIN __INT_LEAST64_MIN
|
||||
# define INT_FAST64_MAX __INT_LEAST64_MAX
|
||||
# define UINT_FAST64_MAX __UINT_LEAST64_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT_LEAST64_WIDTH __UINT_LEAST64_WIDTH
|
||||
# define INT_LEAST64_WIDTH UINT_LEAST64_WIDTH
|
||||
# define UINT_FAST64_WIDTH __UINT_LEAST64_WIDTH
|
||||
# define INT_FAST64_WIDTH UINT_FAST64_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT_LEAST64_MIN */
|
||||
|
||||
|
||||
#ifdef __INT56_TYPE__
|
||||
# define INT56_MAX INT56_C(36028797018963967)
|
||||
# define INT56_MIN (-INT56_C(36028797018963967)-1)
|
||||
# define UINT56_MAX UINT56_C(72057594037927935)
|
||||
# define INT_LEAST56_MIN INT56_MIN
|
||||
# define INT_LEAST56_MAX INT56_MAX
|
||||
# define UINT_LEAST56_MAX UINT56_MAX
|
||||
# define INT_FAST56_MIN INT56_MIN
|
||||
# define INT_FAST56_MAX INT56_MAX
|
||||
# define UINT_FAST56_MAX UINT56_MAX
|
||||
|
||||
# undef __INT_LEAST32_MIN
|
||||
# define __INT_LEAST32_MIN INT56_MIN
|
||||
# undef __INT_LEAST32_MAX
|
||||
# define __INT_LEAST32_MAX INT56_MAX
|
||||
# undef __UINT_LEAST32_MAX
|
||||
# define __UINT_LEAST32_MAX UINT56_MAX
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT56_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT56_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT56_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT56_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT56_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT56_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT56_WIDTH 56
|
||||
# define INT56_WIDTH UINT56_WIDTH
|
||||
# define UINT_LEAST56_WIDTH UINT56_WIDTH
|
||||
# define INT_LEAST56_WIDTH UINT_LEAST56_WIDTH
|
||||
# define UINT_FAST56_WIDTH UINT56_WIDTH
|
||||
# define INT_FAST56_WIDTH UINT_FAST56_WIDTH
|
||||
# undef __UINT_LEAST32_WIDTH
|
||||
# define __UINT_LEAST32_WIDTH UINT56_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT56_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT56_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT56_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT48_TYPE__
|
||||
# define INT48_MAX INT48_C(140737488355327)
|
||||
# define INT48_MIN (-INT48_C(140737488355327)-1)
|
||||
# define UINT48_MAX UINT48_C(281474976710655)
|
||||
# define INT_LEAST48_MIN INT48_MIN
|
||||
# define INT_LEAST48_MAX INT48_MAX
|
||||
# define UINT_LEAST48_MAX UINT48_MAX
|
||||
# define INT_FAST48_MIN INT48_MIN
|
||||
# define INT_FAST48_MAX INT48_MAX
|
||||
# define UINT_FAST48_MAX UINT48_MAX
|
||||
|
||||
# undef __INT_LEAST32_MIN
|
||||
# define __INT_LEAST32_MIN INT48_MIN
|
||||
# undef __INT_LEAST32_MAX
|
||||
# define __INT_LEAST32_MAX INT48_MAX
|
||||
# undef __UINT_LEAST32_MAX
|
||||
# define __UINT_LEAST32_MAX UINT48_MAX
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT48_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT48_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT48_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT48_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT48_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT48_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
#define UINT48_WIDTH 48
|
||||
#define INT48_WIDTH UINT48_WIDTH
|
||||
#define UINT_LEAST48_WIDTH UINT48_WIDTH
|
||||
#define INT_LEAST48_WIDTH UINT_LEAST48_WIDTH
|
||||
#define UINT_FAST48_WIDTH UINT48_WIDTH
|
||||
#define INT_FAST48_WIDTH UINT_FAST48_WIDTH
|
||||
#undef __UINT_LEAST32_WIDTH
|
||||
#define __UINT_LEAST32_WIDTH UINT48_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
#define __UINT_LEAST16_WIDTH UINT48_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
#define __UINT_LEAST8_WIDTH UINT48_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT48_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT40_TYPE__
|
||||
# define INT40_MAX INT40_C(549755813887)
|
||||
# define INT40_MIN (-INT40_C(549755813887)-1)
|
||||
# define UINT40_MAX UINT40_C(1099511627775)
|
||||
# define INT_LEAST40_MIN INT40_MIN
|
||||
# define INT_LEAST40_MAX INT40_MAX
|
||||
# define UINT_LEAST40_MAX UINT40_MAX
|
||||
# define INT_FAST40_MIN INT40_MIN
|
||||
# define INT_FAST40_MAX INT40_MAX
|
||||
# define UINT_FAST40_MAX UINT40_MAX
|
||||
|
||||
# undef __INT_LEAST32_MIN
|
||||
# define __INT_LEAST32_MIN INT40_MIN
|
||||
# undef __INT_LEAST32_MAX
|
||||
# define __INT_LEAST32_MAX INT40_MAX
|
||||
# undef __UINT_LEAST32_MAX
|
||||
# define __UINT_LEAST32_MAX UINT40_MAX
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT40_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT40_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT40_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT40_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT40_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT40_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT40_WIDTH 40
|
||||
# define INT40_WIDTH UINT40_WIDTH
|
||||
# define UINT_LEAST40_WIDTH UINT40_WIDTH
|
||||
# define INT_LEAST40_WIDTH UINT_LEAST40_WIDTH
|
||||
# define UINT_FAST40_WIDTH UINT40_WIDTH
|
||||
# define INT_FAST40_WIDTH UINT_FAST40_WIDTH
|
||||
# undef __UINT_LEAST32_WIDTH
|
||||
# define __UINT_LEAST32_WIDTH UINT40_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT40_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT40_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT40_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT32_TYPE__
|
||||
# define INT32_MAX INT32_C(2147483647)
|
||||
# define INT32_MIN (-INT32_C(2147483647)-1)
|
||||
# define UINT32_MAX UINT32_C(4294967295)
|
||||
|
||||
# undef __INT_LEAST32_MIN
|
||||
# define __INT_LEAST32_MIN INT32_MIN
|
||||
# undef __INT_LEAST32_MAX
|
||||
# define __INT_LEAST32_MAX INT32_MAX
|
||||
# undef __UINT_LEAST32_MAX
|
||||
# define __UINT_LEAST32_MAX UINT32_MAX
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT32_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT32_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT32_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT32_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT32_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT32_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT32_WIDTH 32
|
||||
# define INT32_WIDTH UINT32_WIDTH
|
||||
# undef __UINT_LEAST32_WIDTH
|
||||
# define __UINT_LEAST32_WIDTH UINT32_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT32_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT32_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT32_TYPE__ */
|
||||
|
||||
#ifdef __INT_LEAST32_MIN
|
||||
# define INT_LEAST32_MIN __INT_LEAST32_MIN
|
||||
# define INT_LEAST32_MAX __INT_LEAST32_MAX
|
||||
# define UINT_LEAST32_MAX __UINT_LEAST32_MAX
|
||||
# define INT_FAST32_MIN __INT_LEAST32_MIN
|
||||
# define INT_FAST32_MAX __INT_LEAST32_MAX
|
||||
# define UINT_FAST32_MAX __UINT_LEAST32_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT_LEAST32_WIDTH __UINT_LEAST32_WIDTH
|
||||
# define INT_LEAST32_WIDTH UINT_LEAST32_WIDTH
|
||||
# define UINT_FAST32_WIDTH __UINT_LEAST32_WIDTH
|
||||
# define INT_FAST32_WIDTH UINT_FAST32_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT_LEAST32_MIN */
|
||||
|
||||
|
||||
#ifdef __INT24_TYPE__
|
||||
# define INT24_MAX INT24_C(8388607)
|
||||
# define INT24_MIN (-INT24_C(8388607)-1)
|
||||
# define UINT24_MAX UINT24_C(16777215)
|
||||
# define INT_LEAST24_MIN INT24_MIN
|
||||
# define INT_LEAST24_MAX INT24_MAX
|
||||
# define UINT_LEAST24_MAX UINT24_MAX
|
||||
# define INT_FAST24_MIN INT24_MIN
|
||||
# define INT_FAST24_MAX INT24_MAX
|
||||
# define UINT_FAST24_MAX UINT24_MAX
|
||||
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT24_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT24_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT24_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT24_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT24_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT24_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT24_WIDTH 24
|
||||
# define INT24_WIDTH UINT24_WIDTH
|
||||
# define UINT_LEAST24_WIDTH UINT24_WIDTH
|
||||
# define INT_LEAST24_WIDTH UINT_LEAST24_WIDTH
|
||||
# define UINT_FAST24_WIDTH UINT24_WIDTH
|
||||
# define INT_FAST24_WIDTH UINT_FAST24_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT24_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT24_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT24_TYPE__ */
|
||||
|
||||
|
||||
#ifdef __INT16_TYPE__
|
||||
#define INT16_MAX INT16_C(32767)
|
||||
#define INT16_MIN (-INT16_C(32767)-1)
|
||||
#define UINT16_MAX UINT16_C(65535)
|
||||
|
||||
# undef __INT_LEAST16_MIN
|
||||
# define __INT_LEAST16_MIN INT16_MIN
|
||||
# undef __INT_LEAST16_MAX
|
||||
# define __INT_LEAST16_MAX INT16_MAX
|
||||
# undef __UINT_LEAST16_MAX
|
||||
# define __UINT_LEAST16_MAX UINT16_MAX
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT16_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT16_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT16_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT16_WIDTH 16
|
||||
# define INT16_WIDTH UINT16_WIDTH
|
||||
# undef __UINT_LEAST16_WIDTH
|
||||
# define __UINT_LEAST16_WIDTH UINT16_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT16_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT16_TYPE__ */
|
||||
|
||||
#ifdef __INT_LEAST16_MIN
|
||||
# define INT_LEAST16_MIN __INT_LEAST16_MIN
|
||||
# define INT_LEAST16_MAX __INT_LEAST16_MAX
|
||||
# define UINT_LEAST16_MAX __UINT_LEAST16_MAX
|
||||
# define INT_FAST16_MIN __INT_LEAST16_MIN
|
||||
# define INT_FAST16_MAX __INT_LEAST16_MAX
|
||||
# define UINT_FAST16_MAX __UINT_LEAST16_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT_LEAST16_WIDTH __UINT_LEAST16_WIDTH
|
||||
# define INT_LEAST16_WIDTH UINT_LEAST16_WIDTH
|
||||
# define UINT_FAST16_WIDTH __UINT_LEAST16_WIDTH
|
||||
# define INT_FAST16_WIDTH UINT_FAST16_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT_LEAST16_MIN */
|
||||
|
||||
|
||||
#ifdef __INT8_TYPE__
|
||||
# define INT8_MAX INT8_C(127)
|
||||
# define INT8_MIN (-INT8_C(127)-1)
|
||||
# define UINT8_MAX UINT8_C(255)
|
||||
|
||||
# undef __INT_LEAST8_MIN
|
||||
# define __INT_LEAST8_MIN INT8_MIN
|
||||
# undef __INT_LEAST8_MAX
|
||||
# define __INT_LEAST8_MAX INT8_MAX
|
||||
# undef __UINT_LEAST8_MAX
|
||||
# define __UINT_LEAST8_MAX UINT8_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT8_WIDTH 8
|
||||
# define INT8_WIDTH UINT8_WIDTH
|
||||
# undef __UINT_LEAST8_WIDTH
|
||||
# define __UINT_LEAST8_WIDTH UINT8_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT8_TYPE__ */
|
||||
|
||||
#ifdef __INT_LEAST8_MIN
|
||||
# define INT_LEAST8_MIN __INT_LEAST8_MIN
|
||||
# define INT_LEAST8_MAX __INT_LEAST8_MAX
|
||||
# define UINT_LEAST8_MAX __UINT_LEAST8_MAX
|
||||
# define INT_FAST8_MIN __INT_LEAST8_MIN
|
||||
# define INT_FAST8_MAX __INT_LEAST8_MAX
|
||||
# define UINT_FAST8_MAX __UINT_LEAST8_MAX
|
||||
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
# define UINT_LEAST8_WIDTH __UINT_LEAST8_WIDTH
|
||||
# define INT_LEAST8_WIDTH UINT_LEAST8_WIDTH
|
||||
# define UINT_FAST8_WIDTH __UINT_LEAST8_WIDTH
|
||||
# define INT_FAST8_WIDTH UINT_FAST8_WIDTH
|
||||
#endif /* __STDC_VERSION__ */
|
||||
#endif /* __INT_LEAST8_MIN */
|
||||
|
||||
/* Some utility macros */
|
||||
#define __INTN_MIN(n) __stdint_join3( INT, n, _MIN)
|
||||
#define __INTN_MAX(n) __stdint_join3( INT, n, _MAX)
|
||||
#define __UINTN_MAX(n) __stdint_join3(UINT, n, _MAX)
|
||||
#define __INTN_C(n, v) __stdint_join3( INT, n, _C(v))
|
||||
#define __UINTN_C(n, v) __stdint_join3(UINT, n, _C(v))
|
||||
|
||||
/* C99 7.18.2.4 Limits of integer types capable of holding object pointers. */
|
||||
/* C99 7.18.3 Limits of other integer types. */
|
||||
|
||||
#define INTPTR_MIN (-__INTPTR_MAX__-1)
|
||||
#define INTPTR_MAX __INTPTR_MAX__
|
||||
#define UINTPTR_MAX __UINTPTR_MAX__
|
||||
#define PTRDIFF_MIN (-__PTRDIFF_MAX__-1)
|
||||
#define PTRDIFF_MAX __PTRDIFF_MAX__
|
||||
#define SIZE_MAX __SIZE_MAX__
|
||||
|
||||
/* C23 7.22.2.4 Width of integer types capable of holding object pointers. */
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
/* NB: The C standard requires that these be the same value, but the compiler
|
||||
exposes separate internal width macros. */
|
||||
#define INTPTR_WIDTH __INTPTR_WIDTH__
|
||||
#define UINTPTR_WIDTH __UINTPTR_WIDTH__
|
||||
#endif
|
||||
|
||||
/* ISO9899:2011 7.20 (C11 Annex K): Define RSIZE_MAX if __STDC_WANT_LIB_EXT1__
|
||||
* is enabled. */
|
||||
#if defined(__STDC_WANT_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__ >= 1
|
||||
#define RSIZE_MAX (SIZE_MAX >> 1)
|
||||
#endif
|
||||
|
||||
/* C99 7.18.2.5 Limits of greatest-width integer types. */
|
||||
#define INTMAX_MIN (-__INTMAX_MAX__-1)
|
||||
#define INTMAX_MAX __INTMAX_MAX__
|
||||
#define UINTMAX_MAX __UINTMAX_MAX__
|
||||
|
||||
/* C23 7.22.2.5 Width of greatest-width integer types. */
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
/* NB: The C standard requires that these be the same value, but the compiler
|
||||
exposes separate internal width macros. */
|
||||
#define INTMAX_WIDTH __INTMAX_WIDTH__
|
||||
#define UINTMAX_WIDTH __UINTMAX_WIDTH__
|
||||
#endif
|
||||
|
||||
/* C99 7.18.3 Limits of other integer types. */
|
||||
#define SIG_ATOMIC_MIN __INTN_MIN(__SIG_ATOMIC_WIDTH__)
|
||||
#define SIG_ATOMIC_MAX __INTN_MAX(__SIG_ATOMIC_WIDTH__)
|
||||
#ifdef __WINT_UNSIGNED__
|
||||
# define WINT_MIN __UINTN_C(__WINT_WIDTH__, 0)
|
||||
# define WINT_MAX __UINTN_MAX(__WINT_WIDTH__)
|
||||
#else
|
||||
# define WINT_MIN __INTN_MIN(__WINT_WIDTH__)
|
||||
# define WINT_MAX __INTN_MAX(__WINT_WIDTH__)
|
||||
#endif
|
||||
|
||||
#ifndef WCHAR_MAX
|
||||
# define WCHAR_MAX __WCHAR_MAX__
|
||||
#endif
|
||||
#ifndef WCHAR_MIN
|
||||
# if __WCHAR_MAX__ == __INTN_MAX(__WCHAR_WIDTH__)
|
||||
# define WCHAR_MIN __INTN_MIN(__WCHAR_WIDTH__)
|
||||
# else
|
||||
# define WCHAR_MIN __UINTN_C(__WCHAR_WIDTH__, 0)
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/* 7.18.4.2 Macros for greatest-width integer constants. */
|
||||
#define INTMAX_C(v) __int_c(v, __INTMAX_C_SUFFIX__)
|
||||
#define UINTMAX_C(v) __int_c(v, __UINTMAX_C_SUFFIX__)
|
||||
|
||||
/* C23 7.22.3.x Width of other integer types. */
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
#define PTRDIFF_WIDTH __PTRDIFF_WIDTH__
|
||||
#define SIG_ATOMIC_WIDTH __SIG_ATOMIC_WIDTH__
|
||||
#define SIZE_WIDTH __SIZE_WIDTH__
|
||||
#define WCHAR_WIDTH __WCHAR_WIDTH__
|
||||
#define WINT_WIDTH __WINT_WIDTH__
|
||||
#endif
|
||||
|
||||
#endif /* __STDC_HOSTED__ */
|
||||
#endif /* __MVS__ */
|
||||
#endif /* __CLANG_STDINT_H */
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
// Copyright 2011-2017 David Robillard <d@drobilla.net>
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
/// @file suil.h Public API for Suil
|
||||
|
||||
#ifndef SUIL_SUIL_H
|
||||
#define SUIL_SUIL_H
|
||||
|
||||
typedef struct { const char *URI; void *data; } LV2_Feature;
|
||||
|
||||
#include "stdbool.h"
|
||||
#include "stdint.h"
|
||||
|
||||
// SUIL_LIB_IMPORT and SUIL_LIB_EXPORT mark the entry points of shared libraries
|
||||
#ifdef _WIN32
|
||||
# define SUIL_LIB_IMPORT __declspec(dllimport)
|
||||
# define SUIL_LIB_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
# define SUIL_LIB_IMPORT __attribute__((visibility("default")))
|
||||
# define SUIL_LIB_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
// SUIL_API exposes symbols in the public API
|
||||
#ifndef SUIL_API
|
||||
# ifdef SUIL_STATIC
|
||||
# define SUIL_API
|
||||
# elif defined(SUIL_INTERNAL)
|
||||
# define SUIL_API SUIL_LIB_EXPORT
|
||||
# else
|
||||
# define SUIL_API SUIL_LIB_IMPORT
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
@defgroup suil Suil C API
|
||||
@{
|
||||
*/
|
||||
|
||||
/**
|
||||
@defgroup suil_callbacks Callbacks
|
||||
@{
|
||||
*/
|
||||
|
||||
/**
|
||||
UI controller.
|
||||
|
||||
This is an opaque pointer passed by the user which is passed to the various
|
||||
UI control functions (e.g. SuilPortWriteFunc). It is typically used to pass
|
||||
a pointer to some controller object the host uses to communicate with
|
||||
plugins.
|
||||
*/
|
||||
typedef void* SuilController;
|
||||
|
||||
/// Function to write/send a value to a port
|
||||
typedef void (*SuilPortWriteFunc)( //
|
||||
SuilController controller,
|
||||
uint32_t port_index,
|
||||
uint32_t buffer_size,
|
||||
uint32_t protocol,
|
||||
void const* buffer);
|
||||
|
||||
/// Function to return the index for a port by symbol
|
||||
typedef uint32_t (*SuilPortIndexFunc)( //
|
||||
SuilController controller,
|
||||
const char* port_symbol);
|
||||
|
||||
/// Function to subscribe to notifications for a port
|
||||
typedef uint32_t (*SuilPortSubscribeFunc)( //
|
||||
SuilController controller,
|
||||
uint32_t port_index,
|
||||
uint32_t protocol,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
/// Function to unsubscribe from notifications for a port
|
||||
typedef uint32_t (*SuilPortUnsubscribeFunc)( //
|
||||
SuilController controller,
|
||||
uint32_t port_index,
|
||||
uint32_t protocol,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
/// Function called when a control is grabbed or released
|
||||
typedef void (*SuilTouchFunc)( //
|
||||
SuilController controller,
|
||||
uint32_t port_index,
|
||||
bool grabbed);
|
||||
|
||||
/**
|
||||
@}
|
||||
@defgroup suil_library Library
|
||||
@{
|
||||
*/
|
||||
|
||||
/// Initialization argument
|
||||
typedef enum { SUIL_ARG_NONE } SuilArg;
|
||||
|
||||
/**
|
||||
Initialize suil.
|
||||
|
||||
This function should be called as early as possible, before any other GUI
|
||||
toolkit functions. The variable argument list is a sequence of SuilArg keys
|
||||
and corresponding value pairs for passing any necessary platform-specific
|
||||
information. It must be terminated with SUIL_ARG_NONE.
|
||||
*/
|
||||
SUIL_API
|
||||
void
|
||||
suil_init(int* argc, char*** argv, SuilArg key, ...);
|
||||
|
||||
/**
|
||||
Check if suil can wrap a UI type.
|
||||
|
||||
@param host_type_uri The URI of the desired widget type of the host,
|
||||
corresponding to the `type_uri` parameter of suil_instance_new().
|
||||
|
||||
@param ui_type_uri The URI of the UI widget type.
|
||||
|
||||
@return 0 if wrapping is unsupported, otherwise the quality of the wrapping
|
||||
where 1 is the highest quality (direct native embedding with no wrapping)
|
||||
and increasing values are of a progressively lower quality and/or stability.
|
||||
*/
|
||||
SUIL_API
|
||||
unsigned
|
||||
suil_ui_supported(const char* host_type_uri, const char* ui_type_uri);
|
||||
|
||||
/**
|
||||
@}
|
||||
@defgroup suil_host Host
|
||||
@{
|
||||
*/
|
||||
|
||||
/**
|
||||
UI host descriptor.
|
||||
|
||||
This contains the various functions that a plugin UI may use to communicate
|
||||
with the plugin. It is passed to suil_instance_new() to provide these
|
||||
functions to the UI.
|
||||
*/
|
||||
typedef struct SuilHostImpl SuilHost;
|
||||
|
||||
/**
|
||||
Create a new UI host descriptor.
|
||||
|
||||
@param write_func Function to send a value to a plugin port.
|
||||
@param index_func Function to get the index for a port by symbol.
|
||||
@param subscribe_func Function to subscribe to port updates.
|
||||
@param unsubscribe_func Function to unsubscribe from port updates.
|
||||
*/
|
||||
SUIL_API
|
||||
SuilHost*
|
||||
suil_host_new(SuilPortWriteFunc write_func,
|
||||
SuilPortIndexFunc index_func,
|
||||
SuilPortSubscribeFunc subscribe_func,
|
||||
SuilPortUnsubscribeFunc unsubscribe_func);
|
||||
|
||||
/**
|
||||
Set a touch function for a host descriptor.
|
||||
|
||||
Note this function will only be called if the UI supports it.
|
||||
*/
|
||||
SUIL_API
|
||||
void
|
||||
suil_host_set_touch_func(SuilHost* host, SuilTouchFunc touch_func);
|
||||
|
||||
/**
|
||||
Free `host`.
|
||||
*/
|
||||
SUIL_API
|
||||
void
|
||||
suil_host_free(SuilHost* host);
|
||||
|
||||
/**
|
||||
@}
|
||||
*/
|
||||
|
||||
/**
|
||||
@defgroup suil_instance Instance
|
||||
@{
|
||||
*/
|
||||
|
||||
/// An instance of an LV2 plugin UI
|
||||
typedef struct SuilInstanceImpl SuilInstance;
|
||||
|
||||
/// Opaque pointer to a UI handle
|
||||
typedef void* SuilHandle;
|
||||
|
||||
/// Opaque pointer to a UI widget
|
||||
typedef void* SuilWidget;
|
||||
|
||||
/**
|
||||
Instantiate a UI for an LV2 plugin.
|
||||
|
||||
This funcion may load a suil module to adapt the UI to the desired toolkit.
|
||||
Suil is configured at compile time to load modules from the appropriate
|
||||
place, but this can be changed at run-time via the environment variable
|
||||
SUIL_MODULE_DIR. This makes it possible to bundle suil with an application.
|
||||
|
||||
Note that some situations (Gtk in Qt, Windows in Gtk) require a parent
|
||||
container to be passed as a feature with URI LV2_UI__parent
|
||||
(http://lv2plug.in/ns/extensions/ui#ui) in order to work correctly. The
|
||||
data must point to a single child container of the host widget set.
|
||||
|
||||
@param host Host descriptor.
|
||||
@param controller Opaque host controller pointer.
|
||||
@param container_type_uri URI of the desired host container widget type.
|
||||
@param plugin_uri URI of the plugin to instantiate this UI for.
|
||||
@param ui_uri URI of the specifically desired UI.
|
||||
@param ui_type_uri URI of the actual UI widget type.
|
||||
@param ui_bundle_path Path of the UI bundle.
|
||||
@param ui_binary_path Path of the UI binary.
|
||||
@param features NULL-terminated array of supported features, or NULL.
|
||||
@return A new UI instance, or NULL if instantiation failed.
|
||||
*/
|
||||
SUIL_API
|
||||
SuilInstance*
|
||||
suil_instance_new(SuilHost* host,
|
||||
SuilController controller,
|
||||
const char* container_type_uri,
|
||||
const char* plugin_uri,
|
||||
const char* ui_uri,
|
||||
const char* ui_type_uri,
|
||||
const char* ui_bundle_path,
|
||||
const char* ui_binary_path,
|
||||
const LV2_Feature* const* features);
|
||||
|
||||
/**
|
||||
Free a plugin UI instance.
|
||||
|
||||
The caller must ensure all references to the UI have been dropped before
|
||||
calling this function (e.g. it has been removed from its parent).
|
||||
*/
|
||||
SUIL_API
|
||||
void
|
||||
suil_instance_free(SuilInstance* instance);
|
||||
|
||||
/**
|
||||
Get the handle for a UI instance.
|
||||
|
||||
Returns the handle to the UI instance. The returned handle has opaque type
|
||||
to insulate the Suil API from LV2 extensions, but in pactice it is currently
|
||||
of type `LV2UI_Handle`. This should not normally be needed.
|
||||
|
||||
The returned handle is shared and must not be deleted.
|
||||
*/
|
||||
SUIL_API
|
||||
SuilHandle
|
||||
suil_instance_get_handle(SuilInstance* instance);
|
||||
|
||||
/**
|
||||
Get the widget for a UI instance.
|
||||
|
||||
Returns an opaque pointer to a widget, the type of which matches the
|
||||
`container_type_uri` parameter of suil_instance_new(). Note this may be a
|
||||
wrapper widget created by Suil, and not necessarily the widget directly
|
||||
implemented by the UI.
|
||||
*/
|
||||
SUIL_API
|
||||
SuilWidget
|
||||
suil_instance_get_widget(SuilInstance* instance);
|
||||
|
||||
/**
|
||||
Notify the UI about a change in a plugin port.
|
||||
|
||||
This function can be used to notify the UI about any port change, but in the
|
||||
simplest case is used to set the value of lv2:ControlPort ports. For
|
||||
simplicity, this is a special case where `format` is 0, `buffer_size` is 4,
|
||||
and `buffer` should point to a single float.
|
||||
|
||||
The `buffer` must be valid only for the duration of this call, the UI must
|
||||
not keep a reference to it.
|
||||
|
||||
@param instance UI instance.
|
||||
@param port_index Index of the port which has changed.
|
||||
@param buffer_size Size of `buffer` in bytes.
|
||||
@param format Format of `buffer` (mapped URI, or 0 for float).
|
||||
@param buffer Change data, e.g. the new port value.
|
||||
*/
|
||||
SUIL_API
|
||||
void
|
||||
suil_instance_port_event(SuilInstance* instance,
|
||||
uint32_t port_index,
|
||||
uint32_t buffer_size,
|
||||
uint32_t format,
|
||||
const void* buffer);
|
||||
|
||||
/// Return a data structure defined by some LV2 extension URI
|
||||
SUIL_API
|
||||
const void*
|
||||
suil_instance_extension_data(SuilInstance* instance, const char* uri);
|
||||
|
||||
/**
|
||||
@}
|
||||
@}
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* SUIL_SUIL_H */
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
clojure-reader = "0.1.0"
|
||||
microxdg = "0.1.2"
|
||||
|
||||
tek_core = { path = "../tek_core" }
|
||||
#tek_sequencer = { path = "../tek_sequencer" }
|
||||
#tek_mixer = { path = "../tek_mixer" }
|
||||
#jack = "0.10"
|
||||
#crossterm = "0.27"
|
||||
#ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
#backtrace = "0.3.72"
|
||||
#toml = "0.8.12"
|
||||
#better-panic = "0.3.0"
|
||||
#midly = "0.5"
|
||||
|
||||
#vst = "0.4.0"
|
||||
##vst3 = "0.1.0"
|
||||
#livi = "0.7.4"
|
||||
##atomic_enum = "0.3.0"
|
||||
#wavers = "1.4.3"
|
||||
#music-math = "0.1.1"
|
||||
#fraction = "0.15.3"
|
||||
#rlsf = "0.2.1"
|
||||
#r8brain-rs = "0.3.5"
|
||||
|
||||
#symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||
|
||||
#dasp = { version = "0.11.0", features = [ "all" ] }
|
||||
|
||||
#rubato = "0.15.0"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# `tek`
|
||||
|
||||
This crate unifies the `tek_*` subcrates into a single application.
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
;(bpm 150)
|
||||
|
||||
;(midi-in "nanoKEY Studio.*capture.*")
|
||||
;(midi-in "nanoKEY Studio.*capture.*")
|
||||
;(audio-out "Built-.+:playback_FL", "Built-.+:playback_FR")
|
||||
|
||||
;(scene { :name "Intro" } 0 0 _ _)
|
||||
;(scene { :name "Hook" } 1 1 0 _)
|
||||
;(scene { :name "Verse" } 2 2 1 _)
|
||||
;(scene { :name "Chorus" } 3 3 2 _)
|
||||
;(scene { :name "Bridge" } _ 4 3 _)
|
||||
;(scene { :name "Outro" } 4 1 4 _)
|
||||
|
||||
;(track { :name "Drums" :gain +0.0 }
|
||||
;(phrase { :name "4 kicks" :beats 4 :steps 16 }
|
||||
;(:00 (36 128))
|
||||
;(:04 (36 100))
|
||||
;(:08 (36 100))
|
||||
;(:12 (36 100)))
|
||||
;(phrase { :name "5 kicks" :beats 4 :steps 16 }
|
||||
;(:00 (36 128))
|
||||
;(:04 (36 100))
|
||||
;(:08 (36 128))
|
||||
;(:12 (36 100))
|
||||
;(:14 (36 110)))
|
||||
;(phrase { :name "D Beat" :beats 8 :steps 32 }
|
||||
;(:00 (44 70) (36 128) (49 110))
|
||||
;(:02 (44 30))
|
||||
;(:04 (44 80) (40 100))
|
||||
;(:06 (44 50))
|
||||
;(:08 (44 30) (36 100))
|
||||
;(:10 (44 50) (36 100))
|
||||
;(:12 (44 80) (40 100))
|
||||
;(:14 (44 50))
|
||||
;(:15 (36 50))
|
||||
;(:16 (44 60) (36 80))
|
||||
;(:18 (44 60) (36 80))
|
||||
;(:20 (44 60) (40 80))
|
||||
;(:22 (44 60))
|
||||
;(:24 (44 60))
|
||||
;(:26 (44 30) (36 80))
|
||||
;(:27 (44 60))
|
||||
;(:28 (44 60) (40 80))
|
||||
;(:30 (44 60)))
|
||||
;(phrase { :name "Garage" :beats 4 :steps 16 }
|
||||
;(:00 (44 100) (36 100) (35 100))
|
||||
;(:01 (44 100))
|
||||
;(:02 (44 100) (35 100))
|
||||
;(:03 (44 100))
|
||||
;(:04 (44 100) (40 100))
|
||||
;(:06 (44 100))
|
||||
;(:07 (44 100) (34 100))
|
||||
;(:09 (44 100))
|
||||
;(:10 (44 100))
|
||||
;(:11 (35 100) (36 100))
|
||||
;(:12 (44 100) (40 100))
|
||||
;(:14 (44 100)))
|
||||
|
||||
;(phrase { :name "Trap Pinging" :beats 8 :steps 96 }
|
||||
;(:00 (42 100) (36 100) (34 120) (49 100))
|
||||
;(:01 (42 100))
|
||||
;(:02 (42 100))
|
||||
;(:06 (42 100) (35 80) (36 80) (49 100))
|
||||
;(:07 (42 100))
|
||||
;(:08 (42 100))
|
||||
;(:12 (42 100))
|
||||
;(:15 (39 100) (34 100))
|
||||
;(:18 (42 100))
|
||||
;(:24 (42 100) (38 50) (40 50))
|
||||
;(:27 (42 100) (36 50))
|
||||
;(:30 (42 100))
|
||||
;(:33 (42 100) (36 50) (34 100))
|
||||
;(:36 (42 90))
|
||||
;(:39 (42 80))
|
||||
;(:42 (42 70))
|
||||
;(:45 (42 60))
|
||||
|
||||
;(:48 (42 100) (36 100) (34 100))
|
||||
;(:50 (42 100))
|
||||
;(:52 (42 110))
|
||||
;(:54 (46 50) (42 120))
|
||||
;(:56 (42 90))
|
||||
;(:58 (42 100))
|
||||
;(:60 (42 100) (35 100))
|
||||
;(:64 (39 100))
|
||||
;(:66 (42 100) (34 100))
|
||||
|
||||
;(:70 (42 100))
|
||||
;(:71 (42 100))
|
||||
;(:72 (42 100) (38 50) (40 50))
|
||||
;(:75 (42 100) (36 50) (34 80))
|
||||
;(:78 (42 100))
|
||||
;(:81 (42 100) (36 50))
|
||||
;(:84 (38 40) (40 50) (34 90))
|
||||
;(:87 (42 90) (35 40))
|
||||
;(:90 (42 70)))
|
||||
|
||||
;(sampler { :name "DrumKit1" :dir "/home/user/Lab/Music/pak" }
|
||||
;(sample { :midi 34 :name "808 D" :file "808.wav" })
|
||||
;(sample { :midi 35 :name "Kick 1" :file "kik.wav" })
|
||||
;(sample { :midi 36 :name "Kick 2" :file "kik2.wav" })
|
||||
;(sample { :midi 37 :name "Rim" :file "rim.wav" })
|
||||
;(sample { :midi 38 :name "Snare 1" :file "sna.wav" })
|
||||
;(sample { :midi 39 :name "Shaker" :file "shk.wav" })
|
||||
;(sample { :midi 40 :name "Snare 2" :file "sna2.wav" })
|
||||
;(sample { :midi 42 :name "Closed HH 1" :file "chh.wav" })
|
||||
;(sample { :midi 44 :name "Closed HH 2" :file "chh2.wav" })
|
||||
;(sample { :midi 45 :name "Open HH 0" :file "ohh.wav" })
|
||||
;(sample { :midi 46 :name "Open HH 1" :file "ohh1.wav" })
|
||||
;(sample { :midi 47 :name "Open HH 2" :file "ohh2.wav" })
|
||||
;(sample { :midi 49 :name "Crash" :file "crs.wav" })))
|
||||
|
||||
;(track { :name "Bass" :gain +0.0 }
|
||||
;(phrase { :name "Bass 1" :beats 4 })
|
||||
;(phrase { :name "Bass 2" :beats 4 })
|
||||
;(phrase { :name "Bass 3" :beats 4 })
|
||||
;(phrase { :name "Bass 4" :beats 4 })
|
||||
;(phrase { :name "Bass 5" :beats 4 })
|
||||
;(phrase { :name "Bass 6" :beats 4 })
|
||||
;(phrase { :name "Bass 7" :beats 4 })
|
||||
;(phrase { :name "Bass 8" :beats 4 })
|
||||
;(lv2 {
|
||||
;:name "Odin2"
|
||||
;:path "file:///home/user/.lv2/Odin2.lv2"
|
||||
;}))
|
||||
|
||||
;(track { :name "Lead" :gain +0.0 }
|
||||
;(phrase { :name "Lead 1" :beats 4 })
|
||||
;(phrase { :name "Lead 2" :beats 4 })
|
||||
;(phrase { :name "Lead 3" :beats 4 })
|
||||
;(phrase { :name "Lead 4" :beats 4 })
|
||||
;(phrase { :name "Lead 5" :beats 4 })
|
||||
;(phrase { :name "Lead 6" :beats 4 })
|
||||
;(phrase { :name "Lead 7" :beats 4 })
|
||||
;(phrase { :name "Lead 8" :beats 4 })
|
||||
;(lv2 {
|
||||
;:name "Odin2"
|
||||
;:path "file:///home/user/.lv2/Odin2.lv2"
|
||||
;}))
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
use crate::*;
|
||||
use tek_core::Direction;
|
||||
use tek_mixer::Mixer;
|
||||
use tek_sequencer::{Arranger, TransportToolbar};
|
||||
|
||||
/// Root of application state.
|
||||
pub struct App {
|
||||
/// Whether the currently focused section has input priority
|
||||
pub entered: bool,
|
||||
/// Currently focused section
|
||||
pub section: AppFocus,
|
||||
/// Transport model and view.
|
||||
pub transport: TransportToolbar,
|
||||
/// Arranger (contains sequencers)
|
||||
pub arranger: Arranger,
|
||||
/// Mixer (contains tracks)
|
||||
pub mixer: Mixer,
|
||||
/// Main JACK client.
|
||||
pub jack: Option<JackClientt>,
|
||||
/// Map of external MIDI outs in the jack graph
|
||||
/// to internal MIDI ins of this app.
|
||||
pub midi_in: Option<Arc<Port<MidiIn>>>,
|
||||
/// Names of ports to connect to main MIDI IN.
|
||||
pub midi_ins: Vec<String>,
|
||||
/// Display mode of chain section
|
||||
pub chain_mode: bool,
|
||||
/// Main audio outputs.
|
||||
pub audio_outs: Vec<Arc<Port<Unowned>>>,
|
||||
/// Number of frames requested by process callback
|
||||
chunk_size: usize,
|
||||
/// Paths to user directories
|
||||
_xdg: Option<Arc<XdgApp>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Usually<Self> {
|
||||
let xdg = Arc::new(XdgApp::new("tek")?);
|
||||
let first_run = AppPaths::new(&xdg)?.should_create();
|
||||
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
|
||||
*MODAL.lock().unwrap() =
|
||||
first_run.then(|| ExitableComponent::boxed(SetupModal(Some(xdg.clone()), false)));
|
||||
Ok(Self {
|
||||
entered: true,
|
||||
section: AppFocus::default(),
|
||||
transport: TransportToolbar::new(Some(jack.transport())),
|
||||
arranger: Arranger::new(""),
|
||||
mixer: Mixer::new("")?,
|
||||
jack: Some(jack),
|
||||
audio_outs: vec![],
|
||||
chain_mode: false,
|
||||
chunk_size: 0,
|
||||
midi_in: None,
|
||||
midi_ins: vec![],
|
||||
_xdg: Some(xdg),
|
||||
})
|
||||
}
|
||||
pub fn client(&self) -> &Client {
|
||||
self.jack.as_ref().unwrap().client()
|
||||
}
|
||||
pub fn audio_out(&self, index: usize) -> Option<Arc<Port<Unowned>>> {
|
||||
self.audio_outs.get(index).map(|x| x.clone())
|
||||
}
|
||||
pub fn with_midi_ins(mut self, names: &[&str]) -> Usually<Self> {
|
||||
self.midi_ins = names.iter().map(|x| x.to_string()).collect();
|
||||
Ok(self)
|
||||
}
|
||||
pub fn with_audio_outs(mut self, names: &[&str]) -> Usually<Self> {
|
||||
let client = self.client();
|
||||
self.audio_outs = names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
client
|
||||
.ports(Some(name), None, PortFlags::empty())
|
||||
.get(0)
|
||||
.map(|name| client.port_by_name(name))
|
||||
})
|
||||
.flatten()
|
||||
.filter_map(|x| x)
|
||||
.map(Arc::new)
|
||||
.collect();
|
||||
Ok(self)
|
||||
}
|
||||
pub fn activate(
|
||||
mut self,
|
||||
init: Option<impl FnOnce(&Arc<RwLock<Self>>) -> Usually<()>>,
|
||||
) -> Usually<Arc<RwLock<Self>>> {
|
||||
let jack = self.jack.take().expect("no jack client");
|
||||
let app = Arc::new(RwLock::new(self));
|
||||
app.write().unwrap().jack = Some(jack.activate(&app.clone(), |state, client, scope| {
|
||||
state.write().unwrap().process(client, scope)
|
||||
})?);
|
||||
if let Some(init) = init {
|
||||
init(&app)?;
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
}
|
||||
|
||||
render!(
|
||||
App | self,
|
||||
buf,
|
||||
area | {
|
||||
Stack::down()
|
||||
.add_ref(&self.transport)
|
||||
.add_ref(&self.arranger)
|
||||
.add(If(
|
||||
self.arranger.selected.is_clip(),
|
||||
&Stack::right()
|
||||
.add(tek_mixer::TrackView {
|
||||
direction: Direction::Down,
|
||||
entered: self.entered,
|
||||
focused: self.section == AppFocus::Chain,
|
||||
chain: self.mixer.track(),
|
||||
})
|
||||
.add_ref(&self.arranger.sequencer()),
|
||||
))
|
||||
.render(buf, area)?;
|
||||
if let Some(ref modal) = *MODAL.lock().unwrap() {
|
||||
modal.render(buf, area)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
);
|
||||
|
||||
process!(
|
||||
App | self,
|
||||
_client,
|
||||
scope | {
|
||||
let (reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs) =
|
||||
self.transport.update(&scope);
|
||||
self.chunk_size = chunk_size;
|
||||
for track in self.arranger.tracks.iter_mut() {
|
||||
track.process(
|
||||
self.midi_in.as_ref().map(|p| p.iter(&scope)),
|
||||
&self.transport.timebase,
|
||||
self.transport.playing,
|
||||
self.transport.started,
|
||||
self.transport.quant as usize,
|
||||
reset,
|
||||
&scope,
|
||||
(current_frames as usize, self.chunk_size),
|
||||
(
|
||||
current_usecs as usize,
|
||||
next_usecs.saturating_sub(current_usecs) as usize,
|
||||
),
|
||||
period_usecs as f64,
|
||||
);
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Different sections of the UI that may be focused.
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub enum AppFocus {
|
||||
/// The transport is selected.
|
||||
Transport,
|
||||
/// The arranger is selected.
|
||||
Arranger,
|
||||
/// The sequencer is selected.
|
||||
Sequencer,
|
||||
/// The device chain is selected.
|
||||
Chain,
|
||||
}
|
||||
|
||||
impl Default for AppFocus {
|
||||
fn default () -> Self { Self::Arranger }
|
||||
}
|
||||
|
||||
impl AppFocus {
|
||||
pub fn prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Transport => Self::Chain,
|
||||
Self::Arranger => Self::Transport,
|
||||
Self::Sequencer => Self::Arranger,
|
||||
Self::Chain => Self::Sequencer,
|
||||
}
|
||||
}
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Transport => Self::Arranger,
|
||||
Self::Arranger => Self::Sequencer,
|
||||
Self::Sequencer => Self::Chain,
|
||||
Self::Chain => Self::Transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
//! Global settings.
|
||||
|
||||
use crate::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{File, create_dir_all};
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "tek.toml";
|
||||
const PROJECT_FILE_NAME: &str = "project.toml";
|
||||
|
||||
/// Filesystem locations of things.
|
||||
pub struct AppPaths {
|
||||
config_dir: PathBuf,
|
||||
config_file: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
project_file: PathBuf,
|
||||
}
|
||||
|
||||
impl AppPaths {
|
||||
pub fn new (xdg: &XdgApp) -> Usually<Self> {
|
||||
let config_dir = PathBuf::from(xdg.app_config()?);
|
||||
let config_file = PathBuf::from(config_dir.join(CONFIG_FILE_NAME));
|
||||
let data_dir = PathBuf::from(xdg.app_data()?);
|
||||
let project_file = PathBuf::from(data_dir.join(PROJECT_FILE_NAME));
|
||||
Ok(Self { config_dir, config_file, data_dir, project_file })
|
||||
}
|
||||
pub fn should_create (&self) -> bool {
|
||||
for path in [
|
||||
&self.config_dir,
|
||||
&self.config_file,
|
||||
&self.data_dir,
|
||||
&self.project_file,
|
||||
].iter() {
|
||||
if !Path::new(path).exists() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn create (&self) -> Usually<()> {
|
||||
for dir in [&self.config_dir, &self.data_dir].iter() {
|
||||
if !Path::new(dir).exists() {
|
||||
create_dir_all(&dir)?;
|
||||
}
|
||||
}
|
||||
for file in [&self.config_file, &self.project_file].iter() {
|
||||
if !Path::new(file).exists() {
|
||||
File::create_new(&file)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//! Command line option parser.
|
||||
|
||||
use tek_core::clap::{self, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Launch or control a master transport
|
||||
Transport,
|
||||
/// Launch or control a sequencer
|
||||
Sequencer {
|
||||
#[arg(long="input")]
|
||||
inputs: Vec<Option<String>>,
|
||||
#[arg(long="output")]
|
||||
outputs: Vec<Option<String>>,
|
||||
},
|
||||
/// Launch or control a sampler
|
||||
Sampler,
|
||||
/// Launch or control a mixer
|
||||
Mixer,
|
||||
/// Launch or control a looper
|
||||
Looper,
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
//! Handling of input events.
|
||||
|
||||
use crate::*;
|
||||
|
||||
handle!{
|
||||
App |self, e| {
|
||||
if handle_modal(e)? {
|
||||
return Ok(true)
|
||||
}
|
||||
Ok(if self.entered {
|
||||
handle_focused(self, e)?
|
||||
|| handle_keymap(self, e, KEYMAP_GLOBAL)?
|
||||
|| handle_keymap(self, e, crate::control::KEYMAP_FOCUS)?
|
||||
} else {
|
||||
handle_keymap(self, e, KEYMAP_GLOBAL)?
|
||||
|| handle_keymap(self, e, crate::control::KEYMAP_FOCUS)?
|
||||
|| handle_focused(self, e)?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modal (e: &AppEvent) -> Usually<bool> {
|
||||
let mut handled = false;
|
||||
let mut close = false;
|
||||
if let Some(ref mut modal) = *MODAL.lock().unwrap() {
|
||||
if modal.handle(e)? {
|
||||
handled = true;
|
||||
if modal.exited() {
|
||||
close = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
if close {
|
||||
*MODAL.lock().unwrap() = None;
|
||||
}
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
fn handle_focused (state: &mut App, e: &AppEvent) -> Usually<bool> {
|
||||
unimplemented!()
|
||||
//match state.section {
|
||||
//AppFocus::Transport => state.transport.handle(e),
|
||||
//AppFocus::Arranger => state.arranger.sequencer_mut().map(|s|s.handle(e)),
|
||||
//AppFocus::Sequencer => state.arranger.sequencer_mut().map(|s|s.handle(e)),
|
||||
//AppFocus::Chain => Ok(false)[>if state.entered {
|
||||
//handle_device(state, e)? ||
|
||||
//handle_keymap(state, e, crate::control::KEYMAP_CHAIN)?
|
||||
//} else {
|
||||
//handle_keymap(state, e, crate::control::KEYMAP_CHAIN)? || handle_device(state, e)?
|
||||
//})*/
|
||||
//}
|
||||
}
|
||||
|
||||
fn handle_device (state: &mut App, e: &AppEvent) -> Usually<bool> {
|
||||
state.mixer.track()
|
||||
.and_then(|track|track.device_mut())
|
||||
.map(|mut device|device.handle(e))
|
||||
.transpose()
|
||||
.map(|x|x.unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Global key bindings.
|
||||
pub const KEYMAP_GLOBAL: &'static [KeyBinding<App>] = keymap!(App {
|
||||
[Char(' '), NONE, "play_toggle", "play or pause", |app: &mut App| {
|
||||
app.transport.toggle_play()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('r'), NONE, "record_toggle", "toggle recording", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_record());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('o'), NONE, "overdub_toggle", "toggle overdub", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_overdub());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('m'), NONE, "monitor_toggle", "toggle monitor", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_monitor());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('+'), NONE, "quant_inc", "quantize coarser", |app: &mut App| {
|
||||
app.transport.quant = Note::next(app.transport.quant);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('_'), NONE, "quant_dec", "quantize finer", |app: &mut App| {
|
||||
app.transport.quant = Note::prev(app.transport.quant);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| {
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::prev));
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| {
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::next));
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('x'), NONE, "extend", "double the current clip", |app: &mut App| {
|
||||
app.arranger.phrase().map(|x|x.write().unwrap()).map(|mut phrase|{
|
||||
let mut notes = phrase.notes.clone();
|
||||
notes.extend_from_slice(&mut phrase.notes);
|
||||
phrase.notes = notes;
|
||||
phrase.length = phrase.length * 2;
|
||||
});
|
||||
//app.arranger.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('l'), NONE, "loop_toggle", "toggle looping", |_app: &mut App| {
|
||||
// TODO: This toggles the loop flag for the clip under the cursor.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('['), NONE, "loop_start_dec", "move loop start back", |_app: &mut App| {
|
||||
// TODO: This moves the loop start to the previous quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char(']'), NONE, "loop_start_inc", "move loop start forward", |_app: &mut App| {
|
||||
// TODO: This moves the loop start to the next quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('{'), NONE, "loop_end_dec", "move loop end back", |_app: &mut App| {
|
||||
// TODO: This moves the loop end to the previous quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('}'), NONE, "loop_end_inc", "move loop end forward", |_app: &mut App| {
|
||||
// TODO: This moves the loop end to the next quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('a'), CONTROL, "scene_add", "add a new scene", |app: &mut App| {
|
||||
app.arranger.scene_add(None)?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('t'), CONTROL, "track_add", "add a new track", |app: &mut App| {
|
||||
app.arranger.track_add(None)?;
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
/// Generic key bindings for views that support focus.
|
||||
pub const KEYMAP_FOCUS: &'static [KeyBinding<App>] = keymap!(App {
|
||||
[Char(';'), NONE, "command", "open command palette", |_: &mut App| {
|
||||
*MODAL.lock().unwrap() = Some(Box::new(HelpModal::new()));
|
||||
Ok(true)
|
||||
}],
|
||||
[Tab, NONE, "focus_next", "focus next area", focus_next],
|
||||
[Tab, SHIFT, "focus_prev", "focus previous area", focus_prev],
|
||||
[Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{
|
||||
app.entered = false;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|s.entered = app.entered);
|
||||
Ok(true)
|
||||
}],
|
||||
[Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{
|
||||
app.entered = true;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|s.entered = app.entered);
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
pub fn focus_next (app: &mut App) -> Usually<bool> {
|
||||
app.section.next();
|
||||
app.transport.focused = app.section == AppFocus::Transport;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.focused = app.section == AppFocus::Arranger;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|{
|
||||
s.focused = app.section == AppFocus::Sequencer;
|
||||
s.entered = app.entered;
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn focus_prev (app: &mut App) -> Usually<bool> {
|
||||
app.section.prev();
|
||||
app.transport.focused = app.section == AppFocus::Transport;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.focused = app.section == AppFocus::Arranger;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|{
|
||||
s.focused = app.section == AppFocus::Sequencer;
|
||||
s.entered = app.entered;
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
//! Project file format.
|
||||
//!
|
||||
//! This module `impl`s the `from_edn`, `load_edn`, etc. methods
|
||||
//! of structs that are defined in other modules. See:
|
||||
//!
|
||||
//! * [App::from_edn]
|
||||
//! * [App::load_edn]
|
||||
//! * [App::load_edn_one]
|
||||
//! * [Scene::load_edn]
|
||||
//! * [Track::load_edn]
|
||||
//! * [Phrase::load_edn]
|
||||
//! * [Sampler::load_edn]
|
||||
//! * [Sample::load_edn]
|
||||
//! * [LV2Plugin::load_edn]
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl App {
|
||||
|
||||
pub fn from_edn (src: &str) -> Usually<Self> {
|
||||
let mut app = Self::new()?;
|
||||
app.load_edn(src)?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub fn load_edn (&mut self, mut src: &str) -> Usually<&mut Self> {
|
||||
loop {
|
||||
match read(src) {
|
||||
Ok((edn, rest)) => {
|
||||
self.load_edn_one(edn)?;
|
||||
if rest.len() > 0 {
|
||||
src = rest;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
},
|
||||
Err(EdnError { ptr: None, .. }) => {
|
||||
break
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("{e:?}");
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn load_edn_one <'e> (&mut self, edn: Edn<'e>) -> Usually<()> {
|
||||
match edn {
|
||||
Edn::List(items) => {
|
||||
match items.get(0) {
|
||||
Some(Edn::Symbol("bpm")) => {
|
||||
match items.get(1) {
|
||||
Some(Edn::Int(b)) =>
|
||||
self.transport.timebase.set_bpm(*b as f64),
|
||||
Some(Edn::Double(b)) =>
|
||||
self.transport.timebase.set_bpm(f64::from(*b)),
|
||||
_ => panic!("unspecified bpm")
|
||||
}
|
||||
},
|
||||
Some(Edn::Symbol("scene")) => {
|
||||
tek_sequencer::Scene::from_edn(&items[1..])?;
|
||||
},
|
||||
Some(Edn::Symbol("track")) => {
|
||||
tek_mixer::Track::from_edn(&items[1..])?;
|
||||
},
|
||||
Some(Edn::Symbol("midi-in")) => {
|
||||
self.midi_ins = items[1..].iter().map(|x|match x {
|
||||
Edn::Str(n) => n.to_string(),
|
||||
_ => panic!("unexpected midi-in")
|
||||
}).collect::<Vec<_>>();
|
||||
},
|
||||
Some(Edn::Symbol("audio-out")) => {
|
||||
let client = self.client();
|
||||
self.audio_outs = items[1..].iter().map(|x|match x {
|
||||
Edn::Str(n) => n.to_string(),
|
||||
_ => panic!("unexpected midi-in")
|
||||
}).collect::<Vec<_>>()
|
||||
.iter()
|
||||
.map(|name|client
|
||||
.ports(Some(name), None, PortFlags::empty())
|
||||
.get(0)
|
||||
.map(|name|client.port_by_name(name)))
|
||||
.flatten()
|
||||
.filter_map(|x|x)
|
||||
.map(Arc::new)
|
||||
.collect();
|
||||
},
|
||||
_ => panic!("unexpected edn: {:?}", items.get(0))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
panic!("unexpected edn: {edn:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
//! Help modal / command palette.
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Command palette.
|
||||
pub struct HelpModal {
|
||||
cursor: usize,
|
||||
search: Option<String>,
|
||||
exited: bool,
|
||||
}
|
||||
impl HelpModal {
|
||||
pub fn new () -> Self {
|
||||
Self { cursor: 0, search: None, exited: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Exit for HelpModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
}
|
||||
|
||||
render!(HelpModal |self, buf, area|{
|
||||
make_dim(buf);
|
||||
let area = center_box(area, 64, 20);
|
||||
fill_fg(buf, area, Color::Reset);
|
||||
fill_bg(buf, area, Nord::bg_lo(true, true));
|
||||
fill_char(buf, area, ' ');
|
||||
let x = area.x + 2;
|
||||
let y = area.y + 1;
|
||||
"Command:"
|
||||
.blit(buf, x, y, Some(Style::default().bold()))?;
|
||||
" ".repeat(area.width as usize - 13)
|
||||
.blit(buf, x + 9, y, Some(Style::default().bg(Color::Reset)))?;
|
||||
if let Some(search) = self.search.as_ref() {
|
||||
search.blit(buf, x + 9, y, Some(Style::default().not_dim()))?;
|
||||
}
|
||||
let y = y + 1;
|
||||
fill_char(buf, Rect { y, height: 1, ..area }, '-');
|
||||
let y = y + 1;
|
||||
for i in 0..area.height-3 {
|
||||
let y = y + i;
|
||||
if let Some(command) = crate::control::KEYMAP_FOCUS.get(i as usize) {
|
||||
format!("{:?}", command.0).blit(buf, x, y, Some(Style::default().white().bold()))?;
|
||||
command.2.blit(buf, x + 11, y, Some(Style::default().white().bold()))?;
|
||||
command.3.blit(buf, x + 26, y, Some(Style::default().white().dim()))?;
|
||||
} else if let Some(command) = crate::control::KEYMAP_GLOBAL.get((i as usize) - crate::control::KEYMAP_FOCUS.len()) {
|
||||
format!("{:?}", command.0).blit(buf, x, y, Some(Style::default().white().bold()))?;
|
||||
command.2.blit(buf, x + 11, y, Some(Style::default().white().bold()))?;
|
||||
command.3.blit(buf, x + 26, y, Some(Style::default().white().dim()))?;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
let hi_area = Rect { x: area.x + 1, width: area.width - 2, y: area.y + 3 + self.cursor as u16, height: 1 };
|
||||
fill_bg(buf, hi_area, Nord::bg_hi(true, true));
|
||||
fill_fg(buf, hi_area, Color::White);
|
||||
Lozenge(Style::default()).draw(buf, area)
|
||||
});
|
||||
|
||||
handle!(HelpModal |self, e| {
|
||||
if handle_keymap(self, e, KEYMAP_HELP)? {
|
||||
return Ok(true)
|
||||
}
|
||||
Ok(match e {
|
||||
AppEvent::Input(Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers: KeyModifiers::NONE, ..
|
||||
})) => {
|
||||
if self.search.is_none() {
|
||||
self.search = Some(String::new());
|
||||
}
|
||||
self.search.as_mut().unwrap().push(*c);
|
||||
true
|
||||
},
|
||||
_ => true
|
||||
})
|
||||
});
|
||||
|
||||
pub const KEYMAP_HELP: &'static [KeyBinding<HelpModal>] = keymap!(HelpModal {
|
||||
[Esc, NONE, "help_close", "close help dialog", |modal: &mut HelpModal|{
|
||||
modal.exit();
|
||||
Ok(true)
|
||||
}],
|
||||
[Up, NONE, "help_prev", "select previous command", |modal: &mut HelpModal|{
|
||||
modal.cursor = modal.cursor.saturating_sub(1);
|
||||
Ok(true)
|
||||
}],
|
||||
[Down, NONE, "help_next", "select next command", |modal: &mut HelpModal|{
|
||||
modal.cursor = modal.cursor + 1;
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
//! ***Tek*** is a MIDI sequencer, sampler, and audio plugin host for the Linux terminal.
|
||||
|
||||
//#[global_allocator]
|
||||
//static A: rlsf::SmallGlobalTlsf = rlsf::SmallGlobalTlsf::new();
|
||||
//#![feature(fn_traits)]
|
||||
//#![feature(unboxed_closures)]
|
||||
#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
|
||||
#![allow(ambiguous_glob_reexports)]
|
||||
|
||||
pub(crate) use tek_core::{*, jack::*};
|
||||
pub(crate) use tek_sequencer::*;
|
||||
pub(crate) use tek_mixer::*;
|
||||
pub(crate) use microxdg::XdgApp;
|
||||
|
||||
submod! {
|
||||
app
|
||||
app_focus
|
||||
app_paths
|
||||
cli
|
||||
control
|
||||
edn
|
||||
help
|
||||
setup
|
||||
}
|
||||
|
||||
/// Global modal dialog
|
||||
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn ExitableComponent>>>>> =
|
||||
Lazy::new(||Arc::new(Mutex::new(None)));
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
run(App::from_edn(include_str!("../example.edn"))?
|
||||
.activate(Some(|app: &Arc<RwLock<App>>|Ok({
|
||||
let (midi_in, mut midi_outs) = {
|
||||
let app = app.read().unwrap();
|
||||
let jack = app.jack.as_ref().unwrap();
|
||||
let midi_in = jack.register_port("midi-in", MidiIn)?;
|
||||
let midi_outs = app.arranger.tracks.iter()
|
||||
.map(|t|Some(jack.register_port(&t.name.read().unwrap(), MidiOut).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
(midi_in, midi_outs)
|
||||
};
|
||||
{
|
||||
let mut app = app.write().unwrap();
|
||||
let jack = app.jack.as_ref().unwrap();
|
||||
for name in app.midi_ins.iter() {
|
||||
let ports = jack.client().ports(Some(name), None, PortFlags::empty());
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.client().port_by_name(port) {
|
||||
jack.client().connect_ports(&port, &midi_in)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
app.midi_in = Some(Arc::new(midi_in));
|
||||
for (index, track) in app.arranger.tracks.iter_mut().enumerate() {
|
||||
track.midi_out = midi_outs[index].take();
|
||||
}
|
||||
//for track in app.arranger.tracks.iter() {
|
||||
//track.connect_first_device()?;
|
||||
//track.connect_last_device(&app)?;
|
||||
//}
|
||||
};
|
||||
})))?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
//! Inital setup dialog (TODO: make this the options dialog too?)
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Appears on first run (i.e. if state dir is missing).
|
||||
pub struct SetupModal(pub Option<Arc<XdgApp>>, pub bool);
|
||||
|
||||
render!(SetupModal |self, buf, area| {
|
||||
for cell in buf.content.iter_mut() {
|
||||
cell.fg = ratatui::style::Color::Gray;
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
let lines = [
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Welcome to TEK! ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Press ENTER to create the ", Style::default().white().on_black().not_dim()),
|
||||
(" following directories: ", Style::default().white().on_black().not_dim()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Configuration directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
(" Data directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Or press CTRL-C to exit. ", Style::default().white().on_black().not_dim()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
];
|
||||
let width = lines[0].0.len() as u16;
|
||||
let x = area.x + (area.width - width) / 2;
|
||||
for (i, (line, style)) in lines.iter().enumerate() {
|
||||
line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style))?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
handle!(SetupModal |self, e| {
|
||||
if let AppEvent::Input(Event::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
})) = e {
|
||||
AppPaths::new(&self.0.as_ref().unwrap())?.create()?;
|
||||
self.exit();
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
});
|
||||
impl Exit for SetupModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.1
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.1 = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
#no_deadlocks = "1.3.2"
|
||||
#vst3 = "0.1.0"
|
||||
atomic_float = "1.0.0"
|
||||
backtrace = "0.3.72"
|
||||
better-panic = "0.3.0"
|
||||
clojure-reader = "0.1.0"
|
||||
crossterm = "0.27"
|
||||
jack = { path = "../../rust-jack" }
|
||||
livi = "0.7.4"
|
||||
midly = "0.5"
|
||||
once_cell = "1.19.0"
|
||||
palette = { version = "0.7.6", features = [ "random" ] }
|
||||
quanta = "0.12.3"
|
||||
rand = "0.8.5"
|
||||
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
#suil-rs = { path = "../suil" }
|
||||
symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||
toml = "0.8.12"
|
||||
uuid = { version = "1.10.0", features = [ "v4" ] }
|
||||
#vst = "0.4.0"
|
||||
wavers = "1.4.3"
|
||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# `tek`
|
||||
|
||||
This crate implements several musical utilities.
|
||||
|
||||
## `tek_sequencer`
|
||||
|
||||
A single-track, multi-pattern MIDI sequencer with properly tempo-synced pattern switch.
|
||||
|
||||
---
|
||||
|
||||
## `tek_arranger`
|
||||
|
||||
A multi-track, multi-pattern MIDI sequencer translating the familiar clip launching workflow
|
||||
into the TUI medium.
|
||||
|
||||
---
|
||||
|
||||
## `tek_groovebox`
|
||||
|
||||
TODO: A single-track, multi-pattern MIDI sequencer,
|
||||
attached to a sampler or plugin (see `tek_plugin`).
|
||||
|
||||
---
|
||||
|
||||
## `tek_timer`
|
||||
|
||||
TODO: This crate implements time sync and JACK transport control.
|
||||
|
||||
* Warning: If transport is set rolling by qjackctl, this program can't pause it
|
||||
* Todo: bpm: shift +/- 0.001
|
||||
* Todo: quant/sync: shift = next/prev value of same type (normal, triplet, dotted)
|
||||
* Or: use shift to switch between inc/dec top/bottom value?
|
||||
* Todo: focus play button
|
||||
* Todo: focus time position
|
||||
* Todo: edit numeric values
|
||||
* Todo: jump to time/bbt markers
|
||||
* Todo: count xruns
|
||||
|
||||
---
|
||||
|
||||
## `tek_mixer`
|
||||
|
||||
TODO:
|
||||
// - Meters: propagate clipping:
|
||||
// - If one stage clips, all stages after it are marked red
|
||||
// - If one track clips, all tracks that feed from it are marked red?
|
||||
|
||||
---
|
||||
|
||||
## `tek_track`
|
||||
|
||||
TODO:
|
||||
|
||||
---
|
||||
|
||||
## `tek_sampler`
|
||||
|
||||
TODO: This crate implements a sampler device which plays audio files
|
||||
in response to MIDI notes.
|
||||
|
||||
---
|
||||
|
||||
## `tek_plugin`
|
||||
|
||||
TODO:
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,144 +0,0 @@
|
|||
use tek::*;
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
Tui::run(Arc::new(RwLock::new(Demo::new())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Demo<E: Engine> {
|
||||
index: usize,
|
||||
items: Vec<Box<dyn Render<Engine = E>>>
|
||||
}
|
||||
|
||||
impl Demo<Tui> {
|
||||
fn new () -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
items: vec![
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Stopped),
|
||||
//focused: true
|
||||
//}),
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Rolling),
|
||||
//focused: false
|
||||
//}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for Demo<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> dyn Render<Engine = Tui> {
|
||||
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
||||
Align::Center(Layers::new(move|add|{
|
||||
|
||||
add(&Background(Color::Rgb(0,128,128)))?;
|
||||
|
||||
add(&Outset::XY(1, 1, Stack::down(|add|{
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,96,0)))?;
|
||||
add(&Border(Square(border_style)))?;
|
||||
add(&Outset::XY(2, 1, "..."))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,64,0)))?;
|
||||
add(&Border(Lozenge(border_style)))?;
|
||||
add(&Outset::XY(4, 2, "---"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(96,64,0)))?;
|
||||
add(&Border(SquareBold(border_style)))?;
|
||||
add(&Outset::XY(6, 3, "~~~"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
Ok(())
|
||||
})).debug())?;
|
||||
|
||||
Ok(())
|
||||
|
||||
}))
|
||||
//Align::Center(Outset::X(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Outset::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))
|
||||
//}))
|
||||
//})))
|
||||
|
||||
//Align::Y(Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Outset::X(1, Align::Center(Stack::down(|add|{
|
||||
//add(&Align::X(Outset::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))?;
|
||||
//Ok(())
|
||||
//})))))
|
||||
//}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle<Tui> for Demo<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
use KeyCode::{PageUp, PageDown};
|
||||
match from.event() {
|
||||
key_expr!(PageUp) => {
|
||||
self.index = (self.index + 1) % self.items.len();
|
||||
},
|
||||
key_expr!(PageDown) => {
|
||||
self.index = if self.index > 1 {
|
||||
self.index - 1
|
||||
} else {
|
||||
self.items.len() - 1
|
||||
};
|
||||
},
|
||||
_ => return Ok(None)
|
||||
}
|
||||
Ok(Some(true))
|
||||
}
|
||||
}
|
||||
|
||||
//lisp!(CONTENT Demo (LET
|
||||
//(BORDER-STYLE (STYLE (FG (RGB 0 0 0))))
|
||||
//(BG-COLOR-0 (RGB 0 128 128))
|
||||
//(BG-COLOR-1 (RGB 128 96 0))
|
||||
//(BG-COLOR-2 (RGB 128 64 0))
|
||||
//(BG-COLOR-3 (RGB 96 64 0))
|
||||
//(CENTER (LAYERS
|
||||
//(BACKGROUND BG-COLOR-0)
|
||||
//(OUTSET-XY 1 1 (SPLIT-DOWN
|
||||
//(LAYERS (BACKGROUND BG-COLOR-1)
|
||||
//(BORDER SQUARE BORDER-STYLE)
|
||||
//(OUTSET-XY 2 1 "..."))
|
||||
//(LAYERS (BACKGROUND BG-COLOR-2)
|
||||
//(BORDER LOZENGE BORDER-STYLE)
|
||||
//(OUTSET-XY 4 2 "---"))
|
||||
//(LAYERS (BACKGROUND BG-COLOR-3)
|
||||
//(BORDER SQUARE-BOLD BORDER-STYLE)
|
||||
//(OUTSET-XY 2 1 "~~~"))))))))
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
use tek::*;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
Tui::run(Arc::new(RwLock::new(BspDemo(Default::default()))))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct BspDemo<E: Engine>(std::marker::PhantomData<E>);
|
||||
|
||||
render!(<Tui>|self:BspDemo<Tui>|Fill::wh(Align::c(
|
||||
Bsp::n(Bsp::s(Bsp::e(Bsp::w("00", "11"), "22"), "33"), "44")
|
||||
)));
|
||||
|
||||
impl Handle<Tui> for BspDemo<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
use tek::*;
|
||||
|
||||
struct ExamplePhrases(Vec<Arc<RwLock<Phrase>>>);
|
||||
|
||||
impl HasPhrases for ExamplePhrases {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.0
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
let mut phrases = ExamplePhrases(vec![]);
|
||||
PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
(mixer
|
||||
(track
|
||||
(name "Drums")
|
||||
(sampler
|
||||
(dir "/home/user/Lab/Music/pak")
|
||||
(sample (midi 34) (name "808 D") (file "808.wav"))))
|
||||
(track
|
||||
(name "Lead")
|
||||
(lv2
|
||||
(name "Odin2")
|
||||
(path "file:///home/user/.lv2/Odin2.lv2"))
|
||||
(gain 0.0)))
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
(arranger
|
||||
(track
|
||||
(name "Drums")
|
||||
(phrase
|
||||
(name "4 kicks")
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:00 (36 128))
|
||||
(:04 (36 100))
|
||||
(:08 (36 100))
|
||||
(:12 (36 100))))
|
||||
(track
|
||||
(name "Bass")
|
||||
(phrase
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:04 (36 100))
|
||||
(:12 (36 100)))))
|
||||
|
|
@ -1 +0,0 @@
|
|||
mod sampler; pub(crate) use sampler::*;
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
mod arranger_command; pub(crate) use self::arranger_command::*;
|
||||
mod arranger_scene; pub(crate) use self::arranger_scene::*;
|
||||
mod arranger_select; pub(crate) use self::arranger_select::*;
|
||||
mod arranger_track; pub(crate) use self::arranger_track::*;
|
||||
mod arranger_mode; pub(crate) use self::arranger_mode::*;
|
||||
mod arranger_v; #[allow(unused)] pub(crate) use self::arranger_v::*;
|
||||
mod arranger_h;
|
||||
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerTui {
|
||||
jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PoolModel,
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
pub scenes: Vec<ArrangerScene>,
|
||||
pub splits: [u16;2],
|
||||
pub selected: ArrangerSelection,
|
||||
pub mode: ArrangerMode,
|
||||
pub color: ItemPalette,
|
||||
pub size: Measure<Tui>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub editor: MidiEditorModel,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
impl ArrangerTui {
|
||||
pub fn selected (&self) -> ArrangerSelection {
|
||||
self.selected
|
||||
}
|
||||
pub fn selected_mut (&mut self) -> &mut ArrangerSelection {
|
||||
&mut self.selected
|
||||
}
|
||||
pub fn activate (&mut self) -> Usually<()> {
|
||||
if let ArrangerSelection::Scene(s) = self.selected {
|
||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||
let phrase = self.scenes[s].clips[t].clone();
|
||||
if track.player.play_phrase.is_some() || phrase.is_some() {
|
||||
track.player.enqueue_next(phrase.as_ref());
|
||||
}
|
||||
}
|
||||
if self.clock().is_stopped() {
|
||||
self.clock().play_from(Some(0))?;
|
||||
}
|
||||
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes[s].clips[t].clone();
|
||||
self.tracks[t].player.enqueue_next(phrase.as_ref());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
pub fn selected_phrase (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||
}
|
||||
pub fn toggle_loop (&mut self) {
|
||||
if let Some(phrase) = self.selected_phrase() {
|
||||
phrase.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
pub fn randomize_color (&mut self) {
|
||||
match self.selected {
|
||||
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
||||
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
||||
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
||||
ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemPalette::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
from_jack!(|jack| ArrangerTui {
|
||||
let clock = ClockModel::from(jack);
|
||||
let phrase = Arc::new(RwLock::new(MidiClip::new(
|
||||
"New", true, 4 * clock.timebase.ppq.get() as usize,
|
||||
None, Some(ItemColor::random().into())
|
||||
)));
|
||||
Self {
|
||||
clock,
|
||||
phrases: (&phrase).into(),
|
||||
editor: (&phrase).into(),
|
||||
selected: ArrangerSelection::Clip(0, 0),
|
||||
scenes: vec![],
|
||||
tracks: vec![],
|
||||
color: TuiTheme::bg().into(),
|
||||
mode: ArrangerMode::V(1),
|
||||
size: Measure::new(),
|
||||
splits: [12, 20],
|
||||
midi_buf: vec![vec![];65536],
|
||||
note_buf: vec![],
|
||||
perf: PerfModel::default(),
|
||||
jack: jack.clone(),
|
||||
}
|
||||
});
|
||||
impl ArrangerTui {
|
||||
fn render_mode (state: &Self) -> impl Render<Tui> + use<'_> {
|
||||
match state.mode {
|
||||
ArrangerMode::H => todo!("horizontal arranger"),
|
||||
ArrangerMode::V(factor) => Self::render_mode_v(state, factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(<Tui>|self: ArrangerTui|{
|
||||
let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling()));
|
||||
let transport = TransportView::from((self, Some(ItemPalette::from(TuiTheme::g(96))), true));
|
||||
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
|
||||
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
|
||||
let with_pool = |x|Split::left(false, pool_size, PoolView(&self.phrases), x);
|
||||
let status = ArrangerStatus::from(self);
|
||||
let with_editbar = |x|Tui::split_n(false, 1, MidiEditStatus(&self.editor), x);
|
||||
let with_status = |x|Tui::split_n(false, 2, status, x);
|
||||
let with_size = |x|lay!([&self.size, x]);
|
||||
let arranger = ||lay!(|add|{
|
||||
let color = self.color;
|
||||
add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?;
|
||||
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
|
||||
add(&Self::render_mode(self))
|
||||
});
|
||||
with_size(with_status(with_editbar(with_pool(with_transport(col!([
|
||||
Fill::w(Fixed::h(20, arranger())),
|
||||
Fill::wh(&self.editor),
|
||||
]))))))
|
||||
});
|
||||
audio!(|self: ArrangerTui, client, scope|{
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
// Update transport clock
|
||||
if Control::Quit == ClockAudio(self).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
// Update MIDI sequencers
|
||||
let tracks = &mut self.tracks;
|
||||
let note_buf = &mut self.note_buf;
|
||||
let midi_buf = &mut self.midi_buf;
|
||||
if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
// FIXME: one of these per playing track
|
||||
//self.now.set(0.);
|
||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
//let phrase = self.scenes.get(s).map(|scene|scene.clips.get(t));
|
||||
//if let Some(Some(Some(phrase))) = phrase {
|
||||
//if let Some(track) = self.tracks().get(t) {
|
||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now.set(now);
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
return Control::Continue
|
||||
});
|
||||
has_clock!(|self: ArrangerTui|&self.clock);
|
||||
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
|
||||
has_editor!(|self: ArrangerTui|self.editor);
|
||||
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
|
||||
|
|
@ -1,339 +0,0 @@
|
|||
use crate::*;
|
||||
use ClockCommand::{Play, Pause};
|
||||
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
||||
|
||||
#[derive(Clone, Debug)] pub enum ArrangerCommand {
|
||||
History(isize),
|
||||
Color(ItemPalette),
|
||||
Clock(ClockCommand),
|
||||
Scene(ArrangerSceneCommand),
|
||||
Track(ArrangerTrackCommand),
|
||||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Phrases(PoolCommand),
|
||||
Editor(PhraseCommand),
|
||||
StopAll,
|
||||
Clear,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerTrackCommand {
|
||||
Add,
|
||||
Delete(usize),
|
||||
Stop(usize),
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
SetColor(usize, ItemPalette),
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerSceneCommand {
|
||||
Enqueue(usize),
|
||||
Add,
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
SetColor(usize, ItemPalette),
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerClipCommand {
|
||||
Get(usize, usize),
|
||||
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
||||
Enqueue(usize, usize),
|
||||
Edit(Option<Arc<RwLock<MidiClip>>>),
|
||||
SetLoop(usize, usize, bool),
|
||||
SetColor(usize, usize, ItemPalette),
|
||||
}
|
||||
|
||||
input_to_command!(ArrangerCommand: <Tui>|state: ArrangerTui, input|match input.event() {
|
||||
key_pat!(Char('u')) => Self::History(-1),
|
||||
key_pat!(Char('U')) => Self::History(1),
|
||||
// TODO: k: toggle on-screen keyboard
|
||||
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
|
||||
// Transport: Play/pause
|
||||
key_pat!(Char(' ')) =>
|
||||
Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
|
||||
// Transport: Play from start or rewind to start
|
||||
key_pat!(Shift-Char(' ')) =>
|
||||
Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
|
||||
key_pat!(Char('e')) =>
|
||||
Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
|
||||
key_pat!(Ctrl-Left) =>
|
||||
Self::Scene(ArrangerSceneCommand::Add),
|
||||
key_pat!(Ctrl-Char('t')) =>
|
||||
Self::Track(ArrangerTrackCommand::Add),
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
key_pat!(Tab) =>
|
||||
Self::Phrases(PoolCommand::Show(!state.phrases.visible)),
|
||||
_ => {
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Selected;
|
||||
use ArrangerSceneCommand as Scene;
|
||||
use ArrangerTrackCommand as Track;
|
||||
use ArrangerClipCommand as Clip;
|
||||
let t_len = state.tracks.len();
|
||||
let s_len = state.scenes.len();
|
||||
match state.selected() {
|
||||
Selected::Clip(t, s) => match input.event() {
|
||||
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
|
||||
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
|
||||
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
|
||||
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
|
||||
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
|
||||
key_pat!(Up) => Some(Cmd::Select(
|
||||
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
|
||||
key_pat!(Down) => Some(Cmd::Select(
|
||||
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
|
||||
key_pat!(Left) => Some(Cmd::Select(
|
||||
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
|
||||
key_pat!(Right) => Some(Cmd::Select(
|
||||
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Scene(s) => match input.event() {
|
||||
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
|
||||
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
|
||||
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
|
||||
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
|
||||
|
||||
key_pat!(Up) => Some(
|
||||
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
|
||||
key_pat!(Left) =>
|
||||
return None,
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Clip(0, s))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Track(t) => match input.event() {
|
||||
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
|
||||
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
|
||||
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
|
||||
|
||||
key_pat!(Up) =>
|
||||
return None,
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Clip(t, 0))),
|
||||
key_pat!(Left) => Some(
|
||||
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Mix => match input.event() {
|
||||
key_pat!(Delete) => Some(Cmd::Clear),
|
||||
key_pat!(Char('0')) => Some(Cmd::StopAll),
|
||||
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
|
||||
|
||||
key_pat!(Up) =>
|
||||
return None,
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Scene(0))),
|
||||
key_pat!(Left) =>
|
||||
return None,
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Track(0))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
}
|
||||
}.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
|
||||
Some(Self::Editor(command))
|
||||
} else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) {
|
||||
Some(Self::Phrases(command))
|
||||
} else {
|
||||
None
|
||||
})?
|
||||
});
|
||||
fn to_arrangement_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Selected;
|
||||
use ArrangerSceneCommand as Scene;
|
||||
use ArrangerTrackCommand as Track;
|
||||
use ArrangerClipCommand as Clip;
|
||||
let t_len = state.tracks.len();
|
||||
let s_len = state.scenes.len();
|
||||
match state.selected() {
|
||||
Selected::Clip(t, s) => match input.event() {
|
||||
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
|
||||
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
|
||||
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
|
||||
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
|
||||
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
||||
|
||||
key_pat!(Up) => Some(Cmd::Select(
|
||||
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
|
||||
key_pat!(Down) => Some(Cmd::Select(
|
||||
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
|
||||
key_pat!(Left) => Some(Cmd::Select(
|
||||
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
|
||||
key_pat!(Right) => Some(Cmd::Select(
|
||||
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Scene(s) => match input.event() {
|
||||
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
|
||||
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
|
||||
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
|
||||
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
|
||||
|
||||
key_pat!(Up) => Some(
|
||||
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
|
||||
key_pat!(Left) =>
|
||||
None,
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Clip(0, s))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Track(t) => match input.event() {
|
||||
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
|
||||
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
|
||||
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
|
||||
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
|
||||
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
|
||||
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
|
||||
|
||||
key_pat!(Up) =>
|
||||
None,
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Clip(t, 0))),
|
||||
key_pat!(Left) => Some(
|
||||
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
Selected::Mix => match input.event() {
|
||||
key_pat!(Delete) => Some(Cmd::Clear),
|
||||
key_pat!(Char('0')) => Some(Cmd::StopAll),
|
||||
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
|
||||
|
||||
key_pat!(Up) =>
|
||||
None,
|
||||
key_pat!(Down) => Some(
|
||||
Cmd::Select(Selected::Scene(0))),
|
||||
key_pat!(Left) =>
|
||||
None,
|
||||
key_pat!(Right) => Some(
|
||||
Cmd::Select(Selected::Track(0))),
|
||||
|
||||
_ => None
|
||||
},
|
||||
}
|
||||
}
|
||||
command!(|self: ArrangerCommand, state: ArrangerTui|match self {
|
||||
Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene),
|
||||
Self::Track(cmd) => cmd.execute(state)?.map(Self::Track),
|
||||
Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip),
|
||||
Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Self::Editor),
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
||||
Self::Zoom(_) => { todo!(); },
|
||||
Self::Select(selected) => {
|
||||
*state.selected_mut() = selected;
|
||||
None
|
||||
},
|
||||
Self::Color(palette) => {
|
||||
let old = state.color;
|
||||
state.color = palette;
|
||||
Some(Self::Color(old))
|
||||
},
|
||||
Self::Phrases(cmd) => {
|
||||
let mut default = |cmd: PoolCommand|{
|
||||
cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases))
|
||||
};
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
PoolCommand::Select(_) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
// reload phrase in editor to update color
|
||||
PoolCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
_ => default(cmd)?
|
||||
}
|
||||
},
|
||||
Self::History(_) => { todo!() },
|
||||
Self::StopAll => {
|
||||
for track in 0..state.tracks.len() {
|
||||
state.tracks[track].player.enqueue_next(None);
|
||||
}
|
||||
None
|
||||
},
|
||||
Self::Clear => { todo!() },
|
||||
});
|
||||
command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self {
|
||||
Self::SetColor(index, color) => {
|
||||
let old = state.tracks[index].color;
|
||||
state.tracks[index].color = color;
|
||||
Some(Self::SetColor(index, old))
|
||||
},
|
||||
Self::Stop(track) => {
|
||||
state.tracks[track].player.enqueue_next(None);
|
||||
None
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
command!(|self: ArrangerSceneCommand, state: ArrangerTui|match self {
|
||||
Self::Delete(index) => {
|
||||
state.scene_del(index);
|
||||
None
|
||||
},
|
||||
Self::SetColor(index, color) => {
|
||||
let old = state.scenes[index].color;
|
||||
state.scenes[index].color = color;
|
||||
Some(Self::SetColor(index, old))
|
||||
},
|
||||
Self::Enqueue(scene) => {
|
||||
for track in 0..state.tracks.len() {
|
||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||
}
|
||||
None
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
command!(|self: ArrangerClipCommand, state: ArrangerTui|match self {
|
||||
Self::Get(track, scene) => { todo!() },
|
||||
Self::Put(track, scene, phrase) => {
|
||||
let old = state.scenes[scene].clips[track].clone();
|
||||
state.scenes[scene].clips[track] = phrase;
|
||||
Some(Self::Put(track, scene, old))
|
||||
},
|
||||
Self::Enqueue(track, scene) => {
|
||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||
None
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
// TODO
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use crate::*;
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
/// Tracks are columns
|
||||
V(usize),
|
||||
/// Tracks are rows
|
||||
H,
|
||||
}
|
||||
render!(<Tui>|self: ArrangerMode|{});
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangerMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::H => Self::V(1),
|
||||
Self::V(1) => Self::V(2),
|
||||
Self::V(2) => Self::V(2),
|
||||
Self::V(0) => Self::H,
|
||||
Self::V(_) => Self::V(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
|
||||
Ok(Some([0.into(),0.into()].into()))
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
use crate::*;
|
||||
impl ArrangerTui {
|
||||
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerScene>
|
||||
{
|
||||
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
|
||||
let scene = ArrangerScene {
|
||||
name: Arc::new(name.into()),
|
||||
clips: vec![None;self.tracks.len()],
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
};
|
||||
self.scenes.push(scene);
|
||||
let index = self.scenes.len() - 1;
|
||||
Ok(&mut self.scenes[index])
|
||||
}
|
||||
pub fn scene_del (&mut self, index: usize) {
|
||||
todo!("delete scene");
|
||||
}
|
||||
fn scene_default_name (&self) -> String {
|
||||
format!("S{:3>}", self.scenes.len() + 1)
|
||||
}
|
||||
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get(s))
|
||||
}
|
||||
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
||||
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
|
||||
/// Name of scene
|
||||
pub(crate) name: Arc<RwLock<String>>,
|
||||
/// Clips in scene, one per track
|
||||
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
/// Identifying color of scene
|
||||
pub(crate) color: ItemPalette,
|
||||
}
|
||||
impl ArrangerScene {
|
||||
pub fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
|
||||
&self.clips
|
||||
}
|
||||
pub fn color (&self) -> ItemPalette {
|
||||
self.color
|
||||
}
|
||||
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
|
||||
let mut total = 0;
|
||||
if factor == 0 {
|
||||
scenes.iter().map(|scene|{
|
||||
let pulses = scene.pulses().max(PPQ);
|
||||
total += pulses;
|
||||
(pulses, total - pulses)
|
||||
}).collect()
|
||||
} else {
|
||||
(0..=scenes.len()).map(|i|{
|
||||
(factor*PPQ, factor*PPQ*i)
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
|
||||
}
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips().iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// Returns true if all phrases in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
||||
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(clip) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(phrase))) = track.player().play_phrase() {
|
||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
use crate::*;
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
/// Represents the current user selection in the arranger
|
||||
pub enum ArrangerSelection {
|
||||
/// The whole mix is selected
|
||||
Mix,
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
Clip(usize, usize),
|
||||
}
|
||||
/// Focus identification methods
|
||||
impl ArrangerSelection {
|
||||
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||
pub fn description (
|
||||
&self,
|
||||
tracks: &[ArrangerTrack],
|
||||
scenes: &[ArrangerScene],
|
||||
) -> String {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => "Everything".to_string(),
|
||||
Self::Track(t) => match tracks.get(*t) {
|
||||
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()),
|
||||
None => "T??".into(),
|
||||
},
|
||||
Self::Scene(s) => match scenes.get(*s) {
|
||||
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
|
||||
None => "S??".into(),
|
||||
},
|
||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||
None => format!("T{t} S{s}: Empty")
|
||||
},
|
||||
_ => format!("T{t} S{s}: Empty"),
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(t, _) => Some(*t),
|
||||
Track(t) => Some(*t),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use ArrangerSelection::*;
|
||||
match self {
|
||||
Clip(_, s) => Some(*s),
|
||||
Scene(s) => Some(*s),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
use crate::*;
|
||||
impl ArrangerTui {
|
||||
pub fn track_next_name (&self) -> String {
|
||||
format!("T{}", self.tracks.len() + 1)
|
||||
}
|
||||
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<&mut ArrangerTrack>
|
||||
{
|
||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string());
|
||||
let track = ArrangerTrack {
|
||||
width: name.len() + 2,
|
||||
name: Arc::new(name.into()),
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
player: MidiPlayer::from(&self.clock),
|
||||
};
|
||||
self.tracks.push(track);
|
||||
let index = self.tracks.len() - 1;
|
||||
Ok(&mut self.tracks[index])
|
||||
}
|
||||
pub fn track_del (&mut self, index: usize) {
|
||||
self.tracks.remove(index);
|
||||
for scene in self.scenes.iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)] pub struct ArrangerTrack {
|
||||
/// Name of track
|
||||
pub name: Arc<RwLock<String>>,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// Identifying color of track
|
||||
pub color: ItemPalette,
|
||||
/// MIDI player state
|
||||
pub player: MidiPlayer,
|
||||
}
|
||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
||||
has_player!(|self:ArrangerTrack|self.player);
|
||||
impl ArrangerTrack {
|
||||
pub fn widths (tracks: &[Self]) -> Vec<(usize, usize)> {
|
||||
let mut widths = vec![];
|
||||
let mut total = 0;
|
||||
for track in tracks.iter() {
|
||||
let width = track.width;
|
||||
widths.push((width, total));
|
||||
total += width;
|
||||
}
|
||||
widths.push((0, total));
|
||||
widths
|
||||
}
|
||||
pub fn with_widths (tracks: &[ArrangerTrack])
|
||||
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
||||
{
|
||||
let mut x = 0;
|
||||
tracks.iter().enumerate().map(move |(index, track)|{
|
||||
let data = (index, track, x, x + track.width);
|
||||
x += track.width;
|
||||
data
|
||||
})
|
||||
}
|
||||
/// Name of track
|
||||
pub fn name (&self) -> &Arc<RwLock<String>> {
|
||||
&self.name
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize {
|
||||
&mut self.width
|
||||
}
|
||||
/// Identifying color of track
|
||||
pub fn color (&self) -> ItemPalette {
|
||||
self.color
|
||||
}
|
||||
fn longest_name (tracks: &[Self]) -> usize {
|
||||
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
|
||||
}
|
||||
pub const MIN_WIDTH: usize = 6;
|
||||
fn width_inc (&mut self) {
|
||||
*self.width_mut() += 1;
|
||||
}
|
||||
fn width_dec (&mut self) {
|
||||
if self.width() > Self::MIN_WIDTH {
|
||||
*self.width_mut() -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Hosts the JACK callback for a collection of tracks
|
||||
pub struct TracksAudio<'a>(
|
||||
// Track collection
|
||||
pub &'a mut [ArrangerTrack],
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
);
|
||||
impl Audio for TracksAudio<'_> {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buffer = &mut self.1;
|
||||
let output_buffer = &mut self.2;
|
||||
for track in model.iter_mut() {
|
||||
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
use crate::*;
|
||||
mod v_clips; pub(crate) use self::v_clips::*;
|
||||
mod v_cursor; pub(crate) use self::v_cursor::*;
|
||||
mod v_head; pub(crate) use self::v_head::*;
|
||||
mod v_io; pub(crate) use self::v_io::*;
|
||||
mod v_sep; pub(crate) use self::v_sep::*;
|
||||
const HEADER_H: u16 = 5;
|
||||
const SCENES_W_OFFSET: u16 = 3;
|
||||
impl ArrangerTui {
|
||||
pub fn render_mode_v (state: &ArrangerTui, factor: usize) -> impl Render<Tui> + use<'_> {
|
||||
lay!([
|
||||
ArrangerVColSep::from(state),
|
||||
ArrangerVRowSep::from((state, factor)),
|
||||
col!([
|
||||
ArrangerVHead::from(state),
|
||||
ArrangerVIns::from(state),
|
||||
ArrangerVClips::from((state, factor)),
|
||||
ArrangerVOuts::from(state),
|
||||
]),
|
||||
ArrangerVCursor::from((state, factor)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct ArrangerVClips<'a> {
|
||||
size: &'a Measure<Tui>,
|
||||
scenes: &'a Vec<ArrangerScene>,
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
from!(<'a>|args:(&'a ArrangerTui, usize)|ArrangerVClips<'a> = Self {
|
||||
size: &args.0.size,
|
||||
scenes: &args.0.scenes,
|
||||
tracks: &args.0.tracks,
|
||||
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVClips<'a>|Fill::wh(
|
||||
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
|
||||
Self::format_scene(self.tracks, scene, pulses)
|
||||
})
|
||||
));
|
||||
|
||||
impl<'a> ArrangerVClips<'a> {
|
||||
fn format_scene (
|
||||
tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize
|
||||
) -> impl Render<Tui> + use<'a> {
|
||||
let height = 1.max((pulses / PPQ) as u16);
|
||||
let playing = scene.is_playing(tracks);
|
||||
Fixed::h(height, row!([
|
||||
Tui::bg(scene.color.base.rgb,
|
||||
if playing { "▶ " } else { " " }),
|
||||
Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb,
|
||||
Tui::grow_x(1, Tui::bold(true, scene.name.read().unwrap().as_str()))),
|
||||
row!((index, track, x1, x2) in ArrangerTrack::with_widths(tracks) => {
|
||||
Self::format_clip(scene, index, track, (x2 - x1) as u16, height)
|
||||
})])
|
||||
)
|
||||
}
|
||||
fn format_clip (
|
||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
||||
) -> impl Render<Tui> + use<'a> {
|
||||
Fixed::wh(w, h, Layers::new(move |add|{
|
||||
if let Some(Some(phrase)) = scene.clips.get(index) {
|
||||
let mut bg = TuiTheme::border_bg();
|
||||
let name = &(phrase as &Arc<RwLock<MidiClip>>).read().unwrap().name.to_string();
|
||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||
let color = phrase.read().unwrap().color;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
add(&Tui::bg(bg,
|
||||
Tui::push_x(1, Fixed::w(w, &name.as_str()[0..max_w])))
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct ArrangerVCursor {
|
||||
cols: Vec<(usize, usize)>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
color: ItemPalette,
|
||||
reticle: Reticle,
|
||||
selected: ArrangerSelection,
|
||||
scenes_w: u16,
|
||||
}
|
||||
|
||||
from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self {
|
||||
cols: ArrangerTrack::widths(&args.0.tracks),
|
||||
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
|
||||
selected: args.0.selected(),
|
||||
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
|
||||
color: args.0.color,
|
||||
reticle: Reticle(Style {
|
||||
fg: Some(args.0.color.lighter.rgb),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM
|
||||
}),
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let focused = true;
|
||||
let selected = self.selected;
|
||||
let get_track_area = |t: usize| [
|
||||
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
||||
self.cols[t].0 as u16, area.h(),
|
||||
];
|
||||
let get_scene_area = |s: usize| [
|
||||
area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16,
|
||||
area.w(), (self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let get_clip_area = |t: usize, s: usize| [
|
||||
(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1),
|
||||
HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16,
|
||||
self.cols[t].0 as u16 + 2,
|
||||
(self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let mut track_area: Option<[u16;4]> = None;
|
||||
let mut scene_area: Option<[u16;4]> = None;
|
||||
let mut clip_area: Option<[u16;4]> = None;
|
||||
let area = match selected {
|
||||
ArrangerSelection::Mix => area,
|
||||
ArrangerSelection::Track(t) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
scene_area = Some(get_scene_area(s));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
scene_area = Some(get_scene_area(s));
|
||||
clip_area = Some(get_clip_area(t, s));
|
||||
area
|
||||
},
|
||||
};
|
||||
let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0);
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], bg);
|
||||
to.fill_fg([x + width, y, 1, height], bg);
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||
}
|
||||
Ok(if focused {
|
||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||
else if let Some(track_area) = track_area { track_area.clip_h(HEADER_H) }
|
||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
|
||||
else { area.clip_w(self.scenes_w).clip_h(HEADER_H) }, &self.reticle)?
|
||||
})
|
||||
}));
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct ArrangerVHead<'a> {
|
||||
scenes_w: u16,
|
||||
timebase: &'a Arc<Timebase>,
|
||||
current: &'a Arc<Moment>,
|
||||
tracks: &'a [ArrangerTrack],
|
||||
}
|
||||
|
||||
from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A
|
||||
tracks: &state.tracks,
|
||||
timebase: state.clock().timebase(),
|
||||
current: &state.clock().playhead,
|
||||
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVHead<'a>|Tui::push_x(self.scenes_w, row!(
|
||||
(_, track, x1, x2) in ArrangerTrack::with_widths(self.tracks) => {
|
||||
let (w, h) = (ArrangerTrack::MIN_WIDTH.max(x2 - x1), HEADER_H);
|
||||
let color = track.color();
|
||||
fn row <T: Render<Tui>> (color: ItemPalette, field: &T) -> impl Render<Tui> + use<'_, T> {
|
||||
row!([
|
||||
Tui::fg(color.light.rgb, "▎"),
|
||||
Tui::fg(color.lightest.rgb, field)
|
||||
])
|
||||
}
|
||||
Tui::bg(color.base.rgb, Tui::min_xy(w as u16, h, Fixed::wh(w as u16, 5, col!([
|
||||
row(color, &Self::format_name(track, w)),
|
||||
row(color, &Self::format_input(track)?),
|
||||
row(color, &Self::format_output(track)?),
|
||||
row(color, &Self::format_elapsed(track, self.timebase)),
|
||||
row(color, &Self::format_until_next(track, self.current)),
|
||||
]))))
|
||||
}
|
||||
)));
|
||||
|
||||
impl ArrangerVHead<'_> {
|
||||
/// name and width of track
|
||||
fn format_name (track: &ArrangerTrack, _w: usize) -> impl Render<Tui> {
|
||||
let name = track.name().read().unwrap().clone();
|
||||
Tui::bold(true, Tui::fg(track.color.lightest.rgb, name))
|
||||
}
|
||||
/// input port
|
||||
fn format_input (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
|
||||
Ok(format!(">{}", track.player.midi_ins().first().map(|port|port.short_name())
|
||||
.transpose()?.unwrap_or("?".into())))
|
||||
}
|
||||
/// output port
|
||||
fn format_output (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
|
||||
Ok(format!("<{}", track.player.midi_outs().first().map(|port|port.short_name())
|
||||
.transpose()?.unwrap_or("?".into())))
|
||||
}
|
||||
/// beats elapsed
|
||||
fn format_elapsed (track: &ArrangerTrack, timebase: &Arc<Timebase>) -> impl Render<Tui> {
|
||||
let mut result = String::new();
|
||||
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = track.player.pulses_since_start().unwrap();
|
||||
let elapsed = timebase.format_beats_1_short(
|
||||
(elapsed as usize % length) as f64
|
||||
);
|
||||
result = format!("+{elapsed:>}")
|
||||
}
|
||||
result
|
||||
}
|
||||
/// beats until switchover
|
||||
fn format_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
|
||||
-> Option<impl Render<Tui>>
|
||||
{
|
||||
let timebase = ¤t.timebase;
|
||||
let mut result = String::new();
|
||||
if let Some((t, _)) = track.player.next_phrase().as_ref() {
|
||||
let target = t.pulse.get();
|
||||
let current = current.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
result = format!("-{:>}", timebase.format_beats_0_short(remaining))
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct ArrangerVIns<'a> {
|
||||
size: &'a Measure<Tui>,
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
}
|
||||
|
||||
from!(<'a>|args: &'a ArrangerTui|ArrangerVIns<'a> = Self {
|
||||
size: &args.size,
|
||||
tracks: &args.tracks,
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVIns<'a>|());
|
||||
|
||||
pub struct ArrangerVOuts<'a> {
|
||||
size: &'a Measure<Tui>,
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
}
|
||||
|
||||
from!(<'a>|args: &'a ArrangerTui|ArrangerVOuts<'a> = Self {
|
||||
size: &args.size,
|
||||
tracks: &args.tracks,
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVOuts<'a>|());
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
pub struct ArrangerVColSep {
|
||||
fg: Color,
|
||||
cols: Vec<(usize, usize)>,
|
||||
scenes_w: u16
|
||||
}
|
||||
|
||||
from!(|state:&ArrangerTui|ArrangerVColSep = Self {
|
||||
fg: TuiTheme::separator_fg(false),
|
||||
cols: ArrangerTrack::widths(&state.tracks),
|
||||
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{
|
||||
let style = Some(Style::default().fg(self.fg));
|
||||
Ok(for x in self.cols.iter().map(|col|col.1) {
|
||||
let x = self.scenes_w + to.area().x() + x as u16;
|
||||
for y in to.area().y()..to.area().y2() {
|
||||
to.blit(&"▎", x, y, style);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
pub struct ArrangerVRowSep {
|
||||
fg: Color,
|
||||
rows: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
from!(|args:(&ArrangerTui, usize)|ArrangerVRowSep = Self {
|
||||
fg: TuiTheme::separator_fg(false),
|
||||
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
|
||||
});
|
||||
|
||||
render!(<Tui>|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{
|
||||
Ok(for y in self.rows.iter().map(|row|row.1) {
|
||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||
if y >= to.buffer.area.height { break }
|
||||
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
||||
if x < to.buffer.area.x && y < to.buffer.area.y {
|
||||
let cell = to.buffer.get_mut(x, y);
|
||||
cell.modifier = Modifier::UNDERLINED;
|
||||
cell.underline_color = self.fg;
|
||||
}
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
|
@ -1 +0,0 @@
|
|||
use crate::*;
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
pub(crate) use std::error::Error;
|
||||
|
||||
pub(crate) mod color;
|
||||
pub(crate) use color::*;
|
||||
pub use color::*;
|
||||
|
||||
pub(crate) mod command; pub(crate) use command::*;
|
||||
pub(crate) mod engine; pub(crate) use engine::*;
|
||||
pub(crate) mod focus; pub(crate) use focus::*;
|
||||
pub(crate) mod input; pub(crate) use input::*;
|
||||
pub(crate) mod output; pub(crate) use output::*;
|
||||
|
||||
pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
|
||||
pub(crate) use Ordering::Relaxed;
|
||||
|
||||
pub use self::{
|
||||
engine::Engine,
|
||||
input::Handle,
|
||||
output::Render
|
||||
};
|
||||
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// Define test modules.
|
||||
#[macro_export] macro_rules! testmod {
|
||||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||
}
|
||||
|
||||
/// Prototypal case of implementor macro.
|
||||
/// Saves 4loc per data pats.
|
||||
#[macro_export] macro_rules! from {
|
||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
||||
fn from ($state:$Source) -> Self { $cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
impl Gettable<bool> for AtomicBool {
|
||||
fn get (&self) -> bool { self.load(Relaxed) }
|
||||
}
|
||||
impl InteriorMutable<bool> for AtomicBool {
|
||||
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
|
||||
}
|
||||
impl Gettable<usize> for AtomicUsize {
|
||||
fn get (&self) -> usize { self.load(Relaxed) }
|
||||
}
|
||||
impl InteriorMutable<usize> for AtomicUsize {
|
||||
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
use crate::*;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
pub use ratatui::prelude::Color;
|
||||
|
||||
pub trait HasColor {
|
||||
fn color (&self) -> ItemColor;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_color {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn color (&$self) -> ItemColor { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A color in OKHSL and RGB representations.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemColor {
|
||||
pub okhsl: Okhsl<f32>,
|
||||
pub rgb: Color,
|
||||
}
|
||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemPalette {
|
||||
pub base: ItemColor,
|
||||
pub light: ItemColor,
|
||||
pub lighter: ItemColor,
|
||||
pub lightest: ItemColor,
|
||||
pub dark: ItemColor,
|
||||
pub darker: ItemColor,
|
||||
pub darkest: ItemColor,
|
||||
}
|
||||
from!(|okhsl: Okhsl<f32>|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||
from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||
impl ItemColor {
|
||||
pub fn random () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
||||
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_dark () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
||||
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.mix(Self::random(), distance)
|
||||
}
|
||||
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
||||
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
||||
self.okhsl.mix(other.okhsl, distance).into()
|
||||
}
|
||||
}
|
||||
from!(|base: Color|ItemPalette = Self::from(ItemColor::from(base)));
|
||||
from!(|base: ItemColor|ItemPalette = {
|
||||
let mut light = base.okhsl;
|
||||
light.lightness = (light.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
let mut lighter = light;
|
||||
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
let mut lightest = lighter;
|
||||
lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
|
||||
let mut dark = base.okhsl;
|
||||
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
||||
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
|
||||
let mut darker = dark;
|
||||
darker.lightness = (darker.lightness * 0.66).max(Okhsl::<f32>::min_lightness());
|
||||
darker.saturation = (darker.saturation * 0.66).max(Okhsl::<f32>::min_saturation());
|
||||
let mut darkest = darker;
|
||||
darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::<f32>::min_lightness());
|
||||
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
|
||||
|
||||
Self {
|
||||
base,
|
||||
light: light.into(),
|
||||
lighter: lighter.into(),
|
||||
lightest: lightest.into(),
|
||||
dark: dark.into(),
|
||||
darker: darker.into(),
|
||||
darkest: darkest.into(),
|
||||
}
|
||||
});
|
||||
impl ItemPalette {
|
||||
pub fn random () -> Self {
|
||||
ItemColor::random().into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
}
|
||||
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
||||
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
||||
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
||||
}
|
||||
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
||||
if let Color::Rgb(r, g, b) = color {
|
||||
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
||||
} else {
|
||||
unreachable!("only Color::Rgb is supported")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! command {
|
||||
(|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
||||
impl Command<$State> for $Command {
|
||||
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
|
||||
Ok($handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! input_to_command {
|
||||
($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||
impl InputToCommand<$Engine, $State> for $Command {
|
||||
fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option<Self> {
|
||||
Some($handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NextPrev {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub trait Execute<T> {
|
||||
fn command (&mut self, command: T) -> Perhaps<T>;
|
||||
}
|
||||
|
||||
pub trait Command<S>: Send + Sync + Sized {
|
||||
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
||||
}
|
||||
pub fn delegate <B, C: Command<S>, S> (
|
||||
cmd: C,
|
||||
wrap: impl Fn(C)->B,
|
||||
state: &mut S,
|
||||
) -> Perhaps<B> {
|
||||
Ok(cmd.execute(state)?.map(wrap))
|
||||
}
|
||||
|
||||
pub trait InputToCommand<E: Engine, S>: Command<S> + Sized {
|
||||
fn input_to_command (state: &S, input: &E::Input) -> Option<Self>;
|
||||
fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps<bool> {
|
||||
Ok(if let Some(command) = Self::input_to_command(state, input) {
|
||||
let _undo = command.execute(state)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
pub struct MenuBar<E: Engine, S, C: Command<S>> {
|
||||
pub menus: Vec<Menu<E, S, C>>,
|
||||
pub index: usize,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
|
||||
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
|
||||
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
|
||||
self.menus.push(menu);
|
||||
self
|
||||
}
|
||||
}
|
||||
pub struct Menu<E: Engine, S, C: Command<S>> {
|
||||
pub title: String,
|
||||
pub items: Vec<MenuItem<E, S, C>>,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
||||
pub fn new (title: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
title: title.as_ref().to_string(),
|
||||
items: vec![],
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
pub fn sep (mut self) -> Self {
|
||||
self.items.push(MenuItem::sep());
|
||||
self
|
||||
}
|
||||
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||
self
|
||||
}
|
||||
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
||||
self.items.push(MenuItem::off(hotkey, text));
|
||||
self
|
||||
}
|
||||
}
|
||||
pub enum MenuItem<E: Engine, S, C: Command<S>> {
|
||||
/// Unused.
|
||||
__(PhantomData<E>, PhantomData<S>),
|
||||
/// A separator. Skip it.
|
||||
Separator,
|
||||
/// A menu item with command, description and hotkey.
|
||||
Command(&'static str, &'static str, C),
|
||||
/// A menu item that can't be activated but has description and hotkey
|
||||
Disabled(&'static str, &'static str)
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
||||
pub fn sep () -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
Self::Command(hotkey, text, command)
|
||||
}
|
||||
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
|
||||
Self::Disabled(hotkey, text)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Platform backend.
|
||||
pub trait Engine: Send + Sync + Sized {
|
||||
/// Input event type
|
||||
type Input: Input<Self>;
|
||||
/// Result of handling input
|
||||
type Handled;
|
||||
/// Render target
|
||||
type Output: Output<Self>;
|
||||
/// Unit of length
|
||||
type Unit: Coordinate;
|
||||
/// Rectangle without offset
|
||||
type Size: Size<Self::Unit> + From<[Self::Unit;2]> + Debug + Copy;
|
||||
/// Rectangle with offset
|
||||
type Area: Area<Self::Unit> + From<[Self::Unit;4]> + Debug + Copy;
|
||||
/// Prepare before run
|
||||
fn setup (&mut self) -> Usually<()> { Ok(()) }
|
||||
/// True if done
|
||||
fn exited (&self) -> bool;
|
||||
/// Clean up after run
|
||||
fn teardown (&mut self) -> Usually<()> { Ok(()) }
|
||||
}
|
||||
|
||||
/// A UI component that can render itself as a [Render], and [Handle] input.
|
||||
pub trait Component<E: Engine>: Render<E> + Handle<E> {}
|
||||
|
||||
/// Everything that implements [Render] and [Handle] is a [Component].
|
||||
impl<E: Engine, C: Render<E> + Handle<E>> Component<E> for C {}
|
||||
|
||||
/// A component that can exit.
|
||||
pub trait Exit: Send {
|
||||
fn exited (&self) -> bool;
|
||||
fn exit (&mut self);
|
||||
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for [Component]s that can [Exit].
|
||||
pub trait ExitableComponent<E>: Exit + Component<E> where E: Engine {
|
||||
/// Perform type erasure for collecting heterogeneous components.
|
||||
fn boxed (self) -> Box<dyn ExitableComponent<E>> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// All [Components]s that implement [Exit] implement [ExitableComponent].
|
||||
impl<E: Engine, C: Component<E> + Exit> ExitableComponent<E> for C {}
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FocusState<T: Copy + Debug + PartialEq> {
|
||||
Focused(T),
|
||||
Entered(T),
|
||||
}
|
||||
|
||||
impl<T: Copy + Debug + PartialEq> FocusState<T> {
|
||||
pub fn inner (&self) -> T {
|
||||
match self {
|
||||
Self::Focused(inner) => *inner,
|
||||
Self::Entered(inner) => *inner,
|
||||
}
|
||||
}
|
||||
pub fn set_inner (&mut self, inner: T) {
|
||||
*self = match self {
|
||||
Self::Focused(_) => Self::Focused(inner),
|
||||
Self::Entered(_) => Self::Entered(inner),
|
||||
}
|
||||
}
|
||||
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
|
||||
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
|
||||
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
|
||||
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum FocusCommand<T: Send + Sync> {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next,
|
||||
Prev,
|
||||
Enter,
|
||||
Exit,
|
||||
Set(T)
|
||||
}
|
||||
|
||||
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
|
||||
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
|
||||
use FocusCommand::*;
|
||||
match self {
|
||||
Next => { state.focus_next(); },
|
||||
Prev => { state.focus_prev(); },
|
||||
Up => { state.focus_up(); },
|
||||
Down => { state.focus_down(); },
|
||||
Left => { state.focus_left(); },
|
||||
Right => { state.focus_right(); },
|
||||
Enter => { state.focus_enter(); },
|
||||
Exit => { state.focus_exit(); },
|
||||
Set(to) => { state.set_focused(to); },
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that have focusable subparts.
|
||||
pub trait HasFocus {
|
||||
type Item: Copy + PartialEq + Debug + Send + Sync;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item;
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item);
|
||||
/// Loop forward until a specific item is focused.
|
||||
fn focus_to (&mut self, to: Self::Item) {
|
||||
self.set_focused(to);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Run this on focus update
|
||||
fn focus_updated (&mut self) {}
|
||||
}
|
||||
|
||||
/// Trait for things that have enterable subparts.
|
||||
pub trait HasEnter: HasFocus {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool;
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool);
|
||||
/// Enter into the currently focused component
|
||||
fn focus_enter (&mut self) {
|
||||
self.set_entered(true);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Exit the currently entered component
|
||||
fn focus_exit (&mut self) {
|
||||
self.set_entered(false);
|
||||
self.focus_updated();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement directional navigation between focusable elements.
|
||||
pub trait FocusGrid: HasFocus {
|
||||
fn focus_layout (&self) -> &[&[Self::Item]];
|
||||
fn focus_cursor (&self) -> (usize, usize);
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
|
||||
fn focus_current (&self) -> Self::Item {
|
||||
let (x, y) = self.focus_cursor();
|
||||
self.focus_layout()[y][x]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
self.focus_to(self.focus_current());
|
||||
self.focus_updated()
|
||||
}
|
||||
fn focus_up (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y == 0 {
|
||||
self.focus_layout().len().saturating_sub(1)
|
||||
} else {
|
||||
y - 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_down (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
y + 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_left (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x == 0 {
|
||||
self.focus_layout()[y].len().saturating_sub(1)
|
||||
} else {
|
||||
x - 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
*self.focus_cursor_mut() = (next_x, y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_right (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
x + 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement next/prev navigation between focusable elements.
|
||||
pub trait FocusOrder {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self);
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self);
|
||||
}
|
||||
|
||||
/// Next/prev navigation for directional focusables works in the given way.
|
||||
impl<T: FocusGrid + HasEnter> FocusOrder for T {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, y) = self.focus_cursor();
|
||||
if x < self.focus_layout()[y].len().saturating_sub(1) {
|
||||
self.focus_right();
|
||||
} else {
|
||||
self.focus_down();
|
||||
self.focus_cursor_mut().0 = 0;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_next()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, _) = self.focus_cursor();
|
||||
if x > 0 {
|
||||
self.focus_left();
|
||||
} else {
|
||||
self.focus_up();
|
||||
let (_, y) = self.focus_cursor();
|
||||
let next_x = self.focus_layout()[y].len().saturating_sub(1);
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_prev()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FocusWrap<T> {
|
||||
fn wrap <W: Render<Tui>> (self, focus: T, content: &'_ W) -> impl Render<Tui> + '_;
|
||||
}
|
||||
|
||||
pub fn to_focus_command <T: Send + Sync> (input: &TuiInput) -> Option<FocusCommand<T>> {
|
||||
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
|
||||
Some(match input.event() {
|
||||
key_pat!(Tab) => FocusCommand::Next,
|
||||
key_pat!(Shift-Tab) => FocusCommand::Prev,
|
||||
key_pat!(BackTab) => FocusCommand::Prev,
|
||||
key_pat!(Shift-BackTab) => FocusCommand::Prev,
|
||||
key_pat!(Up) => FocusCommand::Up,
|
||||
key_pat!(Down) => FocusCommand::Down,
|
||||
key_pat!(Left) => FocusCommand::Left,
|
||||
key_pat!(Right) => FocusCommand::Right,
|
||||
key_pat!(Enter) => FocusCommand::Enter,
|
||||
key_pat!(Esc) => FocusCommand::Exit,
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! impl_focus {
|
||||
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
|
||||
impl HasFocus for $Struct {
|
||||
type Item = $Focus;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item {
|
||||
self.focus.inner()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item) {
|
||||
self.focus.set_inner(to)
|
||||
}
|
||||
$(fn focus_updated (&mut $self) { $update_focus })?
|
||||
}
|
||||
impl HasEnter for $Struct {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool {
|
||||
self.focus.is_entered()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool) {
|
||||
if entered {
|
||||
self.focus.to_entered()
|
||||
} else {
|
||||
self.focus.to_focused()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FocusGrid for $Struct {
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[$Focus]] {
|
||||
use $Focus::*;
|
||||
&$Grid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Current input state
|
||||
pub trait Input<E: Engine> {
|
||||
/// Type of input event
|
||||
type Event;
|
||||
/// Currently handled event
|
||||
fn event (&self) -> &Self::Event;
|
||||
/// Whether component should exit
|
||||
fn is_done (&self) -> bool;
|
||||
/// Mark component as done
|
||||
fn done (&self);
|
||||
}
|
||||
|
||||
/// Handle input
|
||||
pub trait Handle<E: Engine>: Send + Sync {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! handle {
|
||||
(<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
||||
impl Handle<$E> for $Struct {
|
||||
fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> {
|
||||
$handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, H: Handle<E>> Handle<E> for &mut H {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
(*self).handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, H: Handle<E>> Handle<E> for Option<H> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
if let Some(ref mut handle) = self {
|
||||
handle.handle(context)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Mutex<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.get_mut().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for RwLock<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! render {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => {
|
||||
impl<E: Engine, $($($L),*$($T $(: $U)?),*)?> Render<E> for $Struct $(<$($L,)* E, $($T),*>)? {
|
||||
fn min_size (&$self, to: <E as Engine>::Size) -> Perhaps<<E as Engine>::Size> {
|
||||
$cb.min_size(to)
|
||||
}
|
||||
fn render (&$self, to: &mut <E as Engine>::Output) -> Usually<()> {
|
||||
$cb.render(to)
|
||||
}
|
||||
}
|
||||
};
|
||||
(<$E:ty>|$self:ident:$Struct:ident$(<
|
||||
$($($L:lifetime),+)?
|
||||
$($($T:ident$(:$U:path)?),+)?
|
||||
>)?|$cb:expr) => {
|
||||
impl $(<
|
||||
$($($L),+)?
|
||||
$($($T$(:$U)?),+)?
|
||||
>)? Render<$E> for $Struct $(<
|
||||
$($($L),+)?
|
||||
$($($T),+)?
|
||||
>)? {
|
||||
fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> {
|
||||
$cb.min_size(to)
|
||||
}
|
||||
fn render (&$self, to: &mut <$E as Engine>::Output) -> Usually<()> {
|
||||
$cb.render(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering target
|
||||
pub trait Output<E: Engine> {
|
||||
/// Current output area
|
||||
fn area (&self) -> E::Area;
|
||||
/// Mutable pointer to area
|
||||
fn area_mut (&mut self) -> &mut E::Area;
|
||||
/// Render widget in area
|
||||
fn render_in (&mut self, area: E::Area, widget: &dyn Render<E>) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// Cast to dynamic pointer
|
||||
pub fn widget <E: Engine, T: Render<E>> (w: &T) -> &dyn Render<E> {
|
||||
w as &dyn Render<E>
|
||||
}
|
||||
|
||||
/// A renderable component
|
||||
pub trait Render<E: Engine>: Send + Sync {
|
||||
/// Minimum size to use
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(Some(to))
|
||||
}
|
||||
/// Draw to output render target
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()>;
|
||||
}
|
||||
|
||||
impl<E: Engine, R: Render<E>> Render<E> for &R {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(*self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(*self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Render<E> for &dyn Render<E> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(*self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(*self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
//impl<E: Engine> Render<E> for &mut dyn Render<E> {
|
||||
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
//(*self).min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
//(*self).render(to)
|
||||
//}
|
||||
//}
|
||||
|
||||
impl<E: Engine> Render<E> for Box<dyn Render<E> + '_> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(**self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(**self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Arc<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.as_ref().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.as_ref().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Mutex<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.lock().unwrap().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.lock().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for RwLock<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.read().unwrap().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.read().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Option<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten())
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom [Render] defined by passing layout and render closures in place.
|
||||
pub struct Widget<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
>(L, R, PhantomData<E>);
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
> Widget<E, L, R> {
|
||||
pub fn new (layout: L, render: R) -> Self {
|
||||
Self(layout, render, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
> Render<E> for Widget<E, L, R> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.0(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.1(to)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#[cfg(test)] mod test_focus {
|
||||
use super::focus::*;
|
||||
#[test] fn test_focus () {
|
||||
|
||||
struct FocusTest {
|
||||
focused: char,
|
||||
cursor: (usize, usize)
|
||||
}
|
||||
|
||||
impl HasFocus for FocusTest {
|
||||
type Item = char;
|
||||
fn focused (&self) -> Self::Item {
|
||||
self.focused
|
||||
}
|
||||
fn set_focused (&mut self, to: Self::Item) {
|
||||
self.focused = to
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusGrid for FocusTest {
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[Self::Item]] {
|
||||
&[
|
||||
&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
&['a', 'a', 'a', 'b', 'b', 'd'],
|
||||
&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
&['a', 'a', 'a', 'c', 'c', 'd'],
|
||||
&['e', 'e', 'e', 'e', 'e', 'e'],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let mut tester = FocusTest { focused: 'a', cursor: (0, 0) };
|
||||
|
||||
tester.focus_right();
|
||||
assert_eq!(tester.cursor.0, 3);
|
||||
assert_eq!(tester.focused, 'b');
|
||||
|
||||
tester.focus_down();
|
||||
assert_eq!(tester.cursor.1, 2);
|
||||
assert_eq!(tester.focused, 'c');
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
|
||||
use ClockCommand::{Play, Pause};
|
||||
use GrooveboxCommand::*;
|
||||
use PhraseCommand::*;
|
||||
use PhrasePoolCommand::*;
|
||||
|
||||
pub struct GrooveboxTui {
|
||||
_jack: Arc<RwLock<JackClient>>,
|
||||
|
||||
pub player: MidiPlayer,
|
||||
pub pool: PoolModel,
|
||||
pub editor: MidiEditorModel,
|
||||
pub sampler: crate::sampler::Sampler,
|
||||
|
||||
pub size: Measure<Tui>,
|
||||
pub status: bool,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
from_jack!(|jack|GrooveboxTui {
|
||||
let mut player = MidiPlayer::new(jack, "sequencer")?;
|
||||
let phrase = Arc::new(RwLock::new(MidiClip::new(
|
||||
"New", true, 4 * player.clock.timebase.ppq.get() as usize,
|
||||
None, Some(ItemColor::random().into())
|
||||
)));
|
||||
player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone())));
|
||||
let pool = crate::pool::PoolModel::from(&phrase);
|
||||
let editor = crate::midi::MidiEditorModel::from(&phrase);
|
||||
let sampler = crate::sampler::Sampler::new(jack, "sampler")?;
|
||||
Self {
|
||||
_jack: jack.clone(),
|
||||
player,
|
||||
pool,
|
||||
editor,
|
||||
sampler,
|
||||
size: Measure::new(),
|
||||
midi_buf: vec![vec![];65536],
|
||||
note_buf: vec![],
|
||||
perf: PerfModel::default(),
|
||||
status: true,
|
||||
}
|
||||
});
|
||||
audio!(|self: GrooveboxTui, client, scope|{
|
||||
let t0 = self.perf.get_t0();
|
||||
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
if Control::Quit == PlayerAudio(
|
||||
&mut self.player, &mut self.note_buf, &mut self.midi_buf
|
||||
).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
match message {
|
||||
MidiMessage::NoteOn { ref key, .. } => {
|
||||
self.editor.set_note_point(key.as_int() as usize);
|
||||
},
|
||||
MidiMessage::Controller { controller, value } => {
|
||||
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
|
||||
let mut sample = sample.write().unwrap();
|
||||
let percentage = value.as_int() as f64 / 127.;
|
||||
match controller.as_int() {
|
||||
20 => {
|
||||
sample.start = (percentage * sample.end as f64) as usize;
|
||||
},
|
||||
21 => {
|
||||
let length = sample.channels[0].len();
|
||||
sample.end = sample.start + (percentage * (length as f64 - sample.start as f64)) as usize;
|
||||
sample.end = sample.end.min(length);
|
||||
},
|
||||
24 => {
|
||||
sample.gain = percentage as f32 * 2.0;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.perf.update(t0, scope);
|
||||
Control::Continue
|
||||
});
|
||||
has_clock!(|self:GrooveboxTui|&self.player.clock);
|
||||
render!(<Tui>|self:GrooveboxTui|{
|
||||
let w = self.size.w();
|
||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
||||
let sampler_w = 11;
|
||||
let note_pt = self.editor.note_point();
|
||||
Fill::wh(lay!([
|
||||
&self.size,
|
||||
Fill::wh(Align::s(Fixed::h(2, GrooveboxStatus::from(self)))),
|
||||
Tui::shrink_y(2, col!([
|
||||
Fixed::h(2, row!([
|
||||
Fixed::wh(5, 2, PlayPause(self.clock().is_rolling())),
|
||||
Fixed::h(2, TransportView::from((self, self.player.play_phrase().as_ref().map(|(_,p)|
|
||||
p.as_ref().map(|p|p.read().unwrap().color)
|
||||
).flatten().clone(), true))),
|
||||
])),
|
||||
Tui::push_x(sampler_w, Fixed::h(1, row!([
|
||||
PhraseSelector::play_phrase(&self.player),
|
||||
PhraseSelector::next_phrase(&self.player),
|
||||
]))),
|
||||
row!([
|
||||
Tui::split_n(false, 9,
|
||||
col!([
|
||||
row!(|add|{
|
||||
if let Some(sample) = &self.sampler.mapped[note_pt] {
|
||||
add(&format!("Sample {}", sample.read().unwrap().end))?;
|
||||
}
|
||||
add(&MidiEditStatus(&self.editor))?;
|
||||
Ok(())
|
||||
}),
|
||||
lay!([
|
||||
Outer(Style::default().fg(TuiTheme::g(128))),
|
||||
Fill::w(Fixed::h(8, if let Some((_, sample)) = &self.sampler.recording {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else if let Some(sample) = &self.sampler.mapped[note_pt] {
|
||||
SampleViewer(Some(sample.clone()))
|
||||
} else {
|
||||
SampleViewer(None)
|
||||
})),
|
||||
]),
|
||||
]),
|
||||
Tui::split_w(false, pool_w,
|
||||
Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.pool)))),
|
||||
Tui::split_e(false, sampler_w, Fill::wh(col!([
|
||||
Meters(self.sampler.input_meter.as_ref()),
|
||||
GrooveboxSamples(self),
|
||||
])), Fill::h(&self.editor))
|
||||
)
|
||||
),
|
||||
]),
|
||||
]))
|
||||
]))
|
||||
});
|
||||
|
||||
struct GrooveboxSamples<'a>(&'a GrooveboxTui);
|
||||
render!(<Tui>|self: GrooveboxSamples<'a>|{
|
||||
let note_lo = self.0.editor.note_lo().load(Relaxed);
|
||||
let note_pt = self.0.editor.note_point();
|
||||
let note_hi = self.0.editor.note_hi();
|
||||
Fill::wh(col!(note in (note_lo..=note_hi).rev() => {
|
||||
let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset };
|
||||
let mut fg = TuiTheme::g(160);
|
||||
if let Some((index, _)) = self.0.sampler.recording {
|
||||
if note == index {
|
||||
bg = Color::Rgb(64,16,0);
|
||||
fg = Color::Rgb(224,64,32)
|
||||
}
|
||||
} else if self.0.sampler.mapped[note].is_some() {
|
||||
fg = TuiTheme::g(224);
|
||||
}
|
||||
Tui::bg(bg, if let Some(sample) = &self.0.sampler.mapped[note] {
|
||||
Tui::fg(fg, format!("{note:3} ?????? "))
|
||||
} else {
|
||||
Tui::fg(fg, format!("{note:3} (none) "))
|
||||
})
|
||||
}))
|
||||
});
|
||||
|
||||
pub enum GrooveboxCommand {
|
||||
History(isize),
|
||||
Clock(ClockCommand),
|
||||
Pool(PoolCommand),
|
||||
Editor(PhraseCommand),
|
||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||
Sampler(SamplerCommand),
|
||||
}
|
||||
|
||||
handle!(<Tui>|self: GrooveboxTui, input|GrooveboxCommand::execute_with_state(self, input));
|
||||
|
||||
input_to_command!(GrooveboxCommand: <Tui>|state: GrooveboxTui, input|match input.event() {
|
||||
// TODO: k: toggle on-screen keyboard
|
||||
key_pat!(Ctrl-Char('k')) => {
|
||||
todo!("keyboard")
|
||||
},
|
||||
|
||||
// Transport: Play from start or rewind to start
|
||||
key_pat!(Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
key_pat!(Tab) => Pool(PoolCommand::Show(!state.pool.visible)),
|
||||
|
||||
// q: Enqueue currently edited phrase
|
||||
key_pat!(Char('q')) => Enqueue(Some(state.pool.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key_pat!(Char('0')) => Enqueue(Some(state.pool.phrases()[0].clone())),
|
||||
|
||||
key_pat!(Shift-Char('R')) => Sampler(if state.sampler.recording.is_some() {
|
||||
SamplerCommand::RecordFinish
|
||||
} else {
|
||||
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
||||
}),
|
||||
|
||||
// e: Toggle between editing currently playing or other phrase
|
||||
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.pool.phrase().clone();
|
||||
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
playing.clone()
|
||||
})))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
|
||||
// For the rest, use the default keybindings of the components.
|
||||
// The ones defined above supersede them.
|
||||
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
|
||||
Editor(command)
|
||||
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||
Pool(command)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
});
|
||||
|
||||
command!(|self:GrooveboxCommand,state:GrooveboxTui|match self {
|
||||
Self::Pool(cmd) => {
|
||||
let mut default = |cmd: PoolCommand|cmd
|
||||
.execute(&mut state.pool)
|
||||
.map(|x|x.map(Pool));
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
PoolCommand::Select(_) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
undo
|
||||
},
|
||||
// update color in all places simultaneously
|
||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||
undo
|
||||
},
|
||||
_ => default(cmd)?
|
||||
}
|
||||
},
|
||||
Self::Editor(cmd) => {
|
||||
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
|
||||
match cmd {
|
||||
_ => default()?
|
||||
}
|
||||
},
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Self::Sampler(cmd) => cmd.execute(&mut state.sampler)?.map(Sampler),
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Self::History(delta) => {
|
||||
todo!("undo/redo")
|
||||
},
|
||||
});
|
||||
|
|
@ -1,377 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub use ::jack as libjack;
|
||||
pub use ::jack::{
|
||||
contrib::ClosureProcessHandler, NotificationHandler,
|
||||
Client, AsyncClient, ClientOptions, ClientStatus,
|
||||
ProcessScope, Control, CycleTimes, Frames,
|
||||
Port, PortId, PortSpec, Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
|
||||
Transport, TransportState, MidiIter, MidiWriter, RawMidi,
|
||||
};
|
||||
|
||||
pub mod activate;
|
||||
pub(crate) use self::activate::*;
|
||||
pub use self::activate::JackActivate;
|
||||
|
||||
pub mod client;
|
||||
pub(crate) use self::client::*;
|
||||
pub use self::client::JackClient;
|
||||
|
||||
pub mod jack_event;
|
||||
pub(crate) use self::jack_event::*;
|
||||
|
||||
pub mod ports;
|
||||
pub(crate) use self::ports::*;
|
||||
pub use self::ports::RegisterPort;
|
||||
|
||||
/// Implement [TryFrom<&Arc<RwLock<JackClient>>>]: create app state from wrapped JACK handle.
|
||||
#[macro_export] macro_rules! from_jack {
|
||||
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok($cb)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Implement [Audio]: provide JACK callbacks.
|
||||
#[macro_export] macro_rules! audio {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
||||
#[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for thing that has 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 wrap a JACK client.
|
||||
pub trait AudioEngine {
|
||||
|
||||
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 client (&self) -> &Client;
|
||||
|
||||
fn activate (
|
||||
self,
|
||||
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
||||
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
///// 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> Render for JackDevice<E> {
|
||||
//type Engine = E;
|
||||
//fn min_size(&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>),
|
||||
//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
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait JackActivate: Sized {
|
||||
fn activate_with <T: Audio + 'static> (
|
||||
self,
|
||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||
)
|
||||
-> Usually<Arc<RwLock<T>>>;
|
||||
}
|
||||
|
||||
impl JackActivate for JackClient {
|
||||
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 = 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use crate::*;
|
||||
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
||||
pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>;
|
||||
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
||||
/// 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),
|
||||
}
|
||||
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 JackClient {
|
||||
pub fn new (name: &str) -> Usually<Self> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
Ok(Self::Inactive(client))
|
||||
}
|
||||
}
|
||||
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 = ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait RegisterPort {
|
||||
fn midi_in (&self, name: &str) -> Usually<Port<MidiIn>>;
|
||||
fn midi_out (&self, name: &str) -> Usually<Port<MidiOut>>;
|
||||
fn audio_in (&self, name: &str) -> Usually<Port<AudioIn>>;
|
||||
fn audio_out (&self, name: &str) -> Usually<Port<AudioOut>>;
|
||||
fn connect_midi_from (&self, my_input: &Port<MidiIn>, ports: &[String]) -> Usually<()>;
|
||||
fn connect_midi_to (&self, my_output: &Port<MidiOut>, ports: &[String]) -> Usually<()>;
|
||||
fn connect_audio_from (&self, my_input: &Port<AudioIn>, ports: &[String]) -> Usually<()>;
|
||||
fn connect_audio_to (&self, my_output: &Port<AudioOut>, ports: &[String]) -> Usually<()>;
|
||||
}
|
||||
|
||||
impl RegisterPort for Arc<RwLock<JackClient>> {
|
||||
fn midi_in (&self, name: &str) -> Usually<Port<MidiIn>> {
|
||||
Ok(self.read().unwrap().client().register_port(name, MidiIn::default())?)
|
||||
}
|
||||
fn midi_out (&self, name: &str) -> Usually<Port<MidiOut>> {
|
||||
Ok(self.read().unwrap().client().register_port(name, MidiOut::default())?)
|
||||
}
|
||||
fn audio_out (&self, name: &str) -> Usually<Port<AudioOut>> {
|
||||
Ok(self.read().unwrap().client().register_port(name, AudioOut::default())?)
|
||||
}
|
||||
fn audio_in (&self, name: &str) -> Usually<Port<AudioIn>> {
|
||||
Ok(self.read().unwrap().client().register_port(name, AudioIn::default())?)
|
||||
}
|
||||
fn connect_midi_from (&self, input: &Port<MidiIn>, ports: &[String]) -> Usually<()> {
|
||||
let jack = self.read().unwrap();
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(port, input)?;
|
||||
} else {
|
||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn connect_midi_to (&self, output: &Port<MidiOut>, ports: &[String]) -> Usually<()> {
|
||||
let jack = self.read().unwrap();
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(output, port)?;
|
||||
} else {
|
||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn connect_audio_from (&self, input: &Port<AudioIn>, ports: &[String]) -> Usually<()> {
|
||||
let jack = self.read().unwrap();
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(port, input)?;
|
||||
} else {
|
||||
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn connect_audio_to (&self, output: &Port<AudioOut>, ports: &[String]) -> Usually<()> {
|
||||
let jack = self.read().unwrap();
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.port_by_name(port).as_ref() {
|
||||
jack.client().connect_ports(output, port)?;
|
||||
} else {
|
||||
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
})
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
#![allow(unused)]
|
||||
#![allow(clippy::unit_arg)]
|
||||
|
||||
pub mod core; pub use self::core::*;
|
||||
|
||||
pub mod time; pub(crate) use self::time::*;
|
||||
|
||||
pub mod space; pub(crate) use self::space::*;
|
||||
|
||||
pub mod tui; pub(crate) use self::tui::*;
|
||||
pub use tui::*;
|
||||
|
||||
pub mod jack; pub(crate) use self::jack::*;
|
||||
pub use self::jack::*;
|
||||
|
||||
pub mod midi; pub(crate) use self::midi::*;
|
||||
|
||||
pub mod meter; pub(crate) use self::meter::*;
|
||||
|
||||
pub mod piano_h; pub(crate) use self::piano_h::*;
|
||||
|
||||
pub mod transport; pub(crate) use self::transport::*;
|
||||
pub use self::transport::TransportTui;
|
||||
|
||||
pub mod sequencer; pub(crate) use self::sequencer::*;
|
||||
pub use self::sequencer::SequencerTui;
|
||||
|
||||
pub mod arranger; pub(crate) use self::arranger::*;
|
||||
pub use self::arranger::ArrangerTui;
|
||||
|
||||
pub mod sampler; pub(crate) use self::sampler::*;
|
||||
pub use self::sampler::{SamplerTui, Sampler, Sample, Voice};
|
||||
|
||||
pub mod mixer; pub(crate) use self::mixer::*;
|
||||
pub use self::mixer::{Mixer, MixerTrack, MixerTrackDevice};
|
||||
|
||||
pub mod plugin; pub(crate) use self::plugin::*;
|
||||
pub use self::plugin::*;
|
||||
|
||||
pub mod groovebox; pub(crate) use self::groovebox::*;
|
||||
pub use self::groovebox::GrooveboxTui;
|
||||
|
||||
pub mod pool; pub(crate) use self::pool::*;
|
||||
pub use self::pool::PoolModel;
|
||||
|
||||
pub mod status; pub(crate) use self::status::*;
|
||||
|
||||
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
|
||||
|
||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||
|
||||
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||
pub(crate) use std::sync::atomic::Ordering;
|
||||
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use std::marker::PhantomData;
|
||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use std::time::Duration;
|
||||
pub(crate) use std::io::{Stdout, stdout};
|
||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
||||
pub(crate) use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
pub use ::crossterm;
|
||||
pub(crate) use crossterm::{ExecutableCommand};
|
||||
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
|
||||
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
|
||||
|
||||
pub use ::ratatui; pub(crate) use ratatui::{
|
||||
prelude::{Style, Buffer},
|
||||
style::{Stylize, Modifier},
|
||||
backend::{Backend, CrosstermBackend, ClearType}
|
||||
};
|
||||
|
||||
pub use ::midly::{self, num::u7}; pub(crate) use ::midly::{
|
||||
Smf,
|
||||
MidiMessage,
|
||||
TrackEventKind,
|
||||
live::LiveEvent,
|
||||
};
|
||||
|
||||
pub use ::palette; pub(crate) use ::palette::{
|
||||
*,
|
||||
convert::*,
|
||||
okhsl::*
|
||||
};
|
||||
|
||||
testmod! { test }
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Meters<'a>(pub &'a[f32]);
|
||||
|
||||
render!(<Tui>|self: Meters<'a>|col!([
|
||||
&format!("L/{:>+9.3}", self.0[0]),
|
||||
&format!("R/{:>+9.3}", self.0[1]),
|
||||
]));
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub(crate) mod midi_pool; pub(crate) use midi_pool::*;
|
||||
pub(crate) mod midi_clip; pub(crate) use midi_clip::*;
|
||||
pub(crate) mod midi_launch; pub(crate) use midi_launch::*;
|
||||
pub(crate) mod midi_play; pub(crate) use midi_play::*;
|
||||
pub(crate) mod midi_rec; pub(crate) use midi_rec::*;
|
||||
|
||||
pub(crate) mod midi_note; pub(crate) use midi_note::*;
|
||||
pub(crate) mod midi_range; pub(crate) use midi_range::*;
|
||||
pub(crate) mod midi_point; pub(crate) use midi_point::*;
|
||||
pub(crate) mod midi_view; pub(crate) use midi_view::*;
|
||||
|
||||
pub(crate) mod midi_editor; pub(crate) use midi_editor::*;
|
||||
|
||||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
|
||||
fn has_midi_ins (&self) -> bool {
|
||||
!self.midi_ins().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
|
||||
fn has_midi_outs (&self) -> bool {
|
||||
!self.midi_outs().is_empty()
|
||||
}
|
||||
/// Buffer for serializing a MIDI event. FIXME rename
|
||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||
}
|
||||
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||
let mut buf = vec![];
|
||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||
evt.write(&mut buf).unwrap();
|
||||
output[0].push(buf);
|
||||
}
|
||||
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||
time as usize,
|
||||
LiveEvent::parse(bytes).unwrap(),
|
||||
bytes
|
||||
)))
|
||||
}
|
||||
|
||||
/// Update notes_in array
|
||||
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
||||
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_note_name (n: usize) -> &'static str {
|
||||
if n > 127 {
|
||||
panic!("to_note_name({n}): must be 0-127");
|
||||
}
|
||||
MIDI_NOTE_NAMES[n]
|
||||
}
|
||||
|
||||
pub const MIDI_NOTE_NAMES: [&str; 128] = [
|
||||
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||||
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||||
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||||
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||||
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||||
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||||
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||||
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||||
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||||
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||||
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||||
];
|
||||
|
||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||
|
||||
impl MidiPlayerApi for MidiPlayer {}
|
||||
|
||||
pub trait HasPlayer {
|
||||
fn player (&self) -> &impl MidiPlayerApi;
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_player {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
|
||||
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state for playing a phrase
|
||||
pub struct MidiPlayer {
|
||||
/// State of clock and playhead
|
||||
pub(crate) clock: ClockModel,
|
||||
/// Start time and phrase being played
|
||||
pub(crate) play_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Start time and next phrase
|
||||
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Play input through output.
|
||||
pub(crate) monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub(crate) recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub(crate) overdub: bool,
|
||||
/// Send all notes off
|
||||
pub(crate) reset: bool, // TODO?: after Some(nframes)
|
||||
/// Record from MIDI ports to current sequence.
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
/// Notes currently held at input
|
||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
impl MidiPlayer {
|
||||
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
clock: ClockModel::from(jack),
|
||||
play_phrase: None,
|
||||
next_phrase: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
midi_ins: vec![
|
||||
jack.midi_in(&format!("M/{name}"))?,
|
||||
],
|
||||
|
||||
midi_outs: vec![
|
||||
jack.midi_out(&format!("{name}/M"))?,
|
||||
],
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
reset: true,
|
||||
|
||||
note_buf: vec![0;8],
|
||||
})
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for MidiPlayer {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiPlayer")
|
||||
.field("clock", &self.clock)
|
||||
.field("play_phrase", &self.play_phrase)
|
||||
.field("next_phrase", &self.next_phrase)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
from!(|clock: &ClockModel| MidiPlayer = Self {
|
||||
clock: clock.clone(),
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
play_phrase: None,
|
||||
next_phrase: None,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
});
|
||||
from!(|state: (&ClockModel, &Arc<RwLock<MidiClip>>)|MidiPlayer = {
|
||||
let (clock, phrase) = state;
|
||||
let mut model = Self::from(clock);
|
||||
model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone())));
|
||||
model
|
||||
});
|
||||
has_clock!(|self: MidiPlayer|&self.clock);
|
||||
|
||||
impl HasMidiIns for MidiPlayer {
|
||||
fn midi_ins (&self) -> &Vec<Port<MidiIn>> {
|
||||
&self.midi_ins
|
||||
}
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>> {
|
||||
&mut self.midi_ins
|
||||
}
|
||||
}
|
||||
|
||||
impl HasMidiOuts for MidiPlayer {
|
||||
fn midi_outs (&self) -> &Vec<Port<MidiOut>> {
|
||||
&self.midi_outs
|
||||
}
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>> {
|
||||
&mut self.midi_outs
|
||||
}
|
||||
fn midi_note (&mut self) -> &mut Vec<u8> {
|
||||
&mut self.note_buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for a single MIDI player
|
||||
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||
/// Player
|
||||
pub &'a mut T,
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
);
|
||||
|
||||
/// JACK process callback for a sequencer's phrase player/recorder.
|
||||
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buf = &mut self.1;
|
||||
let midi_buf = &mut self.2;
|
||||
// Clear output buffer(s)
|
||||
model.clear(scope, midi_buf, false);
|
||||
// Write chunk of phrase to output, handle switchover
|
||||
if model.play(scope, note_buf, midi_buf) {
|
||||
model.switchover(scope, note_buf, midi_buf);
|
||||
}
|
||||
if model.has_midi_ins() {
|
||||
if model.recording() || model.monitoring() {
|
||||
// Record and/or monitor input
|
||||
model.record(scope, midi_buf)
|
||||
} else if model.has_midi_outs() && model.monitoring() {
|
||||
// Monitor input to output
|
||||
model.monitor(scope, midi_buf)
|
||||
}
|
||||
}
|
||||
// Write to output port(s)
|
||||
model.write(scope, midi_buf);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiRecordApi for MidiPlayer {
|
||||
fn recording (&self) -> bool {
|
||||
self.recording
|
||||
}
|
||||
fn recording_mut (&mut self) -> &mut bool {
|
||||
&mut self.recording
|
||||
}
|
||||
fn monitoring (&self) -> bool {
|
||||
self.monitoring
|
||||
}
|
||||
fn monitoring_mut (&mut self) -> &mut bool {
|
||||
&mut self.monitoring
|
||||
}
|
||||
fn overdub (&self) -> bool {
|
||||
self.overdub
|
||||
}
|
||||
fn overdub_mut (&mut self) -> &mut bool {
|
||||
&mut self.overdub
|
||||
}
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_in
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiPlaybackApi for MidiPlayer {
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_out
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPlayPhrase for MidiPlayer {
|
||||
fn reset (&self) -> bool {
|
||||
self.reset
|
||||
}
|
||||
fn reset_mut (&mut self) -> &mut bool {
|
||||
&mut self.reset
|
||||
}
|
||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.play_phrase
|
||||
}
|
||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.play_phrase
|
||||
}
|
||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.next_phrase
|
||||
}
|
||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.next_phrase
|
||||
}
|
||||
}
|
||||
|
||||
//#[derive(Debug)]
|
||||
//pub struct MIDIPlayer {
|
||||
///// Global timebase
|
||||
//pub clock: Arc<Clock>,
|
||||
///// Start time and phrase being played
|
||||
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||
///// Start time and next phrase
|
||||
//pub next_phrase: Option<(Moment, 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]>>,
|
||||
//}
|
||||
|
||||
///// Methods used primarily by the process callback
|
||||
//impl MIDIPlayer {
|
||||
//pub fn new (
|
||||
//jack: &Arc<RwLock<JackClient>>,
|
||||
//clock: &Arc<Clock>,
|
||||
//name: &str
|
||||
//) -> Usually<Self> {
|
||||
//let jack = jack.read().unwrap();
|
||||
//Ok(Self {
|
||||
//clock: clock.clone(),
|
||||
//phrase: None,
|
||||
//next_phrase: None,
|
||||
//notes_in: Arc::new(RwLock::new([false;128])),
|
||||
//notes_out: Arc::new(RwLock::new([false;128])),
|
||||
//monitoring: false,
|
||||
//recording: false,
|
||||
//overdub: true,
|
||||
//reset: true,
|
||||
//midi_note: Vec::with_capacity(8),
|
||||
//midi_chunk: vec![Vec::with_capacity(16);16384],
|
||||
//midi_outputs: vec![
|
||||
//jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
||||
//],
|
||||
//midi_inputs: vec![
|
||||
//jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
||||
//],
|
||||
//})
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasMidiClip {
|
||||
fn phrase (&self) -> &Arc<RwLock<MidiClip>>;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_phrase {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn phrase (&$self) -> &Arc<RwLock<MidiClip>> { &$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A MIDI sequence.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiClip {
|
||||
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: MidiData,
|
||||
/// Whether to loop the phrase or play it once
|
||||
pub looped: 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: ItemPalette,
|
||||
}
|
||||
|
||||
/// MIDI message structural
|
||||
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||
|
||||
impl MidiClip {
|
||||
pub fn new (
|
||||
name: impl AsRef<str>,
|
||||
looped: bool,
|
||||
length: usize,
|
||||
notes: Option<MidiData>,
|
||||
color: Option<ItemPalette>,
|
||||
) -> 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]),
|
||||
looped,
|
||||
loop_start: 0,
|
||||
loop_length: length,
|
||||
percussive: true,
|
||||
color: color.unwrap_or_else(ItemPalette::random)
|
||||
}
|
||||
}
|
||||
pub fn set_length (&mut self, length: usize) {
|
||||
self.length = length;
|
||||
self.notes = vec![Vec::with_capacity(16);length];
|
||||
}
|
||||
pub fn duplicate (&self) -> Self {
|
||||
let mut clone = self.clone();
|
||||
clone.uuid = uuid::Uuid::new_v4();
|
||||
clone
|
||||
}
|
||||
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
|
||||
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 {
|
||||
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 } }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MidiClip {
|
||||
fn default () -> Self {
|
||||
Self::new(
|
||||
"Stop",
|
||||
false,
|
||||
1,
|
||||
Some(vec![vec![MidiMessage::Controller {
|
||||
controller: 123.into(),
|
||||
value: 0.into()
|
||||
}]]),
|
||||
Some(ItemColor::from(Color::Rgb(32, 32, 32)).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MidiClip {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
self.uuid == other.uuid
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MidiClip {}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
use crate::*;
|
||||
use KeyCode::{Char, Up, Down, Left, Right, Enter};
|
||||
use PhraseCommand::*;
|
||||
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &MidiEditorModel;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn editor (&$self) -> &MidiEditorModel { &$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
AppendNote,
|
||||
PutNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
SetTimeLock(bool),
|
||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
}
|
||||
|
||||
event_map_input_to_command!(Tui: MidiEditorModel: PhraseCommand: MidiEditorModel::KEYS);
|
||||
impl MidiEditorModel {
|
||||
const KEYS: KeyMapping<31, Self> = [
|
||||
(kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)),
|
||||
(kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))),
|
||||
(kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))),
|
||||
(kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
|
||||
(kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)),
|
||||
(kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))),
|
||||
(kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))),
|
||||
(kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())),
|
||||
(kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)),
|
||||
(kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))),
|
||||
(kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))),
|
||||
(kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
|
||||
(kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
|
||||
(kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
|
||||
(kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
|
||||
(kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
|
||||
(kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
|
||||
(kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
|
||||
(kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
|
||||
(kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
|
||||
(kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())),
|
||||
(kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
|
||||
(kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
|
||||
(kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
|
||||
(kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
|
||||
(kexp!(Enter), &|s: &Self|PutNote),
|
||||
(kexp!(Ctrl-Enter), &|s: &Self|AppendNote),
|
||||
(kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet
|
||||
(kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
|
||||
(kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet
|
||||
(kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
|
||||
//// TODO: key_pat!(Char('/')) => // toggle 3plet
|
||||
//// TODO: key_pat!(Char('?')) => // toggle dotted
|
||||
];
|
||||
fn phrase_length (&self) -> usize {
|
||||
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<MidiEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut MidiEditorModel) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
match self {
|
||||
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
|
||||
PutNote => { state.put_note(false); },
|
||||
AppendNote => { state.put_note(true); },
|
||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
||||
SetTimeLock(x) => { state.time_lock().set(x); },
|
||||
SetTimeScroll(x) => { state.time_start().set(x); },
|
||||
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
||||
SetNoteLength(x) => { state.set_note_len(x); },
|
||||
SetTimeCursor(x) => { state.set_time_point(x); },
|
||||
SetNoteCursor(note) => { state.set_note_point(note.min(127)); },
|
||||
_ => todo!("{:?}", self)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state for viewing and editing a phrase
|
||||
pub struct MidiEditorModel {
|
||||
/// Renders the phrase
|
||||
pub mode: Box<dyn PhraseViewMode>,
|
||||
pub size: Measure<Tui>
|
||||
}
|
||||
|
||||
impl Default for MidiEditorModel {
|
||||
fn default () -> Self {
|
||||
let mut mode = Box::new(PianoHorizontal::new(None));
|
||||
mode.redraw();
|
||||
Self { mode, size: Measure::new() }
|
||||
}
|
||||
}
|
||||
|
||||
has_size!(<Tui>|self:MidiEditorModel|&self.size);
|
||||
render!(<Tui>|self: MidiEditorModel|{
|
||||
self.autoscroll();
|
||||
self.autozoom();
|
||||
&self.mode
|
||||
});
|
||||
//render!(<Tui>|self: MidiEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
|
||||
|
||||
pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
|
||||
fn redraw (&mut self);
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.phrase_mut() = phrase.cloned();
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiView<Tui> for MidiEditorModel {}
|
||||
|
||||
impl TimeRange for MidiEditorModel {
|
||||
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
||||
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
||||
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
||||
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
||||
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiEditorModel {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
||||
}
|
||||
|
||||
impl NotePoint for MidiEditorModel {
|
||||
fn note_len (&self) -> usize { self.mode.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
|
||||
fn note_point (&self) -> usize { self.mode.note_point() }
|
||||
fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiEditorModel {
|
||||
fn time_point (&self) -> usize { self.mode.time_point() }
|
||||
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
|
||||
}
|
||||
|
||||
impl PhraseViewMode for MidiEditorModel {
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
|
||||
self.mode.buffer_size(phrase)
|
||||
}
|
||||
fn redraw (&mut self) {
|
||||
self.mode.redraw()
|
||||
}
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||
self.mode.phrase()
|
||||
}
|
||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
self.mode.phrase_mut()
|
||||
}
|
||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
self.mode.set_phrase(phrase)
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiEditorModel {
|
||||
/// Put note at current position
|
||||
pub fn put_note (&mut self, advance: bool) {
|
||||
let mut redraw = false;
|
||||
if let Some(phrase) = self.phrase() {
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let note_start = self.time_point();
|
||||
let note_point = self.note_point();
|
||||
let note_len = self.note_len();
|
||||
let note_end = note_start + (note_len.saturating_sub(1));
|
||||
let key: u7 = u7::from(note_point as u8);
|
||||
let vel: u7 = 100.into();
|
||||
let length = phrase.length;
|
||||
let note_end = note_end % length;
|
||||
let note_on = MidiMessage::NoteOn { key, vel };
|
||||
if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) {
|
||||
phrase.notes[note_start].push(note_on);
|
||||
}
|
||||
let note_off = MidiMessage::NoteOff { key, vel };
|
||||
if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) {
|
||||
phrase.notes[note_end].push(note_off);
|
||||
}
|
||||
if advance {
|
||||
self.set_time_point(note_end);
|
||||
}
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
self.mode.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditorModel = {
|
||||
let mut model = Self::from(Some(phrase.clone()));
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditorModel = {
|
||||
let mut model = Self::default();
|
||||
*model.phrase_mut() = phrase;
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
impl std::fmt::Debug for MidiEditorModel {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiEditorModel")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPlayPhrase: HasClock {
|
||||
fn reset (&self) -> bool;
|
||||
fn reset_mut (&mut self) -> &mut bool;
|
||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
fn pulses_since_start (&self) -> Option<f64> {
|
||||
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
Some(elapsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn pulses_since_start_looped (&self) -> Option<f64> {
|
||||
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
|
||||
let elapsed = (elapsed as usize % length) as f64;
|
||||
Some(elapsed)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
let start = self.clock().next_launch_pulse() as f64;
|
||||
let instant = Moment::from_pulse(self.clock().timebase(), start);
|
||||
*self.next_phrase_mut() = Some((instant, phrase.cloned()));
|
||||
*self.reset_mut() = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Note;
|
||||
|
||||
impl Note {
|
||||
/// (pulses, name), assuming 96 PPQ
|
||||
pub const DURATIONS: [(usize, &str);26] = [
|
||||
(1, "1/384"), (2, "1/192"),
|
||||
(3, "1/128"), (4, "1/96"),
|
||||
(6, "1/64"), (8, "1/48"),
|
||||
(12, "1/32"), (16, "1/24"),
|
||||
(24, "1/16"), (32, "1/12"),
|
||||
(48, "1/8"), (64, "1/6"),
|
||||
(96, "1/4"), (128, "1/3"),
|
||||
(192, "1/2"), (256, "2/3"),
|
||||
(384, "1/1"), (512, "4/3"),
|
||||
(576, "3/2"), (768, "2/1"),
|
||||
(1152, "3/1"), (1536, "4/1"),
|
||||
(2304, "6/1"), (3072, "8/1"),
|
||||
(3456, "9/1"), (6144, "16/1"),
|
||||
];
|
||||
/// Returns the next shorter length
|
||||
pub fn prev (pulses: usize) -> usize {
|
||||
for i in 1..=16 { let length = Note::DURATIONS[16-i].0; if length < pulses { return length } }
|
||||
pulses
|
||||
}
|
||||
/// Returns the next longer length
|
||||
pub fn next (pulses: usize) -> usize {
|
||||
for (length, _) in &Note::DURATIONS { if *length > pulses { return *length } }
|
||||
pulses
|
||||
}
|
||||
pub fn pulses_to_name (pulses: usize) -> &'static str {
|
||||
for (length, name) in &Note::DURATIONS { if *length == pulses { return name } }
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
fn clear (
|
||||
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
|
||||
) {
|
||||
for frame in &mut out[0..scope.n_frames() as usize] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Output notes from phrase to MIDI output ports.
|
||||
fn play (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) -> bool {
|
||||
if !self.clock().is_rolling() {
|
||||
return false
|
||||
}
|
||||
// If a phrase is playing, write a chunk of MIDI events from it to the output buffer.
|
||||
// If no phrase is playing, prepare for switchover immediately.
|
||||
self.play_phrase().as_ref().map_or(true, |(started, phrase)|{
|
||||
self.play_chunk(scope, note_buf, out, started, phrase)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle switchover from current to next playing phrase.
|
||||
fn switchover (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) {
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
//let samples = scope.n_frames() as usize;
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
let start = start_at.sample.get() as usize;
|
||||
let sample = self.clock().started.read().unwrap()
|
||||
.as_ref().unwrap().sample.get() as usize;
|
||||
// If it's time to switch to the next phrase:
|
||||
if start <= sample0.saturating_sub(sample) {
|
||||
// Samples elapsed since phrase was supposed to start
|
||||
let _skipped = sample0 - start;
|
||||
// Switch over to enqueued phrase
|
||||
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||
// Launch enqueued phrase
|
||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
*self.next_phrase_mut() = None;
|
||||
// Fill in remaining ticks of chunk from next phrase.
|
||||
self.play(scope, note_buf, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_chunk (
|
||||
&self,
|
||||
scope: &ProcessScope,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
started: &Moment,
|
||||
phrase: &Option<Arc<RwLock<MidiClip>>>
|
||||
) -> bool {
|
||||
// First sample to populate. Greater than 0 means that the first
|
||||
// pulse of the phrase falls somewhere in the middle of the chunk.
|
||||
let sample = (scope.last_frame_time() as usize).saturating_sub(
|
||||
started.sample.get() as usize +
|
||||
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||
);
|
||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||
// paired with pulse (index into phrase from which to take the MIDI event) for each
|
||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
|
||||
// Notes active during current chunk.
|
||||
let notes = &mut self.notes_out().write().unwrap();
|
||||
let length = phrase.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||
for (sample, pulse) in pulses {
|
||||
// If a next phrase is enqueued, and we're past the end of the current one,
|
||||
// break the loop here (FIXME count pulse correctly)
|
||||
let past_end = if phrase.is_some() { pulse >= length } else { true };
|
||||
if self.next_phrase().is_some() && past_end {
|
||||
return true
|
||||
}
|
||||
// If there's a currently playing phrase, output notes from it to buffer:
|
||||
if let Some(ref phrase) = phrase {
|
||||
Self::play_pulse(phrase, pulse, sample, note_buf, out, notes)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn play_pulse (
|
||||
phrase: &RwLock<MidiClip>,
|
||||
pulse: usize,
|
||||
sample: usize,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
notes: &mut [bool;128]
|
||||
) {
|
||||
// Source phrase from which the MIDI events will be taken.
|
||||
let phrase = phrase.read().unwrap();
|
||||
// Phrase with zero length is not processed
|
||||
if phrase.length > 0 {
|
||||
// Current pulse index in source phrase
|
||||
let pulse = pulse % phrase.length;
|
||||
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
||||
for message in phrase.notes[pulse].iter() {
|
||||
// Clear output buffer for this MIDI event.
|
||||
note_buf.clear();
|
||||
// TODO: support MIDI channels other than CH1.
|
||||
let channel = 0.into();
|
||||
// Serialize MIDI event into message buffer.
|
||||
LiveEvent::Midi { channel, message: *message }
|
||||
.write(note_buf)
|
||||
.unwrap();
|
||||
// Append serialized message to output buffer.
|
||||
out[sample].push(note_buf.clone());
|
||||
// Update the list of currently held notes.
|
||||
update_keys(&mut*notes, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
|
||||
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
Self::write_port(&mut port.writer(scope), samples, out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to an output port.
|
||||
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
|
||||
for (time, events) in out.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiPointModel {
|
||||
/// Time coordinate of cursor
|
||||
pub time_point: Arc<AtomicUsize>,
|
||||
/// Note coordinate of cursor
|
||||
pub note_point: Arc<AtomicUsize>,
|
||||
/// Length of note that will be inserted, in pulses
|
||||
pub note_len: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Default for MidiPointModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
time_point: Arc::new(0.into()),
|
||||
note_point: Arc::new(36.into()),
|
||||
note_len: Arc::new(24.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NotePoint {
|
||||
fn note_len (&self) -> usize;
|
||||
fn set_note_len (&self, x: usize);
|
||||
fn note_point (&self) -> usize;
|
||||
fn set_note_point (&self, x: usize);
|
||||
fn note_end (&self) -> usize { self.note_point() + self.note_len() }
|
||||
}
|
||||
|
||||
pub trait TimePoint {
|
||||
fn time_point (&self) -> usize;
|
||||
fn set_time_point (&self, x: usize);
|
||||
}
|
||||
|
||||
pub trait MidiPoint: NotePoint + TimePoint {}
|
||||
|
||||
impl<T: NotePoint + TimePoint> MidiPoint for T {}
|
||||
|
||||
impl NotePoint for MidiPointModel {
|
||||
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
|
||||
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
|
||||
fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) }
|
||||
fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiPointModel {
|
||||
fn time_point (&self) -> usize { self.time_point.load(Relaxed) }
|
||||
fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) }
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPhrases {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<MidiClip>>>;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_phrases {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn phrases (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
|
||||
fn phrases_mut (&mut $self) -> &mut Vec<Arc<RwLock<MidiClip>>> { &mut$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhrasePoolCommand {
|
||||
Add(usize, MidiClip),
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
Import(usize, PathBuf),
|
||||
Export(usize, PathBuf),
|
||||
SetName(usize, String),
|
||||
SetLength(usize, usize),
|
||||
SetColor(usize, ItemColor),
|
||||
}
|
||||
|
||||
impl<T: HasPhrases> Command<T> for PhrasePoolCommand {
|
||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||
use PhrasePoolCommand::*;
|
||||
Ok(match self {
|
||||
Add(mut index, phrase) => {
|
||||
let phrase = Arc::new(RwLock::new(phrase));
|
||||
let phrases = model.phrases_mut();
|
||||
if index >= phrases.len() {
|
||||
index = phrases.len();
|
||||
phrases.push(phrase)
|
||||
} else {
|
||||
phrases.insert(index, phrase);
|
||||
}
|
||||
Some(Self::Delete(index))
|
||||
},
|
||||
Delete(index) => {
|
||||
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
|
||||
Some(Self::Add(index, phrase))
|
||||
},
|
||||
Swap(index, other) => {
|
||||
model.phrases_mut().swap(index, other);
|
||||
Some(Self::Swap(index, other))
|
||||
},
|
||||
Import(index, path) => {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut phrase = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
phrase.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Self::Add(index, phrase).execute(model)?
|
||||
},
|
||||
Export(_index, _path) => {
|
||||
todo!("export phrase to midi file");
|
||||
},
|
||||
SetName(index, name) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_name = phrase.name.clone();
|
||||
phrase.name = name;
|
||||
Some(Self::SetName(index, old_name))
|
||||
},
|
||||
SetLength(index, length) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_len = phrase.length;
|
||||
phrase.length = length;
|
||||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemPalette::from(color);
|
||||
std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MidiRangeModel {
|
||||
pub time_len: Arc<AtomicUsize>,
|
||||
/// Length of visible time axis
|
||||
pub time_axis: Arc<AtomicUsize>,
|
||||
/// Earliest time displayed
|
||||
pub time_start: Arc<AtomicUsize>,
|
||||
/// Time step
|
||||
pub time_zoom: Arc<AtomicUsize>,
|
||||
/// Auto rezoom to fit in time axis
|
||||
pub time_lock: Arc<AtomicBool>,
|
||||
/// Length of visible note axis
|
||||
pub note_axis: Arc<AtomicUsize>,
|
||||
// Lowest note displayed
|
||||
pub note_lo: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
from!(|data:(usize, bool)|MidiRangeModel = Self {
|
||||
time_len: Arc::new(0.into()),
|
||||
note_axis: Arc::new(0.into()),
|
||||
note_lo: Arc::new(0.into()),
|
||||
time_axis: Arc::new(0.into()),
|
||||
time_start: Arc::new(0.into()),
|
||||
time_zoom: Arc::new(data.0.into()),
|
||||
time_lock: Arc::new(data.1.into()),
|
||||
});
|
||||
|
||||
pub trait TimeRange {
|
||||
fn time_len (&self) -> &AtomicUsize;
|
||||
fn time_zoom (&self) -> &AtomicUsize;
|
||||
fn time_lock (&self) -> &AtomicBool;
|
||||
fn time_start (&self) -> &AtomicUsize;
|
||||
fn time_axis (&self) -> &AtomicUsize;
|
||||
fn time_end (&self) -> usize {
|
||||
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NoteRange {
|
||||
fn note_lo (&self) -> &AtomicUsize;
|
||||
fn note_axis (&self) -> &AtomicUsize;
|
||||
fn note_hi (&self) -> usize {
|
||||
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRange: TimeRange + NoteRange {}
|
||||
|
||||
impl<T: TimeRange + NoteRange> MidiRange for T {}
|
||||
|
||||
impl TimeRange for MidiRangeModel {
|
||||
fn time_len (&self) -> &AtomicUsize { &self.time_len }
|
||||
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
|
||||
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
|
||||
fn time_start (&self) -> &AtomicUsize { &self.time_start }
|
||||
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiRangeModel {
|
||||
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
fn recording (&self) -> bool;
|
||||
fn recording_mut (&mut self) -> &mut bool;
|
||||
fn toggle_record (&mut self) {
|
||||
*self.recording_mut() = !self.recording();
|
||||
}
|
||||
fn monitoring (&self) -> bool;
|
||||
fn monitoring_mut (&mut self) -> &mut bool;
|
||||
fn toggle_monitor (&mut self) {
|
||||
*self.monitoring_mut() = !self.monitoring();
|
||||
}
|
||||
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
// For highlighting keys and note repeat
|
||||
let notes_in = self.notes_in().clone();
|
||||
let monitoring = self.monitoring();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
if monitoring {
|
||||
midi_buf[sample].push(bytes.to_vec());
|
||||
}
|
||||
update_keys(&mut notes_in.write().unwrap(), &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
if self.monitoring() {
|
||||
self.monitor(scope, midi_buf);
|
||||
}
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
if let Some((started, ref clip)) = self.play_phrase().clone() {
|
||||
self.record_clip(scope, started, clip, midi_buf);
|
||||
}
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
self.record_next();
|
||||
}
|
||||
}
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
started: Moment,
|
||||
phrase: &Option<Arc<RwLock<MidiClip>>>,
|
||||
midi_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
if let Some(phrase) = phrase {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let start = started.sample.get() as usize;
|
||||
let recording = self.recording();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let quant = self.clock().quant.get();
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let length = phrase.length;
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
phrase.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
quantized as usize % length
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
fn overdub (&self) -> bool;
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait MidiView<E: Engine>: MidiRange + MidiPoint + HasSize<E> {
|
||||
/// Make sure cursor is within range
|
||||
fn autoscroll (&self) {
|
||||
let note_point = self.note_point().min(127);
|
||||
let note_lo = self.note_lo().get();
|
||||
let note_hi = self.note_hi();
|
||||
if note_point < note_lo {
|
||||
self.note_lo().set(note_point);
|
||||
} else if note_point > note_hi {
|
||||
self.note_lo().set((note_lo + note_point).saturating_sub(note_hi));
|
||||
}
|
||||
}
|
||||
/// Make sure range is within display
|
||||
fn autozoom (&self) {
|
||||
let time_len = self.time_len().get();
|
||||
let time_axis = self.time_axis().get();
|
||||
let time_zoom = self.time_zoom().get();
|
||||
//while time_len.div_ceil(time_zoom) > time_axis {
|
||||
//println!("\r{time_len} {time_zoom} {time_axis}");
|
||||
//time_zoom = Note::next(time_zoom);
|
||||
//}
|
||||
//self.time_zoom().set(time_zoom);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
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,
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
audio!(|self: Mixer, _client, _scope|Control::Continue);
|
||||
|
||||
impl Mixer {
|
||||
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
selected_column: 0,
|
||||
selected_track: 1,
|
||||
tracks: vec![],
|
||||
})
|
||||
}
|
||||
pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> {
|
||||
let track = MixerTrack::new(name)?;
|
||||
self.tracks.push(track);
|
||||
Ok(self)
|
||||
}
|
||||
pub fn track (&self) -> Option<&MixerTrack> {
|
||||
self.tracks.get(self.selected_track)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixerTrack {
|
||||
pub fn new (name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
devices: vec![],
|
||||
//ports: JackPorts::default(),
|
||||
//devices: vec![],
|
||||
//device: 0,
|
||||
})
|
||||
}
|
||||
//fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||
//self.devices.get(i).map(|d|d.state.write().unwrap())
|
||||
//}
|
||||
//pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||
//self.get_device_mut(self.device)
|
||||
//}
|
||||
///// Add a device to the end of the chain.
|
||||
//pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
|
||||
//self.devices.push(device);
|
||||
//let index = self.devices.len() - 1;
|
||||
//Ok(&mut self.devices[index])
|
||||
//}
|
||||
//pub fn add_device (&mut self, device: JackDevice<E>) {
|
||||
//self.devices.push(device);
|
||||
//}
|
||||
//pub fn connect_first_device (&self) -> Usually<()> {
|
||||
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
|
||||
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
|
||||
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
|
||||
//Some(device) => {
|
||||
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
|
||||
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
|
||||
//()
|
||||
//},
|
||||
//None => ()
|
||||
//})
|
||||
//}
|
||||
}
|
||||
|
||||
pub struct TrackView<'a> {
|
||||
pub chain: Option<&'a MixerTrack>,
|
||||
pub direction: Direction,
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
impl<'a> Render<Tui> for TrackView<'a> {
|
||||
fn min_size (&self, area: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
todo!()
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
todo!();
|
||||
//let mut area = to.area();
|
||||
//if let Some(chain) = self.chain {
|
||||
//match self.direction {
|
||||
//Direction::Down => area.width = area.width.min(40),
|
||||
//Direction::Right => area.width = area.width.min(10),
|
||||
//_ => { unimplemented!() },
|
||||
//}
|
||||
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
|
||||
//let mut split = Stack::new(self.direction);
|
||||
//for device in chain.devices.as_slice().iter() {
|
||||
//split = split.add_ref(device);
|
||||
//}
|
||||
//let (area, areas) = split.render_areas(to)?;
|
||||
//if self.focused && self.entered && areas.len() > 0 {
|
||||
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
|
||||
//}
|
||||
//Ok(Some(area))
|
||||
//} else {
|
||||
//let [x, y, width, height] = area;
|
||||
//let label = "No chain selected";
|
||||
//let x = x + (width - label.len() as u16) / 2;
|
||||
//let y = y + height / 2;
|
||||
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
|
||||
//Ok(Some(area))
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
//impl Content<Tui> for Mixer<Tui> {
|
||||
//fn content (&self) -> impl Render<Tui> {
|
||||
//Stack::right(|add| {
|
||||
//for channel in self.tracks.iter() {
|
||||
//add(channel)?;
|
||||
//}
|
||||
//Ok(())
|
||||
//})
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl Content<Tui> for Track<Tui> {
|
||||
//fn content (&self) -> impl Render<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,
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
handle!(<Tui>|self:Mixer,engine|{
|
||||
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
|
||||
|
||||
match event.code {
|
||||
//KeyCode::Char('c') => {
|
||||
//if event.modifiers == KeyModifiers::CONTROL {
|
||||
//self.exit();
|
||||
//}
|
||||
//},
|
||||
KeyCode::Down => {
|
||||
self.selected_track = (self.selected_track + 1) % self.tracks.len();
|
||||
println!("{}", self.selected_track);
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Up => {
|
||||
if self.selected_track == 0 {
|
||||
self.selected_track = self.tracks.len() - 1;
|
||||
} else {
|
||||
self.selected_track -= 1;
|
||||
}
|
||||
println!("{}", self.selected_track);
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if self.selected_column == 0 {
|
||||
self.selected_column = 6
|
||||
} else {
|
||||
self.selected_column -= 1;
|
||||
}
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if self.selected_column == 6 {
|
||||
self.selected_column = 0
|
||||
} else {
|
||||
self.selected_column += 1;
|
||||
}
|
||||
return Ok(Some(true))
|
||||
},
|
||||
_ => {
|
||||
println!("\n{event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(None)
|
||||
});
|
||||
|
||||
handle!(<Tui>|self:MixerTrack,from|{
|
||||
match from.event() {
|
||||
//, NONE, "chain_cursor_up", "move cursor up", || {
|
||||
key_pat!(KeyCode::Up) => {
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_cursor_down", "move cursor down", || {
|
||||
key_pat!(KeyCode::Down) => {
|
||||
Ok(Some(true))
|
||||
},
|
||||
// Left, NONE, "chain_cursor_left", "move cursor left", || {
|
||||
key_pat!(KeyCode::Left) => {
|
||||
//if let Some(track) = app.arranger.track_mut() {
|
||||
//track.device = track.device.saturating_sub(1);
|
||||
//return Ok(true)
|
||||
//}
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_cursor_right", "move cursor right", || {
|
||||
key_pat!(KeyCode::Right) => {
|
||||
//if let Some(track) = app.arranger.track_mut() {
|
||||
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
|
||||
//return Ok(true)
|
||||
//}
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_mode_switch", "switch the display mode", || {
|
||||
key_pat!(KeyCode::Char('`')) => {
|
||||
//app.chain_mode = !app.chain_mode;
|
||||
Ok(Some(true))
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
});
|
||||
|
||||
pub enum MixerTrackCommand {}
|
||||
|
||||
//impl MixerTrackDevice for LV2Plugin {}
|
||||
|
||||
pub trait MixerTrackDevice: Debug + Send + Sync {
|
||||
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixerTrackDevice for Sampler {}
|
||||
|
||||
impl MixerTrackDevice for Plugin {}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
mod piano_h_cursor;
|
||||
use self::piano_h_cursor::*;
|
||||
|
||||
mod piano_h_keys;
|
||||
pub(crate) use self::piano_h_keys::*;
|
||||
pub use self::piano_h_keys::render_keys_v;
|
||||
|
||||
mod piano_h_notes; use self::piano_h_notes::*;
|
||||
|
||||
mod piano_h_time; use self::piano_h_time::*;
|
||||
|
||||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
|
||||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||
}
|
||||
|
||||
/// A phrase, rendered as a horizontal piano roll.
|
||||
pub struct PianoHorizontal {
|
||||
phrase: Option<Arc<RwLock<MidiClip>>>,
|
||||
/// Buffer where the whole phrase is rerendered on change
|
||||
buffer: BigBuffer,
|
||||
/// Size of actual notes area
|
||||
size: Measure<Tui>,
|
||||
/// The display window
|
||||
range: MidiRangeModel,
|
||||
/// The note cursor
|
||||
point: MidiPointModel,
|
||||
/// The highlight color palette
|
||||
color: ItemPalette,
|
||||
/// Width of the keyboard
|
||||
keys_width: u16,
|
||||
}
|
||||
|
||||
impl PianoHorizontal {
|
||||
pub fn new (phrase: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||
let size = Measure::new();
|
||||
let mut range = MidiRangeModel::from((24, true));
|
||||
range.time_axis = size.x.clone();
|
||||
range.note_axis = size.y.clone();
|
||||
let color = phrase.as_ref()
|
||||
.map(|p|p.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||
Self {
|
||||
buffer: Default::default(),
|
||||
point: MidiPointModel::default(),
|
||||
phrase: phrase.cloned(),
|
||||
size,
|
||||
range,
|
||||
color,
|
||||
keys_width: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(<Tui>|self: PianoHorizontal|{
|
||||
|
||||
let (color, name, length, looped) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
|
||||
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
|
||||
} else {
|
||||
(ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false)
|
||||
};
|
||||
|
||||
let field = move|x, y|row!([
|
||||
Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)),
|
||||
Tui::fg_bg(color.lighter.rgb, color.dark.rgb, &y),
|
||||
]);
|
||||
|
||||
let keys_width = 5;
|
||||
let keys = move||PianoHorizontalKeys(self);
|
||||
|
||||
let timeline = move||PianoHorizontalTimeline(self);
|
||||
|
||||
let notes = move||PianoHorizontalNotes(self);
|
||||
|
||||
let cursor = move||PianoHorizontalCursor(self);
|
||||
|
||||
let border = Fill::wh(Outer(Style::default().fg(self.color.dark.rgb).bg(self.color.darkest.rgb)));
|
||||
let with_border = |x|lay!([border, Tui::inset_xy(0, 0, &x)]);
|
||||
|
||||
with_border(lay!([
|
||||
Tui::push_x(0, row!(![
|
||||
//" ",
|
||||
field("Edit:", name.to_string()), " ",
|
||||
field("Length:", length.to_string()), " ",
|
||||
field("Loop:", looped.to_string())
|
||||
])),
|
||||
Tui::inset_xy(0, 1, Fill::wh(Bsp::s(
|
||||
Fixed::h(1, Bsp::e(
|
||||
Fixed::w(self.keys_width, ""),
|
||||
Fill::w(timeline()),
|
||||
)),
|
||||
Bsp::e(
|
||||
Fixed::w(self.keys_width, keys()),
|
||||
Fill::wh(lay!([
|
||||
&self.size,
|
||||
Fill::wh(lay!([
|
||||
Fill::wh(notes()),
|
||||
Fill::wh(cursor()),
|
||||
]))
|
||||
])),
|
||||
),
|
||||
)))
|
||||
]))
|
||||
});
|
||||
|
||||
impl PianoHorizontal {
|
||||
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_bg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize, note_len: usize) {
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||||
let cell = buf.get_mut(x, y).unwrap();
|
||||
cell.set_bg(phrase.color.darkest.rgb);
|
||||
cell.set_fg(phrase.color.darker.rgb);
|
||||
cell.set_char(if time % 384 == 0 {
|
||||
'│'
|
||||
} else if time % 96 == 0 {
|
||||
'╎'
|
||||
} else if time % note_len == 0 {
|
||||
'┊'
|
||||
} else if (127 - note) % 12 == 1 {
|
||||
'='
|
||||
} else {
|
||||
'·'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||
fn draw_fg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize) {
|
||||
let style = Style::default().fg(phrase.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
|
||||
let mut notes_on = [false;128];
|
||||
for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() {
|
||||
|
||||
for (y, note) in (0..127).rev().enumerate() {
|
||||
if let Some(cell) = buf.get_mut(x, note) {
|
||||
if notes_on[note] {
|
||||
cell.set_char('▂');
|
||||
cell.set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time_end = time_start + zoom;
|
||||
for time in time_start..time_end.min(phrase.length) {
|
||||
for event in phrase.notes[time].iter() {
|
||||
match event {
|
||||
MidiMessage::NoteOn { key, .. } => {
|
||||
let note = key.as_int() as usize;
|
||||
let cell = buf.get_mut(x, note).unwrap();
|
||||
cell.set_char('█');
|
||||
cell.set_style(style);
|
||||
notes_on[note] = true
|
||||
},
|
||||
MidiMessage::NoteOff { key, .. } => {
|
||||
notes_on[key.as_int() as usize] = false
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_size!(<Tui>|self:PianoHorizontal|&self.size);
|
||||
|
||||
impl TimeRange for PianoHorizontal {
|
||||
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
|
||||
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
|
||||
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
|
||||
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
|
||||
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
|
||||
}
|
||||
impl NoteRange for PianoHorizontal {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
|
||||
}
|
||||
impl NotePoint for PianoHorizontal {
|
||||
fn note_len (&self) -> usize { self.point.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.point.set_note_len(x) }
|
||||
fn note_point (&self) -> usize { self.point.note_point() }
|
||||
fn set_note_point (&self, x: usize) { self.point.set_note_point(x) }
|
||||
}
|
||||
impl TimePoint for PianoHorizontal {
|
||||
fn time_point (&self) -> usize { self.point.time_point() }
|
||||
fn set_time_point (&self, x: usize) { self.point.set_time_point(x) }
|
||||
}
|
||||
impl PhraseViewMode for PianoHorizontal {
|
||||
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||
&self.phrase
|
||||
}
|
||||
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||
&mut self.phrase
|
||||
}
|
||||
/// Determine the required space to render the phrase.
|
||||
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
|
||||
(phrase.length / self.range.time_zoom().get(), 128)
|
||||
}
|
||||
fn redraw (&mut self) {
|
||||
let buffer = if let Some(phrase) = self.phrase.as_ref() {
|
||||
let phrase = phrase.read().unwrap();
|
||||
let buf_size = self.buffer_size(&phrase);
|
||||
let mut buffer = BigBuffer::from(buf_size);
|
||||
let note_len = self.note_len();
|
||||
let time_zoom = self.time_zoom().get();
|
||||
self.time_len().set(phrase.length);
|
||||
PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len);
|
||||
PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom);
|
||||
buffer
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
self.buffer = buffer
|
||||
}
|
||||
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
*self.phrase_mut() = phrase.cloned();
|
||||
self.color = phrase.map(|p|p.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PianoHorizontal {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("PianoHorizontal")
|
||||
.field("time_zoom", &self.range.time_zoom)
|
||||
.field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
// Update sequencer playhead indicator
|
||||
//self.now().set(0.);
|
||||
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now().set(now);
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
use crate::*;
|
||||
use super::note_y_iter;
|
||||
|
||||
pub struct PianoHorizontalCursor<'a>(pub(crate) &'a PianoHorizontal);
|
||||
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to|Ok({
|
||||
let style = Some(Style::default().fg(self.0.color.lightest.rgb));
|
||||
let note_hi = self.0.note_hi();
|
||||
let note_len = self.0.note_len();
|
||||
let note_lo = self.0.note_lo().get();
|
||||
let note_point = self.0.note_point();
|
||||
let time_point = self.0.time_point();
|
||||
let time_start = self.0.time_start().get();
|
||||
let time_zoom = self.0.time_zoom().get();
|
||||
let [x0, y0, w, _] = to.area().xywh();
|
||||
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||
if note == note_point {
|
||||
for x in 0..w {
|
||||
let screen_x = x0 + x;
|
||||
let time_1 = time_start + x as usize * time_zoom;
|
||||
let time_2 = time_1 + time_zoom;
|
||||
if time_1 <= time_point && time_point < time_2 {
|
||||
to.blit(&"█", screen_x, screen_y, style);
|
||||
let tail = note_len as u16 / time_zoom as u16;
|
||||
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
||||
to.blit(&"▂", x_tail, screen_y, style);
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
use crate::*;
|
||||
use super::note_y_iter;
|
||||
|
||||
pub struct PianoHorizontalKeys<'a>(pub(crate) &'a PianoHorizontal);
|
||||
render!(<Tui>|self: PianoHorizontalKeys<'a>|render(|to|Ok(render_keys_v(to, self))));
|
||||
has_color!(|self: PianoHorizontalKeys<'a>|self.0.color.base);
|
||||
impl<'a> NoteRange for PianoHorizontalKeys<'a> {
|
||||
fn note_lo (&self) -> &AtomicUsize { &self.0.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { &self.0.note_axis() }
|
||||
}
|
||||
impl<'a> NotePoint for PianoHorizontalKeys<'a> {
|
||||
fn note_len (&self) -> usize { self.0.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.0.set_note_len(x) }
|
||||
fn note_point (&self) -> usize { self.0.note_point() }
|
||||
fn set_note_point (&self, x: usize) { self.0.set_note_point(x) }
|
||||
}
|
||||
|
||||
pub fn render_keys_v <T: HasColor + NoteRange + NotePoint> (to: &mut TuiOutput, state: &T) {
|
||||
let color = state.color();
|
||||
let note_lo = state.note_lo().get();
|
||||
let note_hi = state.note_hi();
|
||||
let note_point = state.note_point();
|
||||
let [x, y0, w, h] = to.area().xywh();
|
||||
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
|
||||
let off_style = Some(Style::default().fg(TuiTheme::g(160)));
|
||||
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color.rgb).bold());
|
||||
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||
to.blit(&to_key(note), x, screen_y, key_style);
|
||||
if note > 127 {
|
||||
continue
|
||||
}
|
||||
if note == note_point {
|
||||
to.blit(&format!("{:<5}", to_note_name(note)), x, screen_y, on_style)
|
||||
} else {
|
||||
to.blit(&to_note_name(note), x, screen_y, off_style)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn to_key (note: usize) -> &'static str {
|
||||
match note % 12 {
|
||||
11 => "████▌",
|
||||
10 => " ",
|
||||
9 => "████▌",
|
||||
8 => " ",
|
||||
7 => "████▌",
|
||||
6 => " ",
|
||||
5 => "████▌",
|
||||
4 => "████▌",
|
||||
3 => " ",
|
||||
2 => "████▌",
|
||||
1 => " ",
|
||||
0 => "████▌",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
use crate::*;
|
||||
use super::note_y_iter;
|
||||
|
||||
pub struct PianoHorizontalNotes<'a>(pub(crate) &'a PianoHorizontal);
|
||||
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to|Ok({
|
||||
let time_start = self.0.time_start().get();
|
||||
let note_lo = self.0.note_lo().get();
|
||||
let note_hi = self.0.note_hi();
|
||||
let note_point = self.0.note_point();
|
||||
let source = &self.0.buffer;
|
||||
let [x0, y0, w, h] = to.area().xywh();
|
||||
if h as usize != self.0.note_axis().get() {
|
||||
panic!("area height mismatch");
|
||||
}
|
||||
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
||||
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||
//if area_x % 10 == 0 {
|
||||
//to.blit(&format!("{area_y} {note}"), screen_x, screen_y, None);
|
||||
//}
|
||||
let source_x = time_start + area_x;
|
||||
let source_y = note_hi - area_y;
|
||||
////// TODO: enable loop rollover:
|
||||
//////let source_x = (time_start + area_x) % source.width.max(1);
|
||||
//////let source_y = (note_hi - area_y) % source.height.max(1);
|
||||
let is_in_x = source_x < source.width;
|
||||
let is_in_y = source_y < source.height;
|
||||
if is_in_x && is_in_y {
|
||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
||||
*to.buffer.get_mut(screen_x, screen_y) = source_cell.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//let debug = true;
|
||||
//if debug {
|
||||
//let x0=20+x0;
|
||||
//to.blit(&format!("KYP "), x0, y0, None);
|
||||
//to.blit(&format!("x0={x0} "), x0, y0+1, None);
|
||||
//to.blit(&format!("y0={y0} "), x0, y0+2, None);
|
||||
//to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None);
|
||||
//to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None);
|
||||
//to.blit(&format!("note_point={note_point}, {} ", to_note_name(note_point)), x0, y0+5, None);
|
||||
//to.blit(&format!("time_start={time_start} "), x0, y0+6, None);
|
||||
//return Ok(());
|
||||
//}
|
||||
})));
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
|
||||
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to|{
|
||||
let [x, y, w, h] = to.area();
|
||||
let style = Some(Style::default().dim());
|
||||
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
||||
let t = area_x as usize * self.0.time_zoom().get();
|
||||
if t < length {
|
||||
to.blit(&"|", screen_x, y, style);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
//Tui::fg_bg(
|
||||
//self.0.color.lightest.rgb,
|
||||
//self.0.color.darkest.rgb,
|
||||
//format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str()
|
||||
//));
|
||||
|
|
@ -1 +0,0 @@
|
|||
// TODO
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub mod lv2; pub(crate) use lv2::*;
|
||||
pub use self::lv2::LV2Plugin;
|
||||
|
||||
/// 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 midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
/// 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<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", match self {
|
||||
Self::None => "(none)",
|
||||
Self::LV2(_) => "LV2",
|
||||
Self::VST2{..} => "VST2",
|
||||
Self::VST3 => "VST3",
|
||||
})
|
||||
}
|
||||
}
|
||||
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,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginAudio(Arc<RwLock<Plugin>>);
|
||||
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
|
||||
audio!(|self: PluginAudio, client, scope|{
|
||||
let state = &mut*self.0.write().unwrap();
|
||||
match state.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 state.midi_ins.iter() {
|
||||
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 state.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(state.audio_ins.iter().map(|o|o.as_slice(scope)))
|
||||
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => todo!("only lv2 is supported")
|
||||
}
|
||||
Control::Continue
|
||||
});
|
||||
|
||||
//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)
|
||||
//}
|
||||
|
||||
impl Plugin {
|
||||
/// 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,
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
//ports: JackPorts::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Render<Tui> for Plugin {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(Some(to))
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let area = to.area();
|
||||
let [x, y, _, height] = area;
|
||||
let mut width = 20u16;
|
||||
match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
let style = if i == self.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
} ;
|
||||
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(self, to, x, y, width)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually<()> {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
Ok(())
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
handle!(<Tui>|self:Plugin, from|{
|
||||
match from.event() {
|
||||
key_pat!(KeyCode::Up) => {
|
||||
self.selected = self.selected.saturating_sub(1);
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::Down) => {
|
||||
self.selected = (self.selected + 1).min(match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => unimplemented!()
|
||||
});
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::PageUp) => {
|
||||
self.selected = self.selected.saturating_sub(8);
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::PageDown) => {
|
||||
self.selected = (self.selected + 10).min(match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => unimplemented!()
|
||||
});
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::Char(',')) => {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[self.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value - 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::Char('.')) => {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[self.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value + 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key_pat!(KeyCode::Char('g')) => {
|
||||
match self.plugin {
|
||||
//Some(PluginKind::LV2(ref mut plugin)) => {
|
||||
//plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
||||
//},
|
||||
Some(_) => unreachable!(),
|
||||
None => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// 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)
|
||||
.unwrap_or_else(||panic!("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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
use crate::*;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
use ::winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
platform::x11::EventLoopBuilderExtX11
|
||||
};
|
||||
|
||||
//pub struct LV2PluginUI {
|
||||
//write: (),
|
||||
//controller: (),
|
||||
//widget: (),
|
||||
//features: (),
|
||||
//transfer: (),
|
||||
//}
|
||||
|
||||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||||
Ok(spawn(move||{
|
||||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_loop.run_app(&mut ui).unwrap()
|
||||
}))
|
||||
}
|
||||
|
||||
/// A LV2 plugin's X11 UI.
|
||||
pub struct LV2PluginUI {
|
||||
pub window: Option<Window>
|
||||
}
|
||||
|
||||
impl LV2PluginUI {
|
||||
pub fn new () -> Usually<Self> {
|
||||
Ok(Self { window: None })
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for LV2PluginUI {
|
||||
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
|
||||
}
|
||||
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.window.as_ref().unwrap().set_visible(false);
|
||||
event_loop.exit();
|
||||
},
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.window.as_ref().unwrap().request_redraw();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lv2_ui_instantiate (kind: &str) {
|
||||
//let host = Suil
|
||||
}
|
||||
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
|
||||
use super::*;
|
||||
use ::livi::{
|
||||
World,
|
||||
Instance,
|
||||
Plugin as LiviPlugin,
|
||||
Features,
|
||||
FeaturesBuilder,
|
||||
Port,
|
||||
event::LV2AtomSequence,
|
||||
};
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
/// A LV2 plugin.
|
||||
pub struct LV2Plugin {
|
||||
pub world: World,
|
||||
pub instance: Instance,
|
||||
pub plugin: LiviPlugin,
|
||||
pub features: Arc<Features>,
|
||||
pub port_list: Vec<Port>,
|
||||
pub input_buffer: Vec<LV2AtomSequence>,
|
||||
pub ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LV2Plugin {
|
||||
const INPUT_BUFFER: usize = 1024;
|
||||
pub fn new (uri: &str) -> Usually<Self> {
|
||||
// Get 1st plugin at URI
|
||||
let world = World::with_load_bundle(&uri);
|
||||
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
|
||||
let features = world.build_features(features);
|
||||
let mut plugin = None;
|
||||
if let Some(p) = world.iter_plugins().next() { plugin = Some(p); }
|
||||
let plugin = plugin.expect("plugin not found");
|
||||
let err = &format!("init {uri}");
|
||||
let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) };
|
||||
let mut port_list = vec![];
|
||||
for port in plugin.ports() {
|
||||
port_list.push(port);
|
||||
}
|
||||
let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER);
|
||||
// Instantiate
|
||||
Ok(Self {
|
||||
world, instance, port_list, plugin, features, input_buffer, ui_thread: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
||||
|
||||
fn set_vst_plugin <E: Engine> (host: &Arc<Mutex<Plugin<E>>>, _path: &str) -> Usually<PluginKind> {
|
||||
let mut loader = ::vst::host::PluginLoader::load(
|
||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||
host.clone()
|
||||
)?;
|
||||
Ok(PluginKind::VST2 {
|
||||
instance: loader.instance()?
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
//! TODO
|
||||
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
pub mod phrase_length;
|
||||
pub(crate) use phrase_length::*;
|
||||
|
||||
pub mod phrase_rename;
|
||||
pub(crate) use phrase_rename::*;
|
||||
|
||||
pub mod phrase_selector;
|
||||
pub(crate) use phrase_selector::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolModel {
|
||||
pub(crate) visible: bool,
|
||||
/// Collection of phrases
|
||||
pub(crate) phrases: Vec<Arc<RwLock<MidiClip>>>,
|
||||
/// Selected phrase
|
||||
pub(crate) phrase: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub(crate) mode: Option<PoolMode>,
|
||||
/// Rendered size
|
||||
size: Measure<Tui>,
|
||||
/// Scroll offset
|
||||
scroll: usize,
|
||||
}
|
||||
|
||||
/// Modes for phrase pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, String),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, PhraseLengthFocus),
|
||||
/// Load phrase from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save phrase to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PoolCommand {
|
||||
Show(bool),
|
||||
/// Update the contents of the phrase pool
|
||||
Phrase(PhrasePoolCommand),
|
||||
/// Select a phrase from the phrase pool
|
||||
Select(usize),
|
||||
/// Rename a phrase
|
||||
Rename(PhraseRenameCommand),
|
||||
/// Change the length of a phrase
|
||||
Length(PhraseLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
/// Export to file
|
||||
Export(FileBrowserCommand),
|
||||
}
|
||||
|
||||
command!(|self:PoolCommand, state: PoolModel|{
|
||||
use PoolCommand::*;
|
||||
match self {
|
||||
Show(visible) => {
|
||||
state.visible = visible;
|
||||
Some(Self::Show(!visible))
|
||||
}
|
||||
Rename(command) => match command {
|
||||
PhraseRenameCommand::Begin => {
|
||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Rename)
|
||||
},
|
||||
Length(command) => match command {
|
||||
PhraseLengthCommand::Begin => {
|
||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Rename(state.phrase_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Length)
|
||||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Import)
|
||||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
None
|
||||
},
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
}
|
||||
});
|
||||
|
||||
input_to_command!(PoolCommand:<Tui>|state: PoolModel,input|match state.phrases_mode() {
|
||||
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
||||
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
||||
_ => to_phrases_command(state, input)?
|
||||
});
|
||||
|
||||
fn to_phrases_command (state: &PoolModel, input: &TuiInput) -> Option<PoolCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PoolCommand as Cmd;
|
||||
let index = state.phrase_index();
|
||||
let count = state.phrases().len();
|
||||
Some(match input.event() {
|
||||
key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
||||
key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
||||
key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||
key_pat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
key_pat!(Char('c')) => Cmd::Phrase(PhrasePoolCommand::SetColor(index, ItemColor::random())),
|
||||
key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select(
|
||||
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
||||
),
|
||||
key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select(
|
||||
index.saturating_add(1) % state.phrases().len()
|
||||
),
|
||||
key_pat!(Char('<')) => if index > 1 {
|
||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||
Cmd::Phrase(PhrasePoolCommand::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key_pat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
state.set_phrase_index(state.phrase_index() + 1);
|
||||
Cmd::Phrase(PhrasePoolCommand::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key_pat!(Delete) => if index > 0 {
|
||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Phrase(PhrasePoolCommand::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
key_pat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
))),
|
||||
key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
|
||||
Cmd::Phrase(PhrasePoolCommand::Add(index + 1, phrase))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
impl Default for PoolModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
phrases: vec![RwLock::new(MidiClip::default()).into()],
|
||||
phrase: 0.into(),
|
||||
scroll: 0,
|
||||
mode: None,
|
||||
size: Measure::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|phrase:&Arc<RwLock<MidiClip>>|PoolModel = {
|
||||
let mut model = Self::default();
|
||||
model.phrases.push(phrase.clone());
|
||||
model.phrase.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
has_phrases!(|self: PoolModel|self.phrases);
|
||||
has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]);
|
||||
impl PoolModel {
|
||||
pub(crate) fn phrase_index (&self) -> usize {
|
||||
self.phrase.load(Relaxed)
|
||||
}
|
||||
pub(crate) fn set_phrase_index (&self, value: usize) {
|
||||
self.phrase.store(value, Relaxed);
|
||||
}
|
||||
pub(crate) fn phrases_mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
}
|
||||
pub struct PoolView<'a>(pub(crate) &'a PoolModel);
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(<Tui>|self: PoolView<'a>|{
|
||||
let PoolModel { phrases, mode, .. } = self.0;
|
||||
let bg = TuiTheme::g(32);
|
||||
let title_color = TuiTheme::ti1();
|
||||
let upper_left = "Pool:";
|
||||
let upper_right = format!("({})", phrases.len());
|
||||
let color = self.0.phrase().read().unwrap().color;
|
||||
Tui::bg(bg, lay!(move|add|{
|
||||
add(&Fill::wh(Outer(Style::default().fg(color.base.rgb).bg(bg))))?;
|
||||
//add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?;
|
||||
add(&Tui::inset_xy(0, 1, Fill::wh(col!(move|add|match mode {
|
||||
Some(PoolMode::Import(_, ref file_picker)) => add(file_picker),
|
||||
Some(PoolMode::Export(_, ref file_picker)) => add(file_picker),
|
||||
_ => Ok(for (i, phrase) in phrases.iter().enumerate() {
|
||||
add(&lay!(|add|{
|
||||
let MidiClip { ref name, color, length, .. } = *phrase.read().unwrap();
|
||||
let mut length = PhraseLength::new(length, None);
|
||||
if let Some(PoolMode::Length(phrase, new_length, focus)) = mode {
|
||||
if i == *phrase {
|
||||
length.pulses = *new_length;
|
||||
length.focus = Some(*focus);
|
||||
}
|
||||
}
|
||||
add(&Tui::bg(color.base.rgb, Fill::w(col!([
|
||||
Fill::w(lay!(|add|{
|
||||
add(&Fill::w(Align::w(format!(" {i}"))))?;
|
||||
add(&Fill::w(Align::e(Tui::pull_x(1, length.clone()))))
|
||||
})),
|
||||
Tui::bold(true, {
|
||||
let mut row2 = format!(" {name}");
|
||||
if let Some(PoolMode::Rename(phrase, _)) = mode {
|
||||
if i == *phrase {
|
||||
row2 = format!("{row2}▄");
|
||||
}
|
||||
};
|
||||
row2
|
||||
}),
|
||||
]))))?;
|
||||
if i == self.0.phrase_index() {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
Ok(())
|
||||
}))?;
|
||||
})
|
||||
}))))?;
|
||||
add(&Fill::w(Align::nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?;
|
||||
add(&Fill::w(Align::ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?;
|
||||
add(&self.0.size)
|
||||
}))
|
||||
});
|
||||
command!(|self: FileBrowserCommand, state: PoolModel|{
|
||||
use PoolMode::*;
|
||||
use FileBrowserCommand::*;
|
||||
let mode = &mut state.mode;
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
Confirm => if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
PhrasePoolCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
Some(Export(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
None
|
||||
});
|
||||
input_to_command!(FileBrowserCommand:<Tui>|state: PoolModel,from|{
|
||||
|
||||
use FileBrowserCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
||||
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
||||
match from.event() {
|
||||
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len().saturating_sub(1))),
|
||||
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
key_pat!(Right) => Chdir(browser.cwd.clone()),
|
||||
key_pat!(Left) => Chdir(browser.cwd.clone()),
|
||||
key_pat!(Enter) => Confirm,
|
||||
key_pat!(Char(_)) => { todo!() },
|
||||
key_pat!(Backspace) => { todo!() },
|
||||
key_pat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
||||
match from.event() {
|
||||
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||
.min(browser.len())),
|
||||
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
||||
% browser.len()),
|
||||
key_pat!(Right) => Chdir(browser.cwd.clone()),
|
||||
key_pat!(Left) => Chdir(browser.cwd.clone()),
|
||||
key_pat!(Enter) => Confirm,
|
||||
key_pat!(Char(_)) => { todo!() },
|
||||
key_pat!(Backspace) => { todo!() },
|
||||
key_pat!(Esc) => Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
|
||||
|
||||
/// Displays and edits phrase length.
|
||||
#[derive(Clone)]
|
||||
pub struct PhraseLength {
|
||||
/// Pulses per beat (quaver)
|
||||
pub ppq: usize,
|
||||
/// Beats per bar
|
||||
pub bpb: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub pulses: usize,
|
||||
/// Selected subdivision
|
||||
pub focus: Option<PhraseLengthFocus>,
|
||||
}
|
||||
|
||||
impl PhraseLength {
|
||||
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
pub fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
pub fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
pub fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
pub fn bars_string (&self) -> String {
|
||||
format!("{}", self.bars())
|
||||
}
|
||||
pub fn beats_string (&self) -> String {
|
||||
format!("{}", self.beats())
|
||||
}
|
||||
pub fn ticks_string (&self) -> String {
|
||||
format!("{:>02}", self.ticks())
|
||||
}
|
||||
}
|
||||
|
||||
/// Focused field of `PhraseLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl PhraseLengthFocus {
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Bar => Self::Beat,
|
||||
Self::Beat => Self::Tick,
|
||||
Self::Tick => Self::Bar,
|
||||
}
|
||||
}
|
||||
pub fn prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Bar => Self::Tick,
|
||||
Self::Beat => Self::Bar,
|
||||
Self::Tick => Self::Beat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(<Tui>|self: PhraseLength|{
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
row!(move|add|match self.focus {
|
||||
None =>
|
||||
add(&row!([" ", bars(), ".", beats(), ".", ticks()])),
|
||||
Some(PhraseLengthFocus::Bar) =>
|
||||
add(&row!(["[", bars(), "]", beats(), ".", ticks()])),
|
||||
Some(PhraseLengthFocus::Beat) =>
|
||||
add(&row!([" ", bars(), "[", beats(), "]", ticks()])),
|
||||
Some(PhraseLengthFocus::Tick) =>
|
||||
add(&row!([" ", bars(), ".", beats(), "[", ticks()])),
|
||||
})
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.phrases_mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let mut phrase = state.phrases()[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
std::mem::drop(phrase);
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
None
|
||||
});
|
||||
|
||||
input_to_command!(PhraseLengthCommand:<Tui>|state:PoolModel,from|{
|
||||
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
|
||||
match from.event() {
|
||||
key_pat!(Up) => Self::Inc,
|
||||
key_pat!(Down) => Self::Dec,
|
||||
key_pat!(Right) => Self::Next,
|
||||
key_pat!(Left) => Self::Prev,
|
||||
key_pat!(Enter) => Self::Set(*length),
|
||||
key_pat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(String),
|
||||
}
|
||||
|
||||
impl Command<PoolModel> for PhraseRenameCommand {
|
||||
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PoolMode::Rename(phrase, ref mut old_name)) => match self {
|
||||
Set(s) => {
|
||||
state.phrases()[phrase].write().unwrap().name = s;
|
||||
return Ok(Some(Self::Set(old_name.clone())))
|
||||
},
|
||||
Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
state.phrases()[phrase].write().unwrap().name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PoolModel> for PhraseRenameCommand {
|
||||
fn input_to_command (state: &PoolModel, from: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
||||
Some(match from.event() {
|
||||
key_pat!(Char(c)) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key_pat!(Backspace) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key_pat!(Enter) => Self::Confirm,
|
||||
key_pat!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PhraseSelector {
|
||||
pub(crate) title: &'static str,
|
||||
pub(crate) name: String,
|
||||
pub(crate) color: ItemPalette,
|
||||
pub(crate) time: String,
|
||||
}
|
||||
|
||||
// TODO: Display phrases always in order of appearance
|
||||
render!(<Tui>|self: PhraseSelector|Fixed::wh(24, 1, row!([
|
||||
Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)),
|
||||
Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([
|
||||
format!("{:8}", &self.name[0..8.min(self.name.len())]),
|
||||
Tui::bg(self.color.dark.rgb, &self.time),
|
||||
])),
|
||||
])));
|
||||
|
||||
impl PhraseSelector {
|
||||
|
||||
// beats elapsed
|
||||
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
|
||||
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||
let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
|
||||
(name.clone(), color)
|
||||
} else {
|
||||
("".to_string(), ItemPalette::from(TuiTheme::g(64)))
|
||||
};
|
||||
let time = if let Some(elapsed) = state.pulses_since_start_looped() {
|
||||
format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
|
||||
} else {
|
||||
String::from(" ")
|
||||
};
|
||||
Self { title: "Now:|", time, name, color, }
|
||||
}
|
||||
|
||||
// beats until switchover
|
||||
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
|
||||
let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() {
|
||||
let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
|
||||
let time = {
|
||||
let target = t.pulse.get();
|
||||
let current = state.clock().playhead.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("-{:>}", state.clock().timebase.format_beats_0(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
(time, name.clone(), color)
|
||||
} else if let Some((_, Some(phrase))) = state.play_phrase() {
|
||||
let phrase = phrase.read().unwrap();
|
||||
if phrase.looped {
|
||||
(" ".into(), phrase.name.clone(), phrase.color)
|
||||
} else {
|
||||
(" ".into(), " ".into(), TuiTheme::g(64).into())
|
||||
}
|
||||
} else {
|
||||
(" ".into(), " ".into(), TuiTheme::g(64).into())
|
||||
};
|
||||
Self { title: " Next|", time, name, color, }
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue