mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
Compare commits
15 commits
fb99128650
...
997d67a487
| Author | SHA1 | Date | |
|---|---|---|---|
| 997d67a487 | |||
| 329da026d7 | |||
| 836624674e | |||
| ee2efd1c26 | |||
| b9c101081b | |||
| 6db5df5210 | |||
| 7bc37e7659 | |||
| 7690549bdc | |||
| e5752ea4b0 | |||
| 2ef9628ab8 | |||
| 4127c141cc | |||
| 7f255eaea8 | |||
| 5fab1af138 | |||
| c5586c3a35 | |||
| c78b2dc9de |
33 changed files with 1425 additions and 1136 deletions
|
|
@ -6,10 +6,12 @@
|
||||||
(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/w :view-meters-output
|
||||||
|
(bsp/e :view-meters-input
|
||||||
(bsp/n (fixed/y 5 :view-sample-viewer)
|
(bsp/n (fixed/y 5 :view-sample-viewer)
|
||||||
(bsp/w (fixed/x :w-sidebar :view-pool)
|
(bsp/w (fixed/x :w-sidebar :view-pool)
|
||||||
(bsp/e :view-samples-keys
|
(bsp/e :view-samples-keys
|
||||||
(fill/y :view-editor))))))))
|
(fill/y :view-editor))))))))))
|
||||||
|
|
||||||
(keys
|
(keys
|
||||||
(layer-if :focus-pool-import "./keys_pool_file.edn")
|
(layer-if :focus-pool-import "./keys_pool_file.edn")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
|
||||||
track
|
|
||||||
}
|
|
||||||
/// Create a new track containing a sequencer and sampler.
|
|
||||||
pub fn new_groovebox (
|
|
||||||
jack: &Jack,
|
jack: &Jack,
|
||||||
|
clock: Option<&Clock>,
|
||||||
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
midi_from: &[PortConnect],
|
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 connecting the [Sequencer] to a [Sampler].
|
||||||
|
pub fn new_with_sampler (
|
||||||
|
name: &impl AsRef<str>,
|
||||||
|
color: Option<ItemTheme>,
|
||||||
|
jack: &Jack,
|
||||||
|
clock: Option<&Clock>,
|
||||||
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ 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 =>
|
||||||
|
Some(Arc::new(RwLock::new(MidiClip::new(
|
||||||
"Clip", true, 384usize, None, Some(ItemColor::random().into())),
|
"Clip", true, 384usize, None, Some(ItemColor::random().into())),
|
||||||
))),
|
))),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -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![]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
51
crates/device/src/device.rs
Normal file
51
crates/device/src/device.rs
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
5
crates/device/src/editor.rs
Normal file
5
crates/device/src/editor.rs
Normal 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::*;
|
||||||
126
crates/device/src/editor/editor_api.rs
Normal file
126
crates/device/src/editor/editor_api.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
9
crates/device/src/editor/editor_view.rs
Normal file
9
crates/device/src/editor/editor_view.rs
Normal 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)
|
||||||
|
});
|
||||||
315
crates/device/src/editor/editor_view_h.rs
Normal file
315
crates/device/src/editor/editor_view_h.rs
Normal 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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crates/device/src/editor/editor_view_v.rs
Normal file
37
crates/device/src/editor/editor_view_v.rs
Normal 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), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// A LV2 plugin.
|
/// A LV2 plugin.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
||||||
54
crates/device/src/meter.rs
Normal file
54
crates/device/src/meter.rs
Normal 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()
|
||||||
|
}
|
||||||
41
crates/device/src/mixer.rs
Normal file
41
crates/device/src/mixer.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub(crate) use symphonia::{
|
pub(crate) use symphonia::{
|
||||||
core::{
|
core::{
|
||||||
formats::Packet,
|
formats::Packet,
|
||||||
|
|
|
||||||
|
|
@ -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"))))
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//_ =>{}
|
||||||
|
//},
|
||||||
|
//_ =>{}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,50 @@
|
||||||
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 {
|
||||||
|
/// Name of sampler.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mapped: [MaybeSample;128],
|
/// Device color.
|
||||||
pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
|
pub color: ItemTheme,
|
||||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
/// Audio input ports. Samples get recorded here.
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
|
||||||
pub midi_in: Option<JackMidiIn>,
|
|
||||||
pub audio_ins: Vec<JackAudioIn>,
|
pub audio_ins: Vec<JackAudioIn>,
|
||||||
pub input_meter: Vec<f32>,
|
/// Audio input meters.
|
||||||
pub audio_outs: Vec<JackAudioOut>,
|
pub input_meters: Vec<f32>,
|
||||||
|
/// Sample currently being recorded.
|
||||||
|
pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
|
||||||
|
/// Recording buffer.
|
||||||
pub buffer: Vec<Vec<f32>>,
|
pub buffer: Vec<Vec<f32>>,
|
||||||
|
/// Samples mapped to MIDI notes.
|
||||||
|
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||||||
|
/// Samples that are not mapped to MIDI notes.
|
||||||
|
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||||
|
/// Sample currently being edited.
|
||||||
|
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||||
|
/// MIDI input port. Triggers sample playback.
|
||||||
|
pub midi_in: Option<JackMidiIn>,
|
||||||
|
/// Collection of currently playing instances of samples.
|
||||||
|
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,
|
pub output_gain: f32,
|
||||||
pub editing: MaybeSample,
|
/// Currently active modal, if any.
|
||||||
pub mode: Option<SamplerMode>,
|
pub mode: Option<SamplerMode>,
|
||||||
/// Size of actual notes area
|
/// Size of rendered sampler.
|
||||||
pub size: Measure<TuiOut>,
|
pub size: Measure<TuiOut>,
|
||||||
/// Lowest note displayed
|
/// Lowest note displayed.
|
||||||
pub note_lo: AtomicUsize,
|
pub note_lo: AtomicUsize,
|
||||||
/// Selected note
|
/// Currently selected note.
|
||||||
pub note_pt: AtomicUsize,
|
pub note_pt: AtomicUsize,
|
||||||
/// Selected note as row/col
|
/// Selected note as row/col.
|
||||||
pub cursor: (AtomicUsize, AtomicUsize),
|
pub cursor: (AtomicUsize, AtomicUsize),
|
||||||
pub color: ItemTheme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Sampler {
|
impl Default for Sampler {
|
||||||
|
|
@ -34,7 +52,8 @@ impl Default for Sampler {
|
||||||
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],
|
||||||
|
output_meters: vec![0.0;2],
|
||||||
audio_outs: vec![],
|
audio_outs: vec![],
|
||||||
name: "tek_sampler".to_string(),
|
name: "tek_sampler".to_string(),
|
||||||
mapped: [const { None };128],
|
mapped: [const { None };128],
|
||||||
|
|
@ -50,6 +69,8 @@ impl Default for Sampler {
|
||||||
note_pt: 0.into(),
|
note_pt: 0.into(),
|
||||||
cursor: (0.into(), 0.into()),
|
cursor: (0.into(), 0.into()),
|
||||||
color: Default::default(),
|
color: Default::default(),
|
||||||
|
mixing_mode: Default::default(),
|
||||||
|
metering_mode: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
|
.enclose(Fill::xy(draw_viewer(if let Some((_, sample)) = &self.recording {
|
||||||
Some(sample)
|
Some(sample)
|
||||||
} else if let Some(sample) = &self.mapped[note_pt] {
|
} else if let Some(sample) = &self.mapped[note_pt] {
|
||||||
Some(sample)
|
Some(sample)
|
||||||
} else {
|
} else {
|
||||||
None
|
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,11 +133,10 @@ 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;
|
||||||
|
|
@ -143,26 +155,35 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> +
|
||||||
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.;
|
||||||
}
|
}
|
||||||
(
|
|
||||||
[sample.start as f64, sample.end as f64],
|
|
||||||
[min_db, 0.],
|
|
||||||
lines
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
[0.0, width as f64],
|
|
||||||
[0.0, height as f64],
|
|
||||||
vec![
|
|
||||||
Line::new(0.0, 0.0, width as f64, height as f64, Color::Red),
|
|
||||||
Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
Canvas::default()
|
Canvas::default()
|
||||||
.x_bounds(x_bounds)
|
.x_bounds([sample.start as f64, sample.end as f64])
|
||||||
.y_bounds(y_bounds)
|
.y_bounds([min_db, 0.])
|
||||||
.paint(|ctx| { for line in lines.iter() { ctx.draw(line) } })
|
.paint(|ctx| {
|
||||||
|
for line in lines.iter() {
|
||||||
|
ctx.draw(line);
|
||||||
|
}
|
||||||
|
//FIXME: proportions
|
||||||
|
//let text = "press record to finish sampling";
|
||||||
|
//ctx.print(
|
||||||
|
//(width - text.len() as u16) as f64 / 2.0,
|
||||||
|
//height as f64 / 2.0,
|
||||||
|
//text.red()
|
||||||
|
//);
|
||||||
|
}).render(area, &mut to.buffer);
|
||||||
|
} else {
|
||||||
|
Canvas::default()
|
||||||
|
.x_bounds([0.0, width as f64])
|
||||||
|
.y_bounds([0.0, height as f64])
|
||||||
|
.paint(|ctx| {
|
||||||
|
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);
|
.render(area, &mut to.buffer);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:?}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
305
crates/device/src/sequencer/seq_audio.rs
Normal file
305
crates/device/src/sequencer/seq_audio.rs
Normal 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:?}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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:?}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub use ::midly::{
|
||||||
Smf,
|
Smf,
|
||||||
TrackEventKind,
|
TrackEventKind,
|
||||||
MidiMessage,
|
MidiMessage,
|
||||||
|
Error as MidiError,
|
||||||
num::*,
|
num::*,
|
||||||
live::*,
|
live::*,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue