Compare commits

..

15 commits

33 changed files with 1425 additions and 1136 deletions

View file

@ -3,13 +3,15 @@
(info "A sequencer with built-in sampler.") (info "A sequencer with built-in sampler.")
(view (view
(bsp/a :view-dialog (bsp/a :view-dialog
(bsp/s (fixed/y 1 :view-transport) (bsp/s (fixed/y 1 :view-transport)
(bsp/n (fixed/y 1 :view-status) (bsp/n (fixed/y 1 :view-status)
(bsp/n (fixed/y 5 :view-sample-viewer) (bsp/w :view-meters-output
(bsp/w (fixed/x :w-sidebar :view-pool) (bsp/e :view-meters-input
(bsp/e :view-samples-keys (bsp/n (fixed/y 5 :view-sample-viewer)
(fill/y :view-editor)))))))) (bsp/w (fixed/x :w-sidebar :view-pool)
(bsp/e :view-samples-keys
(fill/y :view-editor))))))))))
(keys (keys
(layer-if :focus-pool-import "./keys_pool_file.edn") (layer-if :focus-pool-import "./keys_pool_file.edn")

View file

@ -58,7 +58,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..)))
} }
fn editor_pitch (&self) -> Option<u7> { fn editor_pitch (&self) -> Option<u7> {
Some((self.editor().map(|e|e.note_pos()).unwrap() as u8).into()) Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into())
} }
/// Width of display /// Width of display
pub(crate) fn w (&self) -> u16 { pub(crate) fn w (&self) -> u16 {
@ -202,78 +202,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm
} }
} }
#[tengri_proc::expose] impl MidiEditor {
fn _todo_opt_clip_stub (&self) -> Option<Arc<RwLock<MidiClip>>> {
todo!()
}
fn time_lock (&self) -> bool {
self.get_time_lock()
}
fn time_lock_toggled (&self) -> bool {
!self.get_time_lock()
}
fn note_length (&self) -> usize {
self.get_note_len()
}
fn note_pos (&self) -> usize {
self.get_note_pos()
}
fn note_pos_next (&self) -> usize {
self.get_note_pos() + 1
}
fn note_pos_next_octave (&self) -> usize {
self.get_note_pos() + 12
}
fn note_pos_prev (&self) -> usize {
self.get_note_pos().saturating_sub(1)
}
fn note_pos_prev_octave (&self) -> usize {
self.get_note_pos().saturating_sub(12)
}
fn note_len (&self) -> usize {
self.get_note_len()
}
fn note_len_next (&self) -> usize {
self.get_note_len() + 1
}
fn note_len_prev (&self) -> usize {
self.get_note_len().saturating_sub(1)
}
fn note_range (&self) -> usize {
self.get_note_axis()
}
fn note_range_next (&self) -> usize {
self.get_note_axis() + 1
}
fn note_range_prev (&self) -> usize {
self.get_note_axis().saturating_sub(1)
}
fn time_pos (&self) -> usize {
self.get_time_pos()
}
fn time_pos_next (&self) -> usize {
self.get_time_pos() + self.time_zoom()
}
fn time_pos_prev (&self) -> usize {
self.get_time_pos().saturating_sub(self.time_zoom())
}
fn time_zoom (&self) -> usize {
self.get_time_zoom()
}
fn time_zoom_next (&self) -> usize {
self.get_time_zoom() + 1
}
fn time_zoom_prev (&self) -> usize {
self.get_time_zoom().saturating_sub(1).max(1)
}
}
#[tengri_proc::command(App)] impl AppCommand { #[tengri_proc::command(App)] impl AppCommand {
fn toggle_help (app: &mut App, value: bool) -> Perhaps<Self> { fn toggle_help (app: &mut App, value: bool) -> Perhaps<Self> {
app.toggle_dialog(Some(Dialog::Help)); app.toggle_dialog(Some(Dialog::Help));
@ -469,7 +397,7 @@ impl<'state> Context<'state, SamplerCommand> for App {
Ok(None) Ok(None)
} }
fn stop (app: &mut App, index: usize) -> Perhaps<Self> { fn stop (app: &mut App, index: usize) -> Perhaps<Self> {
app.tracks[index].player.enqueue_next(None); app.tracks[index].sequencer.enqueue_next(None);
Ok(None) Ok(None)
} }
fn add (app: &mut App) -> Perhaps<Self> { fn add (app: &mut App) -> Perhaps<Self> {
@ -535,7 +463,7 @@ impl<'state> Context<'state, SamplerCommand> for App {
} }
fn enqueue (app: &mut App, a: usize, b: usize) -> Perhaps<Self> { fn enqueue (app: &mut App, a: usize, b: usize) -> Perhaps<Self> {
//(Enqueue [t: usize, s: usize] //(Enqueue [t: usize, s: usize]
//cmd!(app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref()))) //cmd!(app.tracks[t].sequencer.enqueue_next(app.scenes[s].clips[t].as_ref())))
//("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
todo!() todo!()
} }
@ -818,56 +746,3 @@ impl<'state> Context<'state, SamplerCommand> for App {
todo!() todo!()
} }
} }
#[tengri_proc::command(MidiEditor)] impl MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the clip
fn note_append (editor: &mut MidiEditor) -> Perhaps<Self> {
editor.put_note(true);
Ok(None)
}
fn note_put (editor: &mut MidiEditor) -> Perhaps<Self> {
editor.put_note(false);
Ok(None)
}
fn note_del (editor: &mut MidiEditor) -> Perhaps<Self> {
todo!()
}
fn note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps<Self> {
editor.set_note_pos(pos.min(127));
Ok(None)
}
fn note_len (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
//let note_len = editor.get_note_len();
//let time_zoom = editor.get_time_zoom();
editor.set_note_len(value);
//if note_len / time_zoom != x / time_zoom {
editor.redraw();
//}
Ok(None)
}
fn note_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_note_lo(value.min(127));
Ok(None)
}
fn time_pos (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_pos(value);
Ok(None)
}
fn time_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_start(value);
Ok(None)
}
fn time_zoom (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_zoom(value);
editor.redraw();
Ok(None)
}
fn time_lock (editor: &mut MidiEditor, value: bool) -> Perhaps<Self> {
editor.set_time_lock(value);
Ok(None)
}
fn show (editor: &mut MidiEditor, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<Self> {
editor.set_clip(clip.as_ref());
Ok(None)
}
}

View file

@ -1,74 +1,14 @@
use crate::*; use crate::*;
impl HasJack for App { fn jack (&self) -> &Jack { &self.jack } }
audio!( audio!(
|self: App, client, scope|{ |self: App, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0(); let t0 = self.perf.get_t0();
// Update transport clock
self.clock().update_from_scope(scope).unwrap(); self.clock().update_from_scope(scope).unwrap();
// Collect MIDI input (TODO preallocate) let midi_in = self.collect_midi_input(scope);
let midi_in = self.midi_ins.iter() self.update_editor_cursor(&midi_in);
.map(|port|port.port().iter(scope) let result = self.render_tracks(client, scope);
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>();
// Update standalone MIDI sequencer
//if let Some(player) = self.player.as_mut() {
//if Control::Quit == PlayerAudio(
//player,
//&mut self.note_buf,
//&mut self.midi_buf,
//).process(client, scope) {
//return Control::Quit
//}
//}
// Update standalone sampler
//if let Some(sampler) = self.sampler.as_mut() {
//if Control::Quit == SamplerAudio(sampler).process(client, scope) {
//return Control::Quit
//}
//for port in midi_in.iter() {
//for message in port.iter() {
//match message {
//Ok(M
//}
//}
//}
//}
// TODO move these to editor and sampler?:
//for port in midi_in.iter() {
//for event in port.iter() {
//match event {
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
//editor.set_note_pos(key.as_int() as usize);
//},
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
//self.editor.as_ref(),
//self.sampler.as_ref(),
//) => {
//// TODO: give sampler its own cursor
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
//sample.write().unwrap().handle_cc(*controller, *value)
//}
//}
//_ =>{}
//},
//_ =>{}
//}
//}
//}
// Update track sequencers
for track in self.tracks.iter_mut() {
if PlayerAudio(
track.player_mut(), &mut self.note_buf, &mut self.midi_buf
).process(client, scope) == Control::Quit {
return Control::Quit
}
}
// End profiling cycle
self.perf.update_from_jack_scope(t0, scope); self.perf.update_from_jack_scope(t0, scope);
Control::Continue result
}; };
|self, event|{ |self, event|{
use JackEvent::*; use JackEvent::*;
@ -93,3 +33,60 @@ audio!(
} }
} }
); );
type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
impl App {
/// Collect MIDI input from app ports (TODO preallocate large buffers)
fn collect_midi_input <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
self.midi_ins.iter()
.map(|port|port.port().iter(scope)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
}
/// Update cursor in MIDI editor
fn update_editor_cursor (&self, midi_in: &CollectedMidiInput) {
if let Some(editor) = &self.editor {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {ref key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
}
/// Run audio callbacks for every track and every device
fn render_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
for track in self.tracks.iter_mut() {
if Control::Quit == PlayerAudio(
track.sequencer_mut(), &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
for device in track.devices.iter_mut() {
if Control::Quit == DeviceAudio(device).process(client, scope) {
return Control::Quit
}
}
}
Control::Continue
}
}
impl HasJack for App {
fn jack (&self) -> &Jack {
&self.jack
}
}

View file

@ -1,7 +1,6 @@
use crate::*; use crate::*;
mod dialog; pub use self::dialog::*; mod dialog; pub use self::dialog::*;
mod editor; pub use self::editor::*;
mod pool; pub use self::pool::*; mod pool; pub use self::pool::*;
mod selection; pub use self::selection::*; mod selection; pub use self::selection::*;
mod track; pub use self::track::*; mod track; pub use self::track::*;
@ -102,7 +101,7 @@ impl App {
let mut track = Track { let mut track = Track {
width: (name.len() + 2).max(12), width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random), color: color.unwrap_or_else(ItemTheme::random),
player: MidiPlayer::new( sequencer: Sequencer::new(
&format!("{name}"), &format!("{name}"),
self.jack(), self.jack(),
Some(self.clock()), Some(self.clock()),
@ -141,7 +140,7 @@ impl App {
let exists = self.tracks().get(index).is_some(); let exists = self.tracks().get(index).is_some();
if exists { if exists {
let track = self.tracks_mut().remove(index); let track = self.tracks_mut().remove(index);
let Track { player: MidiPlayer { midi_ins, midi_outs, .. }, .. } = track; let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
for port in midi_ins.into_iter() { for port in midi_ins.into_iter() {
port.close()?; port.close()?;
} }
@ -196,7 +195,7 @@ impl App {
/// Enqueue clips from a scene across all tracks /// Enqueue clips from a scene across all tracks
pub fn scene_enqueue (&mut self, scene: usize) { pub fn scene_enqueue (&mut self, scene: usize) {
for track in 0..self.tracks.len() { for track in 0..self.tracks.len() {
self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref()); self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref());
} }
} }
@ -315,7 +314,7 @@ impl App {
/// Stop all playing clips /// Stop all playing clips
pub(crate) fn stop_all (&mut self) { pub(crate) fn stop_all (&mut self) {
for track in 0..self.tracks.len() { for track in 0..self.tracks.len() {
self.tracks[track].player.enqueue_next(None); self.tracks[track].sequencer.enqueue_next(None);
} }
} }
@ -324,14 +323,14 @@ impl App {
use Selection::*; use Selection::*;
match self.selected { match self.selected {
Track(t) => { Track(t) => {
self.tracks[t].player.enqueue_next(None) self.tracks[t].sequencer.enqueue_next(None)
}, },
TrackClip { track, scene } => { TrackClip { track, scene } => {
self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref()) self.tracks[track].sequencer.enqueue_next(self.scenes[scene].clips[track].as_ref())
}, },
Scene(s) => { Scene(s) => {
for t in 0..self.tracks.len() { for t in 0..self.tracks.len() {
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref()) self.tracks[t].sequencer.enqueue_next(self.scenes[s].clips[t].as_ref())
} }
}, },
_ => {} _ => {}
@ -417,7 +416,7 @@ impl App {
fn device_add_sampler (&mut self) -> Usually<()> { fn device_add_sampler (&mut self) -> Usually<()> {
let name = self.jack.with_client(|c|c.name().to_string()); let name = self.jack.with_client(|c|c.name().to_string());
let midi = self.track().expect("no active track").player.midi_outs[0].name(); let midi = self.track().expect("no active track").sequencer.midi_outs[0].name();
let sampler = if let Ok(sampler) = Sampler::new( let sampler = if let Ok(sampler) = Sampler::new(
&self.jack, &self.jack,
&format!("{}/Sampler", &self.track().expect("no active track").name), &format!("{}/Sampler", &self.track().expect("no active track").name),

View file

@ -24,7 +24,7 @@ impl Scene {
Some(c) => tracks Some(c) => tracks
.get(track_index) .get(track_index)
.map(|track|{ .map(|track|{
if let Some((_, Some(clip))) = track.player().play_clip() { if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap() *clip.read().unwrap() == *c.read().unwrap()
} else { } else {
false false

View file

@ -7,8 +7,8 @@ use crate::*;
pub width: usize, pub width: usize,
/// Identifying color of track /// Identifying color of track
pub color: ItemTheme, pub color: ItemTheme,
/// MIDI player state /// MIDI sequencer state
pub player: MidiPlayer, pub sequencer: Sequencer,
/// Device chain /// Device chain
pub devices: Vec<Device>, pub devices: Vec<Device>,
/// Inputs of 1st device /// Inputs of 1st device
@ -17,65 +17,62 @@ use crate::*;
pub audio_outs: Vec<JackAudioOut>, pub audio_outs: Vec<JackAudioOut>,
} }
has_clock!(|self: Track|self.player.clock); has_clock!(|self: Track|self.sequencer.clock);
has_player!(|self: Track|self.player); has_sequencer!(|self: Track|self.sequencer);
impl Track { impl Track {
pub const MIN_WIDTH: usize = 9; /// Create a new track with only the default [Sequencer].
/// Create a new track containing a sequencer. pub fn new (
pub fn new_sequencer () -> Self { name: &impl AsRef<str>,
let mut track = Self::default(); color: Option<ItemTheme>,
track.devices.push(Device::Sequencer(MidiPlayer::default())); jack: &Jack,
track clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(
format!("{}/sequencer", name.as_ref()),
jack,
clock,
clip,
midi_from,
midi_to
)?,
..Default::default()
})
} }
/// Create a new track containing a sequencer and sampler. /// Create a new track connecting the [Sequencer] to a [Sampler].
pub fn new_groovebox ( pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack, jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect], midi_from: &[PortConnect],
midi_to: &[PortConnect],
audio_from: &[&[PortConnect];2], audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2], audio_to: &[&[PortConnect];2],
) -> Usually<Self> { ) -> Usually<Self> {
let mut track = Self::new_sequencer(); let mut track = Self::new(
track.devices.push(Device::Sampler( name, color, jack, clock, clip, midi_from, midi_to
Sampler::new(jack, &"sampler", midi_from, audio_from, audio_to)? )?;
)); track.devices.push(Device::Sampler(Sampler::new(
jack,
&format!("{}/sampler", name.as_ref()),
&[PortConnect::exact(format!("{}:{}",
jack.with_client(|c|c.name().to_string()),
track.sequencer.midi_outs[0].name()
))],
audio_from,
audio_to
)?));
Ok(track) Ok(track)
} }
/// Create a new track containing a sampler.
pub fn new_sampler (
jack: &Jack,
midi_from: &[PortConnect],
audio_from: &[&[PortConnect];2],
audio_to: &[&[PortConnect];2],
) -> Usually<Self> {
let mut track = Self::default();
track.devices.push(Device::Sampler(
Sampler::new(jack, &"sampler", midi_from, audio_from, audio_to)?
));
Ok(track)
}
pub fn width_inc (&mut self) {
self.width += 1;
}
pub fn width_dec (&mut self) {
if self.width > Track::MIN_WIDTH {
self.width -= 1;
}
}
pub fn sequencer (&self, mut nth: usize) -> Option<&MidiPlayer> {
for device in self.devices.iter() {
match device {
Device::Sequencer(s) => if nth == 0 {
return Some(s);
} else {
nth -= 1;
},
_ => {}
}
}
None
}
pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() { for device in self.devices.iter() {
match device { match device {
@ -104,6 +101,26 @@ impl Track {
} }
} }
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) {
self.width += 1;
}
fn width_dec (&mut self) {
if self.width > Track::MIN_WIDTH {
self.width -= 1;
}
}
}
pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync {
fn midi_ins (&self) -> &Vec<JackMidiIn>; fn midi_ins (&self) -> &Vec<JackMidiIn>;
fn midi_outs (&self) -> &Vec<JackMidiOut>; fn midi_outs (&self) -> &Vec<JackMidiOut>;
@ -130,14 +147,14 @@ pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync
fn track_toggle_record (&mut self) { fn track_toggle_record (&mut self) {
if let Some(t) = self.selected().track() { if let Some(t) = self.selected().track() {
let tracks = self.tracks_mut(); let tracks = self.tracks_mut();
tracks[t-1].player.recording = !tracks[t-1].player.recording; tracks[t-1].sequencer.recording = !tracks[t-1].sequencer.recording;
} }
} }
/// Toggle track monitoring /// Toggle track monitoring
fn track_toggle_monitor (&mut self) { fn track_toggle_monitor (&mut self) {
if let Some(t) = self.selected().track() { if let Some(t) = self.selected().track() {
let tracks = self.tracks_mut(); let tracks = self.tracks_mut();
tracks[t-1].player.monitoring = !tracks[t-1].player.monitoring; tracks[t-1].sequencer.monitoring = !tracks[t-1].sequencer.monitoring;
} }
} }
} }

View file

@ -56,6 +56,12 @@ impl App {
))) )))
)) ))
} }
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
self.sampler().map(|s|s.view_meters_input())
}
pub fn view_meters_output (&self) -> impl Content<TuiOut> + use<'_> {
self.sampler().map(|s|s.view_meters_output())
}
} }
impl App { impl App {
@ -336,8 +342,8 @@ impl<'a> ArrangerView<'a> {
self.width_mid, self.width_mid,
||self.tracks_with_sizes_scrolled(), ||self.tracks_with_sizes_scrolled(),
move|t, track|{ move|t, track|{
let rec = track.player.recording; let rec = track.sequencer.recording;
let mon = track.player.monitoring; let mon = track.sequencer.monitoring;
let rec = if rec { White } else { track.color.darkest.rgb }; let rec = if rec { White } else { track.color.darkest.rgb };
let mon = if mon { White } else { track.color.darkest.rgb }; let mon = if mon { White } else { track.color.darkest.rgb };
let bg = if self.track_selected == Some(t) { let bg = if self.track_selected == Some(t) {
@ -377,10 +383,10 @@ impl<'a> ArrangerView<'a> {
let label = Align::ne("Next clip:"); let label = Align::ne("Next clip:");
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{ self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{
let queued = track.player.next_clip.is_some(); let queued = track.sequencer.next_clip.is_some();
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
let queued_clip = Thunk::new(||{ let queued_clip = Thunk::new(||{
Tui::bg(Reset, if let Some((_, clip)) = track.player.next_clip.as_ref() { Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() {
if let Some(clip) = clip { if let Some(clip) = clip {
clip.read().unwrap().name.clone() clip.read().unwrap().name.clone()
} else { } else {
@ -927,353 +933,3 @@ content!(TuiOut: |self: ClipLength| {
Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()),
} }
}); });
/// A clip, rendered as a horizontal piano roll.
#[derive(Clone)]
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemTheme,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((12, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
};
piano.redraw();
piano
}
}
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))
}
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline()
),
Bsp::e(
self.keys(),
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
Fill::xy(self.notes()),
Fill::xy(self.cursor()),
)))
),
)));
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &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(clip.color.darkest.rgb);
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.rgb);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('—');
} else {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.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(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
fn notes (&self) -> impl Content<TuiOut> {
let time_start = self.get_time_start();
let note_lo = self.get_note_lo();
let note_hi = self.get_note_hi();
let buffer = self.buffer.clone();
ThunkRender::new(move|to: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, _h] = to.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
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) {
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) {
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
}
fn cursor (&self) -> impl Content<TuiOut> {
let note_hi = self.get_note_hi();
let note_lo = self.get_note_lo();
let note_pos = self.get_note_pos();
let note_len = self.get_note_len();
let time_pos = self.get_time_pos();
let time_start = self.get_time_start();
let time_zoom = self.get_time_zoom();
let style = Some(Style::default().fg(self.color.lightest.rgb));
ThunkRender::new(move|to: &mut TuiOut|{
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_pos {
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_pos && time_pos < 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
}
}
})
}
fn keys (&self) -> impl Content<TuiOut> {
let state = self;
let color = state.color;
let note_lo = state.get_note_lo();
let note_hi = state.get_note_hi();
let note_pos = state.get_note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(Tui::g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y0, _w, _h] = to.area().xywh();
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_pos {
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
};
}
})))
}
fn timeline (&self) -> impl Content<TuiOut> + '_ {
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, w, _h] = to.area();
let style = Some(Style::default().dim());
let length = self.clip.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.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
})))
}
}
has_size!(<TuiOut>|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) -> &AtomicUsize { self.point.note_len() }
fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() }
}
impl MidiViewer for PianoHorizontal {
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.get_note_len();
let time_zoom = self.get_time_zoom();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
}
}
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemTheme::G[64]);
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read().unwrap();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
10 | 8 | 6 | 3 | 1 => " ",
_ => unreachable!(),
}
}
pub struct OctaveVertical {
on: [bool; 12],
colors: [Color; 3]
}
impl Default for OctaveVertical {
fn default () -> Self {
Self {
on: [false; 12],
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
}
}
}
impl OctaveVertical {
fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else {
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
}]
}
}
impl Content<TuiOut> for OctaveVertical {
fn content (&self) -> impl Render<TuiOut> {
row!(
Tui::fg_bg(self.color(0), self.color(1), ""),
Tui::fg_bg(self.color(2), self.color(3), ""),
Tui::fg_bg(self.color(4), self.color(5), ""),
Tui::fg_bg(self.color(6), self.color(7), ""),
Tui::fg_bg(self.color(8), self.color(9), ""),
Tui::fg_bg(self.color(10), self.color(11), ""),
)
}
}

View file

@ -84,9 +84,10 @@ impl Cli {
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; let audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
let clip = match mode { let clip = match mode {
LaunchMode::Sequencer | LaunchMode::Groovebox => Some(Arc::new(RwLock::new(MidiClip::new( LaunchMode::Sequencer | LaunchMode::Groovebox =>
"Clip", true, 384usize, None, Some(ItemColor::random().into())), Some(Arc::new(RwLock::new(MidiClip::new(
))), "Clip", true, 384usize, None, Some(ItemColor::random().into())),
))),
_ => None, _ => None,
}; };
let scenes = vec![]; let scenes = vec![];
@ -136,13 +137,28 @@ impl Cli {
}, },
tracks: match mode { tracks: match mode {
LaunchMode::Sequencer => vec![ LaunchMode::Sequencer => vec![
Track::new_sequencer() Track::new(
&name,
None,
jack,
None,
clip.as_ref(),
midi_froms.as_slice(),
midi_tos.as_slice()
)?
], ],
LaunchMode::Groovebox => vec![ LaunchMode::Groovebox | LaunchMode::Sampler => vec![
Track::new_groovebox(jack, midi_froms.as_slice(), audio_froms, audio_tos)? Track::new_with_sampler(
], &name,
LaunchMode::Sampler => vec![ None,
Track::new_sampler(jack, midi_froms.as_slice(), audio_froms, audio_tos)? jack,
None,
clip.as_ref(),
midi_froms.as_slice(),
midi_froms.as_slice(),
audio_froms,
audio_tos,
)?
], ],
_ => vec![] _ => vec![]
}, },

View file

@ -16,10 +16,13 @@ wavers = { workspace = true, optional = true }
winit = { workspace = true, optional = true } winit = { workspace = true, optional = true }
[features] [features]
default = [ "clock", "sequencer", "sampler", "lv2" ] default = [ "clock", "editor", "sequencer", "sampler", "lv2" ]
clock = [] clock = []
sampler = [ "symphonia", "wavers" ] editor = []
meter = []
mixer = []
sequencer = [ "clock", "uuid" ] sequencer = [ "clock", "uuid" ]
sampler = [ "meter", "mixer", "symphonia", "wavers" ]
lv2 = [ "livi", "winit" ] lv2 = [ "livi", "winit" ]
vst2 = [] vst2 = []
vst3 = [] vst3 = []

View file

@ -29,7 +29,7 @@ pub struct Clock {
} }
impl std::fmt::Debug for Clock { impl std::fmt::Debug for Clock {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("Clock") f.debug_struct("Clock")
.field("timebase", &self.timebase) .field("timebase", &self.timebase)
.field("chunk", &self.chunk) .field("chunk", &self.chunk)

View file

@ -0,0 +1,51 @@
use crate::*;
#[derive(Debug)]
pub enum Device {
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "lv2")] // TODO
Lv2(Lv2),
#[cfg(feature = "vst2")] // TODO
Vst2,
#[cfg(feature = "vst3")] // TODO
Vst3,
#[cfg(feature = "clap")] // TODO
Clap,
#[cfg(feature = "sf2")] // TODO
Sf2,
}
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
}
pub struct DeviceAudio<'a>(pub &'a mut Device);
audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*;
match self.0 {
#[cfg(feature = "sampler")]
Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")]
Lv2(lv2) => lv2.process(client, scope),
#[cfg(feature = "vst2")]
Vst2 => { todo!() }, // TODO
#[cfg(feature = "vst3")]
Vst3 => { todo!() }, // TODO
#[cfg(feature = "clap")]
Clap => { todo!() }, // TODO
#[cfg(feature = "sf2")]
Sf2 => { todo!() }, // TODO
}
});

View file

@ -0,0 +1,5 @@
mod editor_api; pub use self::editor_api::*;
mod editor_model; pub use self::editor_model::*;
mod editor_view; //pub use self::editor_view::*;
mod editor_view_h; pub use self::editor_view_h::*;
mod editor_view_v; pub use self::editor_view_v::*;

View file

@ -0,0 +1,126 @@
use crate::*;
#[tengri_proc::expose] impl MidiEditor {
fn _todo_opt_clip_stub (&self) -> Option<Arc<RwLock<MidiClip>>> {
todo!()
}
fn time_lock (&self) -> bool {
self.get_time_lock()
}
fn time_lock_toggled (&self) -> bool {
!self.get_time_lock()
}
fn note_length (&self) -> usize {
self.get_note_len()
}
fn note_pos (&self) -> usize {
self.get_note_pos()
}
fn note_pos_next (&self) -> usize {
self.get_note_pos() + 1
}
fn note_pos_next_octave (&self) -> usize {
self.get_note_pos() + 12
}
fn note_pos_prev (&self) -> usize {
self.get_note_pos().saturating_sub(1)
}
fn note_pos_prev_octave (&self) -> usize {
self.get_note_pos().saturating_sub(12)
}
fn note_len (&self) -> usize {
self.get_note_len()
}
fn note_len_next (&self) -> usize {
self.get_note_len() + 1
}
fn note_len_prev (&self) -> usize {
self.get_note_len().saturating_sub(1)
}
fn note_range (&self) -> usize {
self.get_note_axis()
}
fn note_range_next (&self) -> usize {
self.get_note_axis() + 1
}
fn note_range_prev (&self) -> usize {
self.get_note_axis().saturating_sub(1)
}
fn time_pos (&self) -> usize {
self.get_time_pos()
}
fn time_pos_next (&self) -> usize {
self.get_time_pos() + self.time_zoom()
}
fn time_pos_prev (&self) -> usize {
self.get_time_pos().saturating_sub(self.time_zoom())
}
fn time_zoom (&self) -> usize {
self.get_time_zoom()
}
fn time_zoom_next (&self) -> usize {
self.get_time_zoom() + 1
}
fn time_zoom_prev (&self) -> usize {
self.get_time_zoom().saturating_sub(1).max(1)
}
}
#[tengri_proc::command(MidiEditor)] impl MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the clip
fn note_append (editor: &mut MidiEditor) -> Perhaps<Self> {
editor.put_note(true);
Ok(None)
}
fn note_put (editor: &mut MidiEditor) -> Perhaps<Self> {
editor.put_note(false);
Ok(None)
}
fn note_del (_editor: &mut MidiEditor) -> Perhaps<Self> {
todo!()
}
fn note_pos (editor: &mut MidiEditor, pos: usize) -> Perhaps<Self> {
editor.set_note_pos(pos.min(127));
Ok(None)
}
fn note_len (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
//let note_len = editor.get_note_len();
//let time_zoom = editor.get_time_zoom();
editor.set_note_len(value);
//if note_len / time_zoom != x / time_zoom {
editor.redraw();
//}
Ok(None)
}
fn note_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_note_lo(value.min(127));
Ok(None)
}
fn time_pos (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_pos(value);
Ok(None)
}
fn time_scroll (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_start(value);
Ok(None)
}
fn time_zoom (editor: &mut MidiEditor, value: usize) -> Perhaps<Self> {
editor.set_time_zoom(value);
editor.redraw();
Ok(None)
}
fn time_lock (editor: &mut MidiEditor, value: bool) -> Perhaps<Self> {
editor.set_time_lock(value);
Ok(None)
}
fn show (editor: &mut MidiEditor, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<Self> {
editor.set_clip(clip.as_ref());
Ok(None)
}
}

View file

@ -9,7 +9,7 @@ pub struct MidiEditor {
} }
impl std::fmt::Debug for MidiEditor { impl std::fmt::Debug for MidiEditor {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditor") f.debug_struct("MidiEditor")
.field("mode", &self.mode) .field("mode", &self.mode)
.finish() .finish()
@ -25,15 +25,6 @@ impl Default for MidiEditor {
} }
} }
has_size!(<TuiOut>|self: MidiEditor|&self.size);
content!(TuiOut: |self: MidiEditor| {
self.autoscroll();
//self.autozoom();
self.size.of(&self.mode)
});
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = { from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(clip.clone())); let model = Self::from(Some(clip.clone()));
model.redraw(); model.redraw();
@ -166,3 +157,4 @@ pub trait HasEditor {
} }
}; };
} }

View file

@ -0,0 +1,9 @@
use crate::*;
has_size!(<TuiOut>|self: MidiEditor|&self.size);
content!(TuiOut: |self: MidiEditor| {
self.autoscroll();
//self.autozoom();
self.size.of(&self.mode)
});

View file

@ -0,0 +1,315 @@
use crate::*;
/// A clip, rendered as a horizontal piano roll.
#[derive(Clone)]
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemTheme,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((12, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
};
piano.redraw();
piano
}
}
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))
}
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline()
),
Bsp::e(
self.keys(),
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
Fill::xy(self.notes()),
Fill::xy(self.cursor()),
)))
),
)));
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &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(clip.color.darkest.rgb);
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.rgb);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('—');
} else {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.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(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
fn notes (&self) -> impl Content<TuiOut> {
let time_start = self.get_time_start();
let note_lo = self.get_note_lo();
let note_hi = self.get_note_hi();
let buffer = self.buffer.clone();
ThunkRender::new(move|to: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, _h] = to.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
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) {
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) {
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
}
fn cursor (&self) -> impl Content<TuiOut> {
let note_hi = self.get_note_hi();
let note_lo = self.get_note_lo();
let note_pos = self.get_note_pos();
let note_len = self.get_note_len();
let time_pos = self.get_time_pos();
let time_start = self.get_time_start();
let time_zoom = self.get_time_zoom();
let style = Some(Style::default().fg(self.color.lightest.rgb));
ThunkRender::new(move|to: &mut TuiOut|{
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_pos {
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_pos && time_pos < 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
}
}
})
}
fn keys (&self) -> impl Content<TuiOut> {
let state = self;
let color = state.color;
let note_lo = state.get_note_lo();
let note_hi = state.get_note_hi();
let note_pos = state.get_note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(Tui::g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y0, _w, _h] = to.area().xywh();
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_pos {
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
};
}
})))
}
fn timeline (&self) -> impl Content<TuiOut> + '_ {
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, w, _h] = to.area();
let style = Some(Style::default().dim());
let length = self.clip.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.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
})))
}
}
has_size!(<TuiOut>|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) -> &AtomicUsize { self.point.note_len() }
fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() }
}
impl MidiViewer for PianoHorizontal {
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.get_note_len();
let time_zoom = self.get_time_zoom();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
}
}
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemTheme::G[64]);
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read().unwrap();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
10 | 8 | 6 | 3 | 1 => " ",
_ => unreachable!(),
}
}

View file

@ -0,0 +1,37 @@
use crate::*;
pub struct OctaveVertical {
on: [bool; 12],
colors: [Color; 3]
}
impl Default for OctaveVertical {
fn default () -> Self {
Self {
on: [false; 12],
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
}
}
}
impl OctaveVertical {
fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else {
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
}]
}
}
impl Content<TuiOut> for OctaveVertical {
fn content (&self) -> impl Render<TuiOut> {
row!(
Tui::fg_bg(self.color(0), self.color(1), ""),
Tui::fg_bg(self.color(2), self.color(3), ""),
Tui::fg_bg(self.color(4), self.color(5), ""),
Tui::fg_bg(self.color(6), self.color(7), ""),
Tui::fg_bg(self.color(8), self.color(9), ""),
Tui::fg_bg(self.color(10), self.color(11), ""),
)
}
}

View file

@ -3,7 +3,8 @@
pub(crate) use std::cmp::Ord; pub(crate) use std::cmp::Ord;
pub(crate) use std::fmt::{Debug, Formatter}; pub(crate) use std::fmt::{Debug, Formatter};
pub(crate) use std::thread::JoinHandle; pub(crate) use std::thread::JoinHandle;
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; pub(crate) use std::sync::{Arc, RwLock};
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
pub(crate) use std::fs::File; pub(crate) use std::fs::File;
pub(crate) use std::path::PathBuf; pub(crate) use std::path::PathBuf;
pub(crate) use std::error::Error; pub(crate) use std::error::Error;
@ -14,16 +15,29 @@ pub(crate) use ::tek_engine::*;
pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage};
pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi};
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
pub(crate) use Color::*;
mod device;
pub use self::device::*;
#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] mod clock;
#[cfg(feature = "clock")] pub use self::clock::*; #[cfg(feature = "clock")] pub use self::clock::*;
#[cfg(feature = "editor")] mod editor;
#[cfg(feature = "editor")] pub use self::editor::*;
#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] mod sequencer;
#[cfg(feature = "sequencer")] pub use self::sequencer::*; #[cfg(feature = "sequencer")] pub use self::sequencer::*;
#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] mod sampler;
#[cfg(feature = "sampler")] pub use self::sampler::*; #[cfg(feature = "sampler")] pub use self::sampler::*;
#[cfg(feature = "meter")] mod meter;
#[cfg(feature = "meter")] pub use self::meter::*;
#[cfg(feature = "mixer")] mod mixer;
#[cfg(feature = "mixer")] pub use self::mixer::*;
#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*; #[cfg(feature = "lv2")] pub use self::lv2::*;
@ -38,23 +52,3 @@ pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}
#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] mod clap;
#[cfg(feature = "clap")] pub use self::clap::*; #[cfg(feature = "clap")] pub use self::clap::*;
#[derive(Debug)]
pub enum Device {
#[cfg(feature = "sequencer")] Sequencer(MidiPlayer),
#[cfg(feature = "sampler")] Sampler(Sampler),
#[cfg(feature = "lv2")] Lv2(Lv2), // TODO
#[cfg(feature = "vst2")] Vst2, // TODO
#[cfg(feature = "vst3")] Vst3, // TODO
#[cfg(feature = "clap")] Clap, // TODO
#[cfg(feature = "sf2")] Sf2, // TODO
}
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
}

View file

@ -1,5 +1,5 @@
mod lv2_model; pub use self::lv2_model::*; mod lv2_model; pub use self::lv2_model::*;
mod lv2_audio; pub use self::lv2_audio::*; mod lv2_audio; //pub use self::lv2_audio::*;
mod lv2_gui; pub use self::lv2_gui::*; mod lv2_gui; pub use self::lv2_gui::*;
mod lv2_tui; pub use self::lv2_tui::*; mod lv2_tui; //pub use self::lv2_tui::*;
pub(self) use std::thread::JoinHandle; //pub(self) use std::thread::JoinHandle;

View file

@ -1,5 +1,4 @@
use crate::*; use crate::*;
use super::*;
/// A LV2 plugin. /// A LV2 plugin.
#[derive(Debug)] #[derive(Debug)]

View file

@ -0,0 +1,54 @@
use crate::*;
#[derive(Debug, Default)]
pub enum MeteringMode {
#[default]
Rms,
Log10,
}
#[derive(Debug, Default, Clone)]
pub struct Log10Meter(pub f32);
render!(TuiOut: |self: Log10Meter, to| {
let [x, y, w, h] = to.area();
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32 / 100.0).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"", x, y2 - y, Some(Style::default().green()));
}
}
});
pub fn to_log10 (samples: &[f32]) -> f32 {
let total: f32 = samples.iter().map(|x|x.abs()).sum();
let count = samples.len() as f32;
10. * (total / count).log10()
}
#[derive(Debug, Default, Clone)]
pub struct RmsMeter(pub f32);
render!(TuiOut: |self: RmsMeter, to| {
let [x, y, w, h] = to.area();
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"", x, y2.saturating_sub(y), Some(Style::default().green()));
}
}
});
pub fn to_rms (samples: &[f32]) -> f32 {
let sum = samples.iter()
.map(|s|*s)
.reduce(|sum, sample|sum + sample.abs())
.unwrap_or(0.0);
(sum / samples.len() as f32).sqrt()
}

View file

@ -0,0 +1,41 @@
#[derive(Debug, Default)]
pub enum MixingMode {
#[default]
Summing,
Average,
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
buffer[channel][index] += sample * gain;
}
} else {
return false
}
}
true
}
pub fn mix_average <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
let value = buffer[channel][index];
buffer[channel][index] = (value + sample * gain) / 2.0;
}
} else {
return false
}
}
true
}

View file

@ -1,5 +1,3 @@
use crate::*;
pub(crate) use symphonia::{ pub(crate) use symphonia::{
core::{ core::{
formats::Packet, formats::Packet,

View file

@ -1,8 +1,5 @@
use crate::*; use crate::*;
macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; }
macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; }
#[tengri_proc::expose] #[tengri_proc::expose]
impl Sampler { impl Sampler {
//fn file_browser_filter (&self) -> Arc<str> { //fn file_browser_filter (&self) -> Arc<str> {
@ -12,7 +9,7 @@ impl Sampler {
//todo!(); //todo!();
//} //}
///// Immutable reference to sample at cursor. ///// Immutable reference to sample at cursor.
//fn sample_selected (&self) -> MaybeSample { //fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
//for (i, sample) in self.mapped.iter().enumerate() { //for (i, sample) in self.mapped.iter().enumerate() {
//if i == self.cursor().0 { //if i == self.cursor().0 {
//return sample.as_ref() //return sample.as_ref()
@ -60,14 +57,11 @@ impl SamplerCommand {
Self::record_begin(sampler, sample) Self::record_begin(sampler, sample)
} }
} }
fn record_begin (sampler: &mut Sampler, sample: usize) -> Perhaps<Self> { fn record_begin (sampler: &mut Sampler, pitch: usize) -> Perhaps<Self> {
sampler.recording = Some(( sampler.recording = Some((
sample, pitch,
Arc::new(RwLock::new(Sample::new( Arc::new(RwLock::new(Sample::new(
"Sample", "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
0,
0,
vec![vec![];sampler.audio_ins.len()]
))) )))
)); ));
Ok(None) Ok(None)
@ -91,7 +85,7 @@ impl SamplerCommand {
//Self::Select(state.set_note_pos(i)) //Self::Select(state.set_note_pos(i))
//} //}
///// Assign sample to pitch ///// Assign sample to pitch
//fn set (&self, pitch: u7, sample: MaybeSample) -> Option<Self> { //fn set (&self, pitch: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
//let i = pitch.as_int() as usize; //let i = pitch.as_int() as usize;
//let old = self.mapped[i].clone(); //let old = self.mapped[i].clone();
//self.mapped[i] = sample; //self.mapped[i] = sample;
@ -109,7 +103,7 @@ impl SamplerCommand {
//fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option<Self> { //fn note_off (&self, state: &mut Sampler, pitch: u7) -> Option<Self> {
//todo!() //todo!()
//} //}
//fn set_sample (&self, state: &mut Sampler, pitch: u7, s: MaybeSample) -> Option<Self> { //fn set_sample (&self, state: &mut Sampler, pitch: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
//Some(Self::SetSample(p, state.set_sample(p, s))) //Some(Self::SetSample(p, state.set_sample(p, s)))
//} //}
//fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> { //fn import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
@ -134,7 +128,7 @@ impl SamplerCommand {
////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}"))
////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}"))
////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}"))
////(SetSample [p: u7, s: MaybeSample] Some(Self::SetSample(p, state.set_sample(p, s)))) ////(SetSample [p: u7, s: Option<Arc<RwLock<Sample>>>] Some(Self::SetSample(p, state.set_sample(p, s))))
////(Import [c: FileBrowserCommand] match c { ////(Import [c: FileBrowserCommand] match c {
////FileBrowserCommand::Begin => { ////FileBrowserCommand::Begin => {
//////let voices = &state.state.voices; //////let voices = &state.state.voices;
@ -157,7 +151,7 @@ impl SamplerCommand {
////Some(Self::RecordCancel)) ////Some(Self::RecordCancel))
////("record/finish" [] ////("record/finish" []
////Some(Self::RecordFinish)) ////Some(Self::RecordFinish))
////("set/sample" [i: u7, s: MaybeSample] ////("set/sample" [i: u7, s: Option<Arc<RwLock<Sample>>>]
////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler"))))
////("set/start" [i: u7, s: usize] ////("set/start" [i: u7, s: usize]
////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) ////Some(Self::SetStart(i.expect("no index"), s.expect("no start"))))

View file

@ -1,80 +1,98 @@
use crate::*; use crate::*;
pub struct SamplerAudio<'a>(pub &'a mut Sampler); audio!(|self: Sampler, _client, scope|{
self.process_midi_in(scope);
audio!(|self: SamplerAudio<'a>, _client, scope|{ self.process_audio_out(scope);
self.0.process_midi_in(scope); self.process_audio_in(scope);
self.0.clear_output_buffer();
self.0.process_audio_out(scope);
self.0.write_output_buffer(scope);
self.0.process_audio_in(scope);
Control::Continue Control::Continue
}); });
impl Sampler { impl Sampler {
pub fn process_audio_in (&mut self, scope: &ProcessScope) { pub fn process_audio_in (&mut self, scope: &ProcessScope) {
let Sampler { audio_ins, input_meter, recording, .. } = self; self.reset_input_meters();
if audio_ins.len() != input_meter.len() { if self.recording.is_some() {
*input_meter = vec![0.0;audio_ins.len()]; self.record_into(scope);
}
if let Some((_, sample)) = recording {
let mut sample = sample.write().unwrap();
if sample.channels.len() != audio_ins.len() {
panic!("channel count mismatch");
}
let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut());
let mut length = 0;
for ((input, meter), channel) in iterator {
let slice = input.port().as_slice(scope);
length = length.max(slice.len());
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
channel.extend_from_slice(slice);
}
sample.end += length;
} else { } else {
for (input, meter) in audio_ins.iter().zip(input_meter) { self.update_input_meters(scope);
let slice = input.port().as_slice(scope);
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
}
} }
} }
/// Zero the output buffer. /// Make sure that input meter count corresponds to input channel count
pub fn clear_output_buffer (&mut self) { fn reset_input_meters (&mut self) {
for buffer in self.buffer.iter_mut() { let channels = self.audio_ins.len();
buffer.fill(0.0); if self.input_meters.len() != channels {
self.input_meters = vec![f32::MIN;channels];
}
}
/// Record from inputs to sample
fn record_into (&mut self, scope: &ProcessScope) {
let mut sample = self.recording
.as_mut().expect("no recording sample").1
.write().unwrap();
if sample.channels.len() != self.audio_ins.len() {
panic!("channel count mismatch");
}
let samples_with_meters = self.audio_ins.iter()
.zip(self.input_meters.iter_mut())
.zip(sample.channels.iter_mut());
let mut length = 0;
for ((input, meter), channel) in samples_with_meters {
let slice = input.port().as_slice(scope);
length = length.max(slice.len());
*meter = to_rms(slice);
channel.extend_from_slice(slice);
}
sample.end += length;
}
/// Update input meters
fn update_input_meters (&mut self, scope: &ProcessScope) {
for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) {
let slice = input.port().as_slice(scope);
*meter = to_rms(slice);
}
}
/// Make sure that output meter count corresponds to input channel count
fn reset_output_meters (&mut self) {
let channels = self.audio_outs.len();
if self.output_meters.len() != channels {
self.output_meters = vec![f32::MIN;channels];
} }
} }
/// Mix all currently playing samples into the output. /// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) { pub fn process_audio_out (&mut self, scope: &ProcessScope) {
let Sampler { ref mut buffer, voices, output_gain, .. } = self; self.clear_output_buffer();
self.populate_output_buffer(scope.n_frames() as usize);
self.write_output_buffer(scope);
}
/// Zero the output buffer.
fn clear_output_buffer (&mut self) {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Write playing voices to output buffer
fn populate_output_buffer (&mut self, frames: usize) {
let Sampler { ref mut buffer, voices, output_gain, mixing_mode, .. } = self;
let channel_count = buffer.len(); let channel_count = buffer.len();
voices.write().unwrap().retain_mut(|voice|{ match mixing_mode {
for index in 0..scope.n_frames() as usize { MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{
if let Some(frame) = voice.next() { mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
for (channel, sample) in frame.iter().enumerate() { }),
// Averaging mixer: MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{
//self.buffer[channel % channel_count][index] = ( mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
//(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 }),
//); }
buffer[channel % channel_count][index] += sample * *output_gain;
}
} else {
return false
}
}
true
});
} }
/// Write output buffer to output ports. /// Write output buffer to output ports.
pub fn write_output_buffer (&mut self, scope: &ProcessScope) { fn write_output_buffer (&mut self, scope: &ProcessScope) {
let Sampler { ref mut audio_outs, buffer, .. } = self; let Sampler { ref mut audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() { for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i]; let buffer = &buffer[i];

View file

@ -1,6 +1,7 @@
use crate::*; use crate::*;
impl Sample { impl Sample {
/// Read WAV from file /// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> { pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![]; let mut channels: Vec<wavers::Samples<f32>> = vec![];
@ -16,6 +17,7 @@ impl Sample {
} }
Ok((end, data)) Ok((end, data))
} }
pub fn from_file (path: &PathBuf) -> Usually<Self> { pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into(); let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() }; let mut sample = Self { name, ..Default::default() };
@ -49,6 +51,7 @@ impl Sample {
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample) Ok(sample)
} }
fn decode_packet ( fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet &mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> { ) -> Usually<()> {
@ -84,4 +87,5 @@ impl Sample {
} }
Ok(()) Ok(())
} }
} }

View file

@ -52,3 +52,27 @@ impl Sample {
} }
} }
} }
// TODO:
//for port in midi_in.iter() {
//for event in port.iter() {
//match event {
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
//editor.set_note_pos(key.as_int() as usize);
//},
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
//self.editor.as_ref(),
//self.sampler.as_ref(),
//) => {
//// TODO: give sampler its own cursor
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
//sample.write().unwrap().handle_cc(*controller, *value)
//}
//}
//_ =>{}
//},
//_ =>{}
//}
//}
//}

View file

@ -1,55 +1,76 @@
use crate::*; use crate::*;
pub type MaybeSample = Option<Arc<RwLock<Sample>>>;
/// The sampler device plays sounds in response to MIDI notes. /// The sampler device plays sounds in response to MIDI notes.
#[derive(Debug)] #[derive(Debug)]
pub struct Sampler { pub struct Sampler {
pub name: String, /// Name of sampler.
pub mapped: [MaybeSample;128], pub name: String,
pub recording: Option<(usize, Arc<RwLock<Sample>>)>, /// Device color.
pub unmapped: Vec<Arc<RwLock<Sample>>>, pub color: ItemTheme,
pub voices: Arc<RwLock<Vec<Voice>>>, /// Audio input ports. Samples get recorded here.
pub midi_in: Option<JackMidiIn>, pub audio_ins: Vec<JackAudioIn>,
pub audio_ins: Vec<JackAudioIn>, /// Audio input meters.
pub input_meter: Vec<f32>, pub input_meters: Vec<f32>,
pub audio_outs: Vec<JackAudioOut>, /// Sample currently being recorded.
pub buffer: Vec<Vec<f32>>, pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
pub output_gain: f32, /// Recording buffer.
pub editing: MaybeSample, pub buffer: Vec<Vec<f32>>,
pub mode: Option<SamplerMode>, /// Samples mapped to MIDI notes.
/// Size of actual notes area pub mapped: [Option<Arc<RwLock<Sample>>>;128],
pub size: Measure<TuiOut>, /// Samples that are not mapped to MIDI notes.
/// Lowest note displayed pub unmapped: Vec<Arc<RwLock<Sample>>>,
pub note_lo: AtomicUsize, /// Sample currently being edited.
/// Selected note pub editing: Option<Arc<RwLock<Sample>>>,
pub note_pt: AtomicUsize, /// MIDI input port. Triggers sample playback.
/// Selected note as row/col pub midi_in: Option<JackMidiIn>,
pub cursor: (AtomicUsize, AtomicUsize), /// Collection of currently playing instances of samples.
pub color: ItemTheme pub voices: Arc<RwLock<Vec<Voice>>>,
/// Audio output ports. Voices get played here.
pub audio_outs: Vec<JackAudioOut>,
/// Audio output meters.
pub output_meters: Vec<f32>,
/// How to mix the voices.
pub mixing_mode: MixingMode,
/// How to meter the inputs and outputs.
pub metering_mode: MeteringMode,
/// Fixed gain applied to all output.
pub output_gain: f32,
/// Currently active modal, if any.
pub mode: Option<SamplerMode>,
/// Size of rendered sampler.
pub size: Measure<TuiOut>,
/// Lowest note displayed.
pub note_lo: AtomicUsize,
/// Currently selected note.
pub note_pt: AtomicUsize,
/// Selected note as row/col.
pub cursor: (AtomicUsize, AtomicUsize),
} }
impl Default for Sampler { impl Default for Sampler {
fn default () -> Self { fn default () -> Self {
Self { Self {
midi_in: None, midi_in: None,
audio_ins: vec![], audio_ins: vec![],
input_meter: vec![0.0;2], input_meters: vec![0.0;2],
audio_outs: vec![], output_meters: vec![0.0;2],
name: "tek_sampler".to_string(), audio_outs: vec![],
mapped: [const { None };128], name: "tek_sampler".to_string(),
unmapped: vec![], mapped: [const { None };128],
voices: Arc::new(RwLock::new(vec![])), unmapped: vec![],
buffer: vec![vec![0.0;16384];2], voices: Arc::new(RwLock::new(vec![])),
output_gain: 1., buffer: vec![vec![0.0;16384];2],
recording: None, output_gain: 1.,
mode: None, recording: None,
editing: None, mode: None,
size: Default::default(), editing: None,
note_lo: 0.into(), size: Default::default(),
note_pt: 0.into(), note_lo: 0.into(),
cursor: (0.into(), 0.into()), note_pt: 0.into(),
color: Default::default(), cursor: (0.into(), 0.into()),
color: Default::default(),
mixing_mode: Default::default(),
metering_mode: Default::default(),
} }
} }
} }

View file

@ -62,7 +62,7 @@ impl Sampler {
Fixed::x(12, Map::south( Fixed::x(12, Map::south(
1, 1,
move||(note_lo..=note_hi).rev(), move||(note_lo..=note_hi).rev(),
move|note, i| { move|note, _index| {
//let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); //let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a))));
let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset };
let mut fg = Tui::g(160); let mut fg = Tui::g(160);
@ -90,18 +90,31 @@ impl Sampler {
} }
pub fn view_sample (&self, note_pt: usize) -> impl Content<TuiOut> + use<'_> { pub fn view_sample (&self, note_pt: usize) -> impl Content<TuiOut> + use<'_> {
Outer(true, Style::default().fg(Tui::g(96))).enclose(draw_viewer(if let Some((_, sample)) = &self.recording { Outer(true, Style::default().fg(Tui::g(96)))
Some(sample) .enclose(Fill::xy(draw_viewer(if let Some((_, sample)) = &self.recording {
} else if let Some(sample) = &self.mapped[note_pt] { Some(sample)
Some(sample) } else if let Some(sample) = &self.mapped[note_pt] {
} else { Some(sample)
None } else {
})) None
})))
} }
pub fn status (&self, index: usize) -> impl Content<TuiOut> { pub fn status (&self, index: usize) -> impl Content<TuiOut> {
draw_status(self.mapped[index].as_ref()) draw_status(self.mapped[index].as_ref())
} }
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.input_meters.iter(), |value, _index|{
Fill::y(RmsMeter(*value))
})))
}
pub fn view_meters_output (&self) -> impl Content<TuiOut> + use<'_> {
Tui::bg(Black, Fixed::x(2, Map::east(1, ||self.output_meters.iter(), |value, _index|{
Fill::y(RmsMeter(*value))
})))
}
} }
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String { fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
@ -120,49 +133,57 @@ fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
} }
fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> { fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
let min_db = -40.0; let min_db = -64.0;
ThunkRender::new(move|to: &mut TuiOut|{ ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, width, height] = to.area(); let [x, y, width, height] = to.area();
let area = Rect { x, y, width, height }; let area = Rect { x, y, width, height };
let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec<Line>) = if let Some(sample) = &sample {
if let Some(sample) = &sample { let sample = sample.read().unwrap();
let sample = sample.read().unwrap(); let start = sample.start as f64;
let start = sample.start as f64; let end = sample.end as f64;
let end = sample.end as f64; let length = end - start;
let length = end - start; let step = length / width as f64;
let step = length / width as f64; let mut t = start;
let mut t = start; let mut lines = vec![];
let mut lines = vec![]; while t < end {
while t < end { let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let total: f32 = chunk.iter().map(|x|x.abs()).sum(); let count = chunk.len() as f32;
let count = chunk.len() as f32; let meter = 10. * (total / count).log10();
let meter = 10. * (total / count).log10(); let x = t as f64;
let x = t as f64; let y = meter as f64;
let y = meter as f64; lines.push(Line::new(x, min_db, x, y, Color::Green));
lines.push(Line::new(x, min_db, x, y, Color::Green)); t += step / 2.;
t += step / 2.; }
} Canvas::default()
( .x_bounds([sample.start as f64, sample.end as f64])
[sample.start as f64, sample.end as f64], .y_bounds([min_db, 0.])
[min_db, 0.], .paint(|ctx| {
lines for line in lines.iter() {
) ctx.draw(line);
} else { }
( //FIXME: proportions
[0.0, width as f64], //let text = "press record to finish sampling";
[0.0, height as f64], //ctx.print(
vec![ //(width - text.len() as u16) as f64 / 2.0,
Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), //height as f64 / 2.0,
Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), //text.red()
] //);
) }).render(area, &mut to.buffer);
}; } else {
Canvas::default() Canvas::default()
.x_bounds(x_bounds) .x_bounds([0.0, width as f64])
.y_bounds(y_bounds) .y_bounds([0.0, height as f64])
.paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) .paint(|ctx| {
.render(area, &mut to.buffer); let text = "press record to begin sampling";
ctx.print(
(width - text.len() as u16) as f64 / 2.0,
height as f64 / 2.0,
text.red()
);
})
.render(area, &mut to.buffer);
}
}) })
} }

View file

@ -1,11 +1,11 @@
use crate::*; mod seq_audio; pub use self::seq_audio::*;
mod seq_clip; pub use self::seq_clip::*; mod seq_clip; pub use self::seq_clip::*;
mod seq_launch; pub use self::seq_launch::*; mod seq_launch; pub use self::seq_launch::*;
mod seq_model; pub use self::seq_model::*; mod seq_model; pub use self::seq_model::*;
mod seq_view; pub use self::seq_view::*; mod seq_view; pub use self::seq_view::*;
#[cfg(test)] #[test] pub fn test_midi_clip () { #[cfg(test)] #[test] pub fn test_midi_clip () {
use crate::*;
let clip = MidiClip::stop_all(); let clip = MidiClip::stop_all();
println!("{clip:?}"); println!("{clip:?}");
@ -22,6 +22,7 @@ mod seq_view; pub use self::seq_view::*;
} }
#[cfg(test)] #[test] fn test_midi_play () { #[cfg(test)] #[test] fn test_midi_play () {
let player = MidiPlayer::default(); use crate::*;
println!("{player:?}"); let sequencer = Sequencer::default();
println!("{sequencer:?}");
} }

View file

@ -0,0 +1,305 @@
use crate::*;
/// Hosts the JACK callback for a single MIDI sequencer
pub struct PlayerAudio<'a, T: MidiSequencer>(
/// 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 clip sequencer/recorder.
impl<T: MidiSequencer> 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 clip 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
}
}
pub trait MidiSequencer: MidiRecorder + MidiPlayer + Send + Sync {}
impl MidiSequencer for Sequencer {}
pub trait MidiRecorder: HasClock + HasPlayClip + 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 overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
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.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
// FIXME: don't lock on every event!
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_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((_start_at, _clip)) = &self.next_clip() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
clip: &Option<Arc<RwLock<MidiClip>>>,
_midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(clip) = clip {
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 clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
clip.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
}
}
pub trait MidiPlayer: HasPlayClip + 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
) {
let n_frames = (scope.n_frames() as usize).min(out.len());
for frame in &mut out[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from clip 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 clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
if let Some((started, clip)) = self.play_clip() {
self.play_chunk(scope, note_buf, out, started, clip)
} else {
true
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// Index of first sample to populate.
let offset = self.get_sample_offset(scope, started);
// Notes active during current chunk.
let notes = &mut self.notes_out().write().unwrap();
// Length of clip.
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
// Write MIDI events from clip at sample offsets corresponding to pulses.
for (sample, pulse) in self.get_pulses(scope, offset) {
// If a next clip is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if clip.is_some() { pulse >= length } else { true };
// Is it time for switchover?
if self.next_clip().is_some() && past_end {
return true
}
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
/// Get index of first sample to populate.
///
/// Greater than 0 means that the first pulse of the clip
/// falls somewhere in the middle of the chunk.
fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{
(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
)
}
// Get iterator that emits sample paired with pulse.
//
// * Sample: index into output buffer at which to write MIDI event
// * Pulse: index into clip from which to take the MIDI event
//
// Emitted for each sample of the output buffer that corresponds to a MIDI pulse.
fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator {
self.clock().timebase().pulses_between_samples(
offset, offset + scope.n_frames() as usize)
}
/// Handle switchover from current to next playing clip.
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, clip)) = &self.next_clip() {
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 clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
}
fn play_pulse (
clip: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source clip from which the MIDI events will be taken.
let clip = clip.read().unwrap();
// Clip with zero length is not processed
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.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.port_mut().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:?}");
});
}
}
}
}

View file

@ -1,27 +1,22 @@
//! MIDI player //! MIDI sequencer
use crate::*; use crate::*;
use tek_engine::jack::*;
pub trait HasPlayer { pub trait HasSequencer {
fn player (&self) -> &impl MidiPlayerApi; fn sequencer (&self) -> &impl MidiSequencer;
fn player_mut (&mut self) -> &mut impl MidiPlayerApi; fn sequencer_mut (&mut self) -> &mut impl MidiSequencer;
} }
#[macro_export] macro_rules! has_player { #[macro_export] macro_rules! has_sequencer {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { impl $(<$($L),*$($T $(: $U)?),*>)? HasSequencer for $Struct $(<$($L),*$($T),*>)? {
fn player (&$self) -> &impl MidiPlayerApi { &$cb } fn sequencer (&$self) -> &impl MidiSequencer { &$cb }
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } fn sequencer_mut (&mut $self) -> &mut impl MidiSequencer { &mut$cb }
} }
} }
} }
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
impl MidiPlayerApi for MidiPlayer {}
/// Contains state for playing a clip /// Contains state for playing a clip
pub struct MidiPlayer { pub struct Sequencer {
/// State of clock and playhead /// State of clock and playhead
pub clock: Clock, pub clock: Clock,
/// Start time and clip being played /// Start time and clip being played
@ -48,7 +43,7 @@ pub struct MidiPlayer {
pub note_buf: Vec<u8>, pub note_buf: Vec<u8>,
} }
impl Default for MidiPlayer { impl Default for Sequencer {
fn default () -> Self { fn default () -> Self {
Self { Self {
play_clip: None, play_clip: None,
@ -69,7 +64,7 @@ impl Default for MidiPlayer {
} }
} }
impl MidiPlayer { impl Sequencer {
pub fn new ( pub fn new (
name: impl AsRef<str>, name: impl AsRef<str>,
jack: &Jack, jack: &Jack,
@ -97,9 +92,9 @@ impl MidiPlayer {
} }
} }
impl std::fmt::Debug for MidiPlayer { impl std::fmt::Debug for Sequencer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer") f.debug_struct("Sequencer")
.field("clock", &self.clock) .field("clock", &self.clock)
.field("play_clip", &self.play_clip) .field("play_clip", &self.play_clip)
.field("next_clip", &self.next_clip) .field("next_clip", &self.next_clip)
@ -107,57 +102,20 @@ impl std::fmt::Debug for MidiPlayer {
} }
} }
has_clock!(|self: MidiPlayer|self.clock); has_clock!(|self: Sequencer|self.clock);
impl HasMidiIns for MidiPlayer { impl HasMidiIns for Sequencer {
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins } fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins } fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
} }
impl HasMidiOuts for MidiPlayer { impl HasMidiOuts for Sequencer {
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs } fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs } fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf } fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
} }
/// Hosts the JACK callback for a single MIDI player impl MidiRecorder for Sequencer {
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 clip 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 clip 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 { fn recording (&self) -> bool {
self.recording self.recording
} }
@ -181,13 +139,13 @@ impl MidiRecordApi for MidiPlayer {
} }
} }
impl MidiPlaybackApi for MidiPlayer { impl MidiPlayer for Sequencer {
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> { fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_out &self.notes_out
} }
} }
impl HasPlayClip for MidiPlayer { impl HasPlayClip for Sequencer {
fn reset (&self) -> bool { fn reset (&self) -> bool {
self.reset self.reset
} }
@ -207,247 +165,3 @@ impl HasPlayClip for MidiPlayer {
&mut self.next_clip &mut self.next_clip
} }
} }
pub trait MidiRecordApi: HasClock + HasPlayClip + 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 overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
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.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
// FIXME: don't lock on every event!
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_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((_start_at, _clip)) = &self.next_clip() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
clip: &Option<Arc<RwLock<MidiClip>>>,
_midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(clip) = clip {
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 clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
clip.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
}
}
pub trait MidiPlaybackApi: HasPlayClip + 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
) {
let n_frames = (scope.n_frames() as usize).min(out.len());
for frame in &mut out[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from clip 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 clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
self.play_clip().as_ref().map_or(true, |(started, clip)|{
self.play_chunk(scope, note_buf, out, started, clip)
})
}
/// Handle switchover from current to next playing clip.
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, clip)) = &self.next_clip() {
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 clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// First sample to populate. Greater than 0 means that the first
// pulse of the clip 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 clip 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 = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
for (sample, pulse) in pulses {
// If a next clip is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if clip.is_some() { pulse >= length } else { true };
if self.next_clip().is_some() && past_end {
return true
}
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
clip: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source clip from which the MIDI events will be taken.
let clip = clip.read().unwrap();
// Clip with zero length is not processed
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.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.port_mut().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:?}");
});
}
}
}
}

View file

@ -4,6 +4,7 @@ pub use ::midly::{
Smf, Smf,
TrackEventKind, TrackEventKind,
MidiMessage, MidiMessage,
Error as MidiError,
num::*, num::*,
live::*, live::*,
}; };