mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
465 lines
17 KiB
Rust
465 lines
17 KiB
Rust
use crate::*;
|
|
/// MIDI message structural
|
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
/// MIDI message serialized
|
|
pub type PhraseMessage = Vec<u8>;
|
|
/// Collection of serialized MIDI messages
|
|
pub type PhraseChunk = [Vec<PhraseMessage>];
|
|
/// Root level object for standalone `tek_sequencer`
|
|
pub struct Sequencer<E: Engine> {
|
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
pub jack: Option<Arc<JackClient>>,
|
|
/// Which view is focused
|
|
pub focus_cursor: (usize, usize),
|
|
/// Controls the JACK transport.
|
|
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
|
/// Global timebase
|
|
pub clock: Arc<TransportTime>,
|
|
/// Pool of all phrases available to the sequencer
|
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
|
/// Phrase editor view
|
|
pub editor: PhraseEditor<E>,
|
|
}
|
|
/// Sections in the sequencer app that may be focused
|
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
pub enum SequencerFocus {
|
|
/// The transport (toolbar) is focused
|
|
Transport,
|
|
/// The phrase list (pool) is focused
|
|
PhrasePool,
|
|
/// The phrase editor (sequencer) is focused
|
|
PhraseEditor,
|
|
}
|
|
/// Status bar for sequencer app
|
|
pub enum SequencerStatusBar {
|
|
Transport,
|
|
PhrasePool,
|
|
PhraseEditor,
|
|
}
|
|
/// Contains all phrases in a project
|
|
pub struct PhrasePool<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Scroll offset
|
|
pub scroll: usize,
|
|
/// Highlighted phrase
|
|
pub phrase: usize,
|
|
/// Phrases in the pool
|
|
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
|
/// Whether this widget is focused
|
|
pub focused: bool,
|
|
/// Mode switch
|
|
pub mode: Option<PhrasePoolMode>,
|
|
}
|
|
/// Modes for phrase pool
|
|
pub enum PhrasePoolMode {
|
|
/// Renaming a pattern
|
|
Rename(usize, String),
|
|
/// Editing the length of a pattern
|
|
Length(usize, usize, PhraseLengthFocus),
|
|
}
|
|
/// A MIDI sequence.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Phrase {
|
|
pub uuid: uuid::Uuid,
|
|
/// Name of phrase
|
|
pub name: String,
|
|
/// Temporal resolution in pulses per quarter note
|
|
pub ppq: usize,
|
|
/// Length of phrase in pulses
|
|
pub length: usize,
|
|
/// Notes in phrase
|
|
pub notes: PhraseData,
|
|
/// Whether to loop the phrase or play it once
|
|
pub loop_on: bool,
|
|
/// Start of loop
|
|
pub loop_start: usize,
|
|
/// Length of loop
|
|
pub loop_length: usize,
|
|
/// All notes are displayed with minimum length
|
|
pub percussive: bool,
|
|
/// Identifying color of phrase
|
|
pub color: Color,
|
|
}
|
|
/// Contains state for viewing and editing a phrase
|
|
pub struct PhraseEditor<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Phrase being played
|
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
|
/// Length of note that will be inserted, in pulses
|
|
pub note_len: usize,
|
|
/// The full piano keys are rendered to this buffer
|
|
pub keys: Buffer,
|
|
/// The full piano roll is rendered to this buffer
|
|
pub buffer: BigBuffer,
|
|
/// Cursor/scroll/zoom in pitch axis
|
|
pub note_axis: RwLock<FixedAxis<usize>>,
|
|
/// Cursor/scroll/zoom in time axis
|
|
pub time_axis: RwLock<ScaledAxis<usize>>,
|
|
/// Whether this widget is focused
|
|
pub focused: bool,
|
|
/// Whether note enter mode is enabled
|
|
pub entered: bool,
|
|
/// Display mode
|
|
pub mode: bool,
|
|
/// Notes currently held at input
|
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
/// Notes currently held at output
|
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
/// Current position of global playhead
|
|
pub now: Arc<AtomicUsize>,
|
|
/// Width of notes area at last render
|
|
pub width: AtomicUsize,
|
|
/// Height of notes area at last render
|
|
pub height: AtomicUsize,
|
|
}
|
|
/// Phrase player.
|
|
pub struct PhrasePlayer<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Global timebase
|
|
pub clock: Arc<TransportTime>,
|
|
/// Start time and phrase being played
|
|
pub phrase: Option<(AtomicUsize, Option<Arc<RwLock<Phrase>>>)>,
|
|
/// Start time (FIXME move into phrase, using Instant)
|
|
pub started: Option<(usize, usize)>,
|
|
/// Start time and next phrase
|
|
pub next_phrase: Option<(AtomicUsize, 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_out_buf: 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]>>,
|
|
}
|
|
/// Focus layout of sequencer app
|
|
impl<E: Engine> FocusGrid<SequencerFocus> for Sequencer<E> {
|
|
fn cursor (&self) -> (usize, usize) { self.focus_cursor }
|
|
fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor }
|
|
fn layout (&self) -> &[&[SequencerFocus]] { &[
|
|
&[SequencerFocus::Transport],
|
|
&[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor],
|
|
] }
|
|
fn update_focus (&mut self) {
|
|
let focused = *self.focused();
|
|
if let Some(transport) = self.transport.as_ref() {
|
|
transport.write().unwrap().focused = focused == SequencerFocus::Transport
|
|
}
|
|
self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool;
|
|
self.editor.focused = focused == SequencerFocus::PhraseEditor;
|
|
}
|
|
}
|
|
impl<E: Engine> PhrasePool<E> {
|
|
pub fn new () -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
scroll: 0,
|
|
phrase: 0,
|
|
phrases: vec![Arc::new(RwLock::new(Phrase::default()))],
|
|
focused: false,
|
|
mode: None,
|
|
}
|
|
}
|
|
pub fn len (&self) -> usize { self.phrases.len() }
|
|
pub fn phrase (&self) -> &Arc<RwLock<Phrase>> { &self.phrases[self.phrase] }
|
|
pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) }
|
|
pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) }
|
|
pub fn index_before (&self, index: usize) -> usize {
|
|
index.overflowing_sub(1).0.min(self.len() - 1)
|
|
}
|
|
pub fn index_after (&self, index: usize) -> usize {
|
|
(index + 1) % self.len()
|
|
}
|
|
pub fn index_of (&self, phrase: &Phrase) -> Option<usize> {
|
|
for i in 0..self.phrases.len() {
|
|
if *self.phrases[i].read().unwrap() == *phrase { return Some(i) }
|
|
}
|
|
return None
|
|
}
|
|
fn new_phrase (name: Option<&str>, color: Option<Color>) -> Arc<RwLock<Phrase>> {
|
|
Arc::new(RwLock::new(Phrase::new(
|
|
String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color
|
|
)))
|
|
}
|
|
pub fn delete_selected (&mut self) {
|
|
if self.phrase > 0 {
|
|
self.phrases.remove(self.phrase);
|
|
self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1));
|
|
}
|
|
}
|
|
pub fn append_new (&mut self, name: Option<&str>, color: Option<Color>) {
|
|
self.phrases.push(Self::new_phrase(name, color));
|
|
self.phrase = self.phrases.len() - 1;
|
|
}
|
|
pub fn insert_new (&mut self, name: Option<&str>, color: Option<Color>) {
|
|
self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color));
|
|
self.phrase += 1;
|
|
}
|
|
pub fn insert_dup (&mut self) {
|
|
let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate();
|
|
phrase.color = random_color_near(phrase.color, 0.25);
|
|
self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase)));
|
|
self.phrase += 1;
|
|
}
|
|
pub fn randomize_color (&mut self) {
|
|
let mut phrase = self.phrases[self.phrase].write().unwrap();
|
|
phrase.color = random_color();
|
|
}
|
|
pub fn begin_rename (&mut self) {
|
|
self.mode = Some(PhrasePoolMode::Rename(
|
|
self.phrase, self.phrases[self.phrase].read().unwrap().name.clone()
|
|
));
|
|
}
|
|
pub fn begin_length (&mut self) {
|
|
self.mode = Some(PhrasePoolMode::Length(
|
|
self.phrase, self.phrases[self.phrase].read().unwrap().length, PhraseLengthFocus::Bar
|
|
));
|
|
}
|
|
pub fn move_up (&mut self) {
|
|
if self.phrase > 1 {
|
|
self.phrases.swap(self.phrase - 1, self.phrase);
|
|
self.phrase -= 1;
|
|
}
|
|
}
|
|
pub fn move_down (&mut self) {
|
|
if self.phrase < self.phrases.len().saturating_sub(1) {
|
|
self.phrases.swap(self.phrase + 1, self.phrase);
|
|
self.phrase += 1;
|
|
}
|
|
}
|
|
}
|
|
impl<E: Engine> PhraseEditor<E> {
|
|
pub fn new () -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
phrase: None,
|
|
note_len: 24,
|
|
notes_in: Arc::new(RwLock::new([false;128])),
|
|
notes_out: Arc::new(RwLock::new([false;128])),
|
|
keys: keys_vert(),
|
|
buffer: Default::default(),
|
|
focused: false,
|
|
entered: false,
|
|
mode: false,
|
|
now: Arc::new(0.into()),
|
|
width: 0.into(),
|
|
height: 0.into(),
|
|
note_axis: RwLock::new(FixedAxis {
|
|
start: 12,
|
|
point: Some(36),
|
|
clamp: Some(127)
|
|
}),
|
|
time_axis: RwLock::new(ScaledAxis {
|
|
start: 00,
|
|
point: Some(00),
|
|
clamp: Some(000),
|
|
scale: 24
|
|
}),
|
|
}
|
|
}
|
|
pub fn note_cursor_inc (&self) {
|
|
let mut axis = self.note_axis.write().unwrap();
|
|
axis.point_dec(1);
|
|
if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } }
|
|
}
|
|
pub fn note_cursor_dec (&self) {
|
|
let mut axis = self.note_axis.write().unwrap();
|
|
axis.point_inc(1);
|
|
if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } }
|
|
}
|
|
pub fn note_page_up (&self) {
|
|
let mut axis = self.note_axis.write().unwrap();
|
|
axis.start_dec(3);
|
|
axis.point_dec(3);
|
|
}
|
|
pub fn note_page_down (&self) {
|
|
let mut axis = self.note_axis.write().unwrap();
|
|
axis.start_inc(3);
|
|
axis.point_inc(3);
|
|
}
|
|
pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); }
|
|
pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); }
|
|
pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) }
|
|
pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) }
|
|
pub fn time_cursor_advance (&self) {
|
|
let point = self.time_axis.read().unwrap().point;
|
|
let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
|
let forward = |time|(time + self.note_len) % length;
|
|
self.time_axis.write().unwrap().point = point.map(forward);
|
|
}
|
|
pub fn time_cursor_inc (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().point_inc(scale);
|
|
}
|
|
pub fn time_cursor_dec (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().point_dec(scale);
|
|
}
|
|
pub fn time_scroll_inc (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().start_inc(scale);
|
|
}
|
|
pub fn time_scroll_dec (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().start_dec(scale);
|
|
}
|
|
pub fn time_zoom_in (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().scale = prev_note_length(scale)
|
|
}
|
|
pub fn time_zoom_out (&self) {
|
|
let scale = self.time_axis.read().unwrap().scale;
|
|
self.time_axis.write().unwrap().scale = next_note_length(scale)
|
|
}
|
|
}
|
|
impl Phrase {
|
|
pub fn new (
|
|
name: impl AsRef<str>,
|
|
loop_on: bool,
|
|
length: usize,
|
|
notes: Option<PhraseData>,
|
|
color: Option<Color>,
|
|
) -> Self {
|
|
Self {
|
|
uuid: uuid::Uuid::new_v4(),
|
|
name: name.as_ref().to_string(),
|
|
ppq: PPQ,
|
|
length,
|
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
loop_on,
|
|
loop_start: 0,
|
|
loop_length: length,
|
|
percussive: true,
|
|
color: color.unwrap_or_else(random_color)
|
|
}
|
|
}
|
|
pub fn duplicate (&self) -> Self {
|
|
let mut clone = self.clone();
|
|
clone.uuid = uuid::Uuid::new_v4();
|
|
clone
|
|
}
|
|
pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; }
|
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
if pulse >= self.length { panic!("extend phrase first") }
|
|
self.notes[pulse].push(message);
|
|
}
|
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
//panic!("{:?} {start} {end}", &self);
|
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
for event in events.iter() {
|
|
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
impl Default for Phrase {
|
|
fn default () -> Self { Self::new("(empty)", false, 0, None, Some(Color::Rgb(0, 0, 0))) }
|
|
}
|
|
impl std::cmp::PartialEq for Phrase {
|
|
fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid }
|
|
}
|
|
impl Eq for Phrase {}
|
|
impl<E: Engine> PhrasePlayer<E> {
|
|
pub fn new (clock: &Arc<TransportTime>) -> Self {
|
|
Self {
|
|
_engine: Default::default(),
|
|
clock: clock.clone(),
|
|
phrase: None,
|
|
started: 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,
|
|
midi_inputs: vec![],
|
|
midi_outputs: vec![],
|
|
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
|
reset: true,
|
|
}
|
|
}
|
|
pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; }
|
|
pub fn toggle_record (&mut self) { self.recording = !self.recording; }
|
|
pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; }
|
|
pub fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
|
let start = self.clock.next_launch_pulse();
|
|
self.next_phrase = Some((start.into(), phrase.map(|p|p.clone())));
|
|
self.reset = true;
|
|
}
|
|
pub fn frames_since_start (&self) -> Option<usize> {
|
|
self.phrase.as_ref()
|
|
.map(|(started,_)|started.load(Ordering::Relaxed))
|
|
.map(|started|started - self.clock.sample())
|
|
}
|
|
pub fn playing_phrase (&self) -> Option<(usize, Arc<RwLock<Phrase>>)> {
|
|
if let (
|
|
Some(TransportState::Rolling),
|
|
Some((start_frame, _)),
|
|
Some((_started, Some(ref phrase)))
|
|
) = (*self.clock.playing.read().unwrap(), self.started, &self.phrase) {
|
|
Some((start_frame, phrase.clone()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
/// Displays and edits phrase length
|
|
pub struct PhraseLength<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
/// Pulses per beat (quaver)
|
|
pub ppq: usize,
|
|
/// Beats per bar
|
|
pub bpb: usize,
|
|
/// Length of phrase in pulses
|
|
pub pulses: usize,
|
|
/// Selected subdivision
|
|
pub focus: Option<PhraseLengthFocus>,
|
|
}
|
|
impl<E: Engine> PhraseLength<E> {
|
|
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
|
|
Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus }
|
|
}
|
|
pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) }
|
|
pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq }
|
|
pub fn ticks (&self) -> usize { self.pulses % self.ppq }
|
|
pub fn bars_string (&self) -> String { format!("{}", self.bars()) }
|
|
pub fn beats_string (&self) -> String { format!("{}", self.beats()) }
|
|
pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) }
|
|
}
|
|
#[derive(Copy,Clone)]
|
|
pub enum PhraseLengthFocus {
|
|
/// Editing the number of bars
|
|
Bar,
|
|
/// Editing the number of beats
|
|
Beat,
|
|
/// Editing the number of ticks
|
|
Tick,
|
|
}
|
|
impl PhraseLengthFocus {
|
|
pub fn next (&mut self) {
|
|
*self = match self {
|
|
Self::Bar => Self::Beat,
|
|
Self::Beat => Self::Tick,
|
|
Self::Tick => Self::Bar,
|
|
}
|
|
}
|
|
pub fn prev (&mut self) {
|
|
*self = match self {
|
|
Self::Bar => Self::Tick,
|
|
Self::Beat => Self::Bar,
|
|
Self::Tick => Self::Beat,
|
|
}
|
|
}
|
|
}
|