├── .gitignore ├── Cargo.toml ├── README.md ├── rustfmt.toml └── src ├── main.rs ├── scheduler.rs └── vm.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Seq" 3 | version = "0.1.0" 4 | authors = ["Maurice van Leeuwen "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | portmidi = "^0.2" 11 | structopt = "^0.3" 12 | rand = "^0.7" 13 | termion = "^1.5" 14 | toml = "^0.5" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequencer-rs 2 | This is a programmable sequencer. The sequencer interfaces over MIDI, OSC or UDP. The application doesn't make sound on it's own, and it is not a synthesizer. The intended usage is live coding environments and modular synthesizers (using [CV-rs](https://github.com/MauricevanLeeuwen/cv-rs)). 3 | 4 | ## Quickstart 5 | 6 | ``` 7 | git clone https://github.com/MauricevanLeeuwen/seq-rs 8 | cd seq-rs 9 | cargo run 10 | ``` 11 | 12 | ## Implementation 13 | * PortMidi using [portmidi-rs](https://github.com/musitdev/portmidi-rs) 14 | * Text-based UI using [Termion](https://gitlab.redox-os.org/redox-os/termion) 15 | 16 | ## Usage 17 | ``` 18 | q Quit 19 | h cursor left 20 | j cursor down 21 | k cursor up 22 | l cursor right 23 | ``` 24 | 25 | 26 | ## Extra 27 | Create control voltages for a modular setup: [CV-rs](https://github.com/MauricevanLeeuwen/cv-rs) 28 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width=120 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate termion; 2 | use portmidi; 3 | use termion::event::Key; 4 | use termion::event::Key::Char; 5 | use termion::input::TermRead; 6 | use termion::raw::IntoRawMode; 7 | use termion::{clear, color, cursor, style}; 8 | 9 | use std::sync::mpsc; 10 | use std::thread; 11 | use std::time::{Duration, Instant}; 12 | 13 | use std::io::{self, Read, Write}; 14 | use std::path::PathBuf; 15 | use structopt::StructOpt; 16 | 17 | mod vm; 18 | 19 | #[derive(StructOpt, Debug)] 20 | #[structopt(name = "Seq")] 21 | struct Opt { 22 | #[structopt(short, long, parse(from_os_str))] 23 | config: Option, /* config-rs */ 24 | #[structopt(short, long)] 25 | debug: bool, 26 | #[structopt(short = "n", long, default_value = "1")] 27 | steps: u8, 28 | 29 | #[structopt(short, long, default_value = "60")] 30 | tempo: u8, 31 | 32 | #[structopt(short, long, parse(from_occurrences))] 33 | verbose: u8, 34 | } 35 | 36 | struct Event { 37 | t: u128, 38 | } 39 | struct Tui { 40 | stdout: W, 41 | stdin: R, 42 | x: u16, 43 | y: u16, 44 | width: u16, 45 | height: u16, 46 | } 47 | 48 | fn tui_init(mut stdout: W, stdin: R) -> Tui, W> { 49 | write!(stdout, "{}", clear::All).unwrap(); 50 | write!(stdout, "{}", cursor::Goto(1, 1)).unwrap(); 51 | stdout.flush().unwrap(); 52 | 53 | let mut tui = Tui { 54 | stdin: stdin.keys(), 55 | stdout: stdout, 56 | x: 0, 57 | y: 0, 58 | width: 64, 59 | height: 16, 60 | }; 61 | tui 62 | } 63 | 64 | impl>, W: Write> Tui { 65 | fn reset(&mut self) {} 66 | 67 | fn start(&mut self, rx: mpsc::Receiver) { 68 | loop { 69 | for receive in rx.try_iter() { 70 | write!( 71 | self.stdout, 72 | "{}{}{}", 73 | cursor::Goto(1, self.height + 1), 74 | clear::CurrentLine, 75 | receive.t 76 | ) 77 | .unwrap(); 78 | } 79 | 80 | loop { 81 | let key = self.stdin.next(); 82 | match key { 83 | Some(Ok(Char('q'))) => return, 84 | Some(Ok(Char('h'))) => self.x = self.left(self.x), 85 | Some(Ok(Char('j'))) => self.y = self.down(self.y), 86 | Some(Ok(Char('k'))) => self.y = self.up(self.y), 87 | Some(Ok(Char('l'))) => self.x = self.right(self.x), 88 | Some(Ok(Char(x))) => write!(self.stdout, "{}{}", cursor::Goto(self.x + 1, self.y + 1), x).unwrap(), 89 | Some(Err(_)) => {} 90 | None => break, 91 | _ => {} 92 | } 93 | } 94 | 95 | write!(self.stdout, "{}", cursor::Goto(self.x + 1, self.y + 1)).unwrap(); 96 | self.stdout.flush().unwrap(); 97 | thread::sleep(Duration::from_millis(10)); 98 | } 99 | } 100 | 101 | fn left(&self, x: u16) -> u16 { 102 | if x == 0 { 103 | self.width - 1 104 | } else { 105 | x - 1 106 | } 107 | } 108 | fn right(&self, x: u16) -> u16 { 109 | if x + 1 < self.width { 110 | x + 1 111 | } else { 112 | 0 113 | } 114 | } 115 | fn up(&self, y: u16) -> u16 { 116 | if y == 0 { 117 | self.height - 1 118 | } else { 119 | y - 1 120 | } 121 | } 122 | fn down(&self, y: u16) -> u16 { 123 | if y + 1 < self.height { 124 | y + 1 125 | } else { 126 | 0 127 | } 128 | } 129 | 130 | fn print_status(&mut self) { 131 | write!(self.stdout, "{}", cursor::Goto(1, self.height + 1)).unwrap(); 132 | write!(self.stdout, "{}{}x{}\r\n", clear::CurrentLine, self.width, self.height).unwrap(); 133 | write!(self.stdout, "{}{},{}\r\n", clear::CurrentLine, self.x, self.y).unwrap(); 134 | } 135 | } 136 | 137 | fn main() { 138 | let opt = Opt::from_args(); 139 | 140 | // lock stdio 141 | let stdout = io::stdout(); 142 | let mut stdout = stdout.lock(); 143 | let stdin = termion::async_stdin(); 144 | let stderr = io::stderr(); 145 | let mut stderr = stderr.lock(); 146 | 147 | let mut input = String::new(); 148 | // Initialize portmidi 149 | let pm_context = portmidi::PortMidi::new().unwrap(); 150 | for res in pm_context.devices().unwrap() { 151 | println!("{:#?}", res) 152 | } 153 | let midi_out = pm_context.default_output_port(64).unwrap(); 154 | 155 | let mut stdout = stdout.into_raw_mode().unwrap(); 156 | //let termsize = termion::terminal_size().ok() 157 | 158 | let (tx, rx) = mpsc::channel(); 159 | let mut tui = tui_init(stdout, stdin); 160 | 161 | { 162 | let tx = mpsc::Sender::clone(&tx); 163 | let mut instance = vm::from_string(input, midi_out); 164 | 165 | let h = thread::spawn(move || { 166 | let mut tick = Instant::now(); 167 | 168 | loop { 169 | while Instant::now() < tick { 170 | thread::yield_now(); 171 | } 172 | tx.send(Event { 173 | t: (Instant::now() - tick).as_micros(), 174 | }); 175 | 176 | let accuracy = Duration::from_millis(1); 177 | instance.tick(); 178 | tick += instance.tick; 179 | thread::park_timeout(tick - accuracy - Instant::now()); 180 | } 181 | }); 182 | } 183 | 184 | tui.start(rx); 185 | 186 | //Ok(()) 187 | } 188 | -------------------------------------------------------------------------------- /src/scheduler.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | 3 | fn control() { 4 | let t = time::Duration::from_millis(1); /* for smaller granularity you should spin, not sleep */ 5 | thread::sleep(t); 6 | } 7 | -------------------------------------------------------------------------------- /src/vm.rs: -------------------------------------------------------------------------------- 1 | //#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] 2 | use portmidi::{self, MidiMessage}; 3 | use rand::prelude::*; 4 | use std::time::Duration; 5 | 6 | struct Cell { 7 | channel: u8, 8 | note: u8, 9 | active: bool, 10 | } 11 | impl Cell { 12 | fn new(note: u8) -> Cell { 13 | Cell { 14 | channel: 0, 15 | note: note, 16 | active: false, 17 | } 18 | } 19 | fn bang(&mut self) -> bool { 20 | false 21 | } 22 | } 23 | 24 | pub struct Instance { 25 | cells: Vec, 26 | notes: Vec<(u8, u8)>, 27 | midi_out: portmidi::OutputPort, 28 | ctr: u32, 29 | pub tick: Duration, 30 | } 31 | pub fn new(bpm: f32, midi_out: portmidi::OutputPort) -> Instance { 32 | Instance { 33 | cells: vec![], 34 | notes: vec![], 35 | midi_out: midi_out, 36 | ctr: 0, 37 | tick: Duration::from_millis((60_000.0 / (bpm * 24.0)) as u64), // todo: that f32->u64 is a bit hacky, just schedule ticks as events with a deadline. 38 | } 39 | } 40 | impl Instance { 41 | fn note_off_sustained(&mut self) { 42 | // turn off all sustained notes 43 | 44 | // TODO: use midi message to turn all notes off. 45 | while let Some((ch, i)) = self.notes.pop() { 46 | let event = MidiMessage { 47 | status: 0x80 + ch, 48 | data1: i, //note, 49 | data2: 0, 50 | }; 51 | self.midi_out.write_message(event); 52 | } 53 | } 54 | 55 | pub fn tick(&mut self) { 56 | // With a tick rate of 1/24th quarter note (ie. 1/96th note) 57 | // 1/16 note events happen every 6 ticks. 58 | 59 | self.ctr = (self.ctr + 1) % 24; 60 | 61 | self.note_off_sustained(); 62 | // note on 63 | let x = rand::random::() % self.cells.len(); 64 | let x = &self.cells[x]; 65 | 66 | let note: u8 = 60; 67 | let event = MidiMessage { 68 | status: 0x90 + x.channel, 69 | data1: x.note, 70 | data2: 64, 71 | }; 72 | self.notes.push((x.channel, x.note)); 73 | self.midi_out.write_message(event); 74 | } 75 | } 76 | 77 | pub fn from_string(input: String, mut midi_out: portmidi::OutputPort) -> Instance { 78 | let y = 12 * 3; 79 | Instance { 80 | cells: vec![Cell::new(y + 0)], 81 | ctr: 0, 82 | midi_out: midi_out, 83 | notes: vec![], 84 | tick: Duration::from_millis(1000 / 24), /* midi clock syncs at 1/24 of a quarter note */ 85 | } 86 | } 87 | --------------------------------------------------------------------------------