mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
wip: improvements to arranger view
This commit is contained in:
parent
2bb8979058
commit
a6a8552996
11 changed files with 762 additions and 577 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1044,7 +1044,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek"
|
name = "tek"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tek"
|
name = "tek"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jack = "0.10"
|
jack = "0.10"
|
||||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref" ] }
|
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||||
backtrace = "0.3.72"
|
backtrace = "0.3.72"
|
||||||
microxdg = "0.1.2"
|
microxdg = "0.1.2"
|
||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
(:12 (44 100) (40 100))
|
(:12 (44 100) (40 100))
|
||||||
(:14 (44 100)))
|
(:14 (44 100)))
|
||||||
|
|
||||||
(phrase { :name "Trapping" :beats 8 :steps 96 }
|
(phrase { :name "Trap Pinging" :beats 8 :steps 96 }
|
||||||
(:00 (42 100) (36 100) (34 120) (49 100))
|
(:00 (42 100) (36 100) (34 120) (49 100))
|
||||||
(:01 (42 100))
|
(:01 (42 100))
|
||||||
(:02 (42 100))
|
(:02 (42 100))
|
||||||
|
|
|
||||||
1
shell.nix
Normal file → Executable file
1
shell.nix
Normal file → Executable file
|
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env nix-shell
|
||||||
{pkgs?import<nixpkgs>{}}:pkgs.mkShell{
|
{pkgs?import<nixpkgs>{}}:pkgs.mkShell{
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@ pub(crate) use ratatui::buffer::{Buffer, Cell};
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
|
|
||||||
pub fn buffer_update (
|
pub fn buffer_update (
|
||||||
buffer: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
||||||
) {
|
) {
|
||||||
for row in 0..area.height {
|
for row in 0..area.height {
|
||||||
let y = area.y + row;
|
let y = area.y + row;
|
||||||
for col in 0..area.width {
|
for col in 0..area.width {
|
||||||
let x = area.x + col;
|
let x = area.x + col;
|
||||||
callback(buffer.get_mut(x, y), col, row);
|
if x < buf.area.width && y < buf.area.height {
|
||||||
|
callback(buf.get_mut(x, y), col, row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +65,7 @@ pub trait Render: Send {
|
||||||
($T:ty) => {
|
($T:ty) => {
|
||||||
impl Render for $T {}
|
impl Render for $T {}
|
||||||
};
|
};
|
||||||
($T:ty |$self:ident, $buf:ident, $area:ident|$block:tt) => {
|
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
|
||||||
impl Render for $T {
|
impl Render for $T {
|
||||||
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
|
||||||
$block
|
$block
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -44,6 +44,87 @@ pub const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// A MIDI sequence.
|
||||||
|
pub struct Phrase {
|
||||||
|
pub name: String,
|
||||||
|
pub length: usize,
|
||||||
|
pub notes: PhraseData,
|
||||||
|
pub looped: Option<(usize, usize)>,
|
||||||
|
/// Immediate note-offs in view
|
||||||
|
pub percussive: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Phrase {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self::new("", 0, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Phrase {
|
||||||
|
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
length,
|
||||||
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
|
looped: Some((0, length)),
|
||||||
|
percussive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||||
|
if pulse >= self.length {
|
||||||
|
panic!("extend phrase first")
|
||||||
|
}
|
||||||
|
self.notes[pulse].push(message);
|
||||||
|
}
|
||||||
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||||
|
//panic!("{:?} {start} {end}", &self);
|
||||||
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||||
|
for event in events.iter() {
|
||||||
|
match event {
|
||||||
|
MidiMessage::NoteOn {key,..} => {
|
||||||
|
if *key == k {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
/// Write a chunk of MIDI events to an output port.
|
||||||
|
pub fn process_out (
|
||||||
|
&self,
|
||||||
|
output: &mut MIDIChunk,
|
||||||
|
notes_on: &mut [bool;128],
|
||||||
|
timebase: &Arc<Timebase>,
|
||||||
|
(frame0, frames, _): (usize, usize, f64),
|
||||||
|
) {
|
||||||
|
let mut buf = Vec::with_capacity(8);
|
||||||
|
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
||||||
|
frame0, frame0 + frames
|
||||||
|
) {
|
||||||
|
let tick = tick % self.length;
|
||||||
|
for message in self.notes[tick].iter() {
|
||||||
|
buf.clear();
|
||||||
|
let channel = 0.into();
|
||||||
|
let message = *message;
|
||||||
|
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
||||||
|
output[time as usize].push(buf.clone());
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
||||||
|
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Phrase editor.
|
/// Phrase editor.
|
||||||
pub struct Sequencer {
|
pub struct Sequencer {
|
||||||
pub mode: bool,
|
pub mode: bool,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@
|
||||||
//! * [LV2Plugin::load_edn]
|
//! * [LV2Plugin::load_edn]
|
||||||
|
|
||||||
use crate::{core::*, model::*, App};
|
use crate::{core::*, model::*, App};
|
||||||
use crate::devices::sampler::{Sampler, Sample, read_sample_data};
|
use crate::devices::{
|
||||||
|
arranger::Scene,
|
||||||
|
sequencer::Phrase,
|
||||||
|
sampler::{Sampler, Sample, read_sample_data}
|
||||||
|
};
|
||||||
use crate::devices::plugin::{Plugin, LV2Plugin};
|
use crate::devices::plugin::{Plugin, LV2Plugin};
|
||||||
use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ pub struct JackDevice {
|
||||||
/// The "real" readable/writable `Port`s are owned by the `state`.
|
/// The "real" readable/writable `Port`s are owned by the `state`.
|
||||||
pub ports: UnownedJackPorts,
|
pub ports: UnownedJackPorts,
|
||||||
}
|
}
|
||||||
|
impl std::fmt::Debug for JackDevice {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("JackDevice").field("ports", &self.ports).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
render!(JackDevice |self, buf, area| {
|
render!(JackDevice |self, buf, area| {
|
||||||
self.state.read().unwrap().render(buf, area)
|
self.state.read().unwrap().render(buf, area)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub struct JackPorts {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
/// Collection of JACK ports as [Unowned].
|
/// Collection of JACK ports as [Unowned].
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct UnownedJackPorts {
|
pub struct UnownedJackPorts {
|
||||||
pub audio_ins: BTreeMap<String, Port<Unowned>>,
|
pub audio_ins: BTreeMap<String, Port<Unowned>>,
|
||||||
pub midi_ins: BTreeMap<String, Port<Unowned>>,
|
pub midi_ins: BTreeMap<String, Port<Unowned>>,
|
||||||
|
|
|
||||||
99
src/model.rs
99
src/model.rs
|
|
@ -123,7 +123,7 @@ impl App {
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
pub enum AppFocus {
|
pub enum AppFocus {
|
||||||
/// The transport is selected.
|
/// The transport is selected.
|
||||||
Transport,
|
Transport,
|
||||||
/// The arranger is selected.
|
/// The arranger is selected.
|
||||||
Arranger,
|
Arranger,
|
||||||
/// The sequencer is selected.
|
/// The sequencer is selected.
|
||||||
|
|
@ -156,6 +156,7 @@ impl AppFocus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sequencer track.
|
/// A sequencer track.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Play input through output.
|
/// Play input through output.
|
||||||
|
|
@ -366,102 +367,6 @@ impl Track {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// A MIDI sequence.
|
|
||||||
pub struct Phrase {
|
|
||||||
pub name: String,
|
|
||||||
pub length: usize,
|
|
||||||
pub notes: PhraseData,
|
|
||||||
pub looped: Option<(usize, usize)>,
|
|
||||||
/// Immediate note-offs in view
|
|
||||||
pub percussive: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Phrase {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self::new("", 0, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Phrase {
|
|
||||||
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
length,
|
|
||||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
||||||
looped: Some((0, length)),
|
|
||||||
percussive: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
||||||
if pulse >= self.length {
|
|
||||||
panic!("extend phrase first")
|
|
||||||
}
|
|
||||||
self.notes[pulse].push(message);
|
|
||||||
}
|
|
||||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
||||||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
||||||
//panic!("{:?} {start} {end}", &self);
|
|
||||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
||||||
for event in events.iter() {
|
|
||||||
match event {
|
|
||||||
MidiMessage::NoteOn {key,..} => {
|
|
||||||
if *key == k {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
/// Write a chunk of MIDI events to an output port.
|
|
||||||
pub fn process_out (
|
|
||||||
&self,
|
|
||||||
output: &mut MIDIChunk,
|
|
||||||
notes_on: &mut [bool;128],
|
|
||||||
timebase: &Arc<Timebase>,
|
|
||||||
(frame0, frames, _): (usize, usize, f64),
|
|
||||||
) {
|
|
||||||
let mut buf = Vec::with_capacity(8);
|
|
||||||
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
|
||||||
frame0, frame0 + frames
|
|
||||||
) {
|
|
||||||
let tick = tick % self.length;
|
|
||||||
for message in self.notes[tick].iter() {
|
|
||||||
buf.clear();
|
|
||||||
let channel = 0.into();
|
|
||||||
let message = *message;
|
|
||||||
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
|
||||||
output[time as usize].push(buf.clone());
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
|
||||||
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A collection of phrases to play on each track.
|
|
||||||
pub struct Scene {
|
|
||||||
pub name: String,
|
|
||||||
pub clips: Vec<Option<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scene {
|
|
||||||
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.as_ref().into(),
|
|
||||||
clips: clips.as_ref().iter().map(|x|x.clone()).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
||||||
impl $A<$T> {
|
impl $A<$T> {
|
||||||
pub fn start_inc (&mut self) -> $T {
|
pub fn start_inc (&mut self) -> $T {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue