185 lines
6.1 KiB
Rust
185 lines
6.1 KiB
Rust
use jack::RawMidi;
|
|
use kanal::{Receiver, Sender};
|
|
use std::io::{self, Write};
|
|
|
|
#[derive(Debug)]
|
|
enum MidiCommand {
|
|
ControlChange { cc: u8, value: u8 },
|
|
Quit,
|
|
}
|
|
|
|
struct MidiSender {
|
|
midi_out: jack::Port<jack::MidiOut>,
|
|
command_receiver: Receiver<MidiCommand>,
|
|
}
|
|
|
|
impl MidiSender {
|
|
fn new(
|
|
client: &jack::Client,
|
|
command_receiver: Receiver<MidiCommand>,
|
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
|
let midi_out = client
|
|
.register_port("midi_out", jack::MidiOut::default())
|
|
.map_err(|e| format!("Could not create MIDI output port: {}", e))?;
|
|
|
|
Ok(Self {
|
|
midi_out,
|
|
command_receiver,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl jack::ProcessHandler for MidiSender {
|
|
fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control {
|
|
let mut midi_writer = self.midi_out.writer(ps);
|
|
|
|
// Process all pending commands
|
|
while let Ok(Some(command)) = self.command_receiver.try_recv() {
|
|
match command {
|
|
MidiCommand::ControlChange { cc, value } => {
|
|
let midi_data = [0xB0, cc, value]; // Control Change on channel 1
|
|
if let Err(e) = midi_writer.write(&RawMidi {
|
|
time: 0,
|
|
bytes: &midi_data,
|
|
}) {
|
|
eprintln!("Failed to send MIDI: {}", e);
|
|
}
|
|
}
|
|
MidiCommand::Quit => return jack::Control::Quit,
|
|
}
|
|
}
|
|
|
|
jack::Control::Continue
|
|
}
|
|
}
|
|
|
|
fn show_help() {
|
|
println!("\nFCB1010 MIDI simulator");
|
|
println!("=====================================");
|
|
println!("Button mappings:");
|
|
println!("1 - Button 1 (CC 20): Record/Arm / Tap Tempo");
|
|
println!("2 - Button 2 (CC 21): Play/Mute Track / Click Toggle");
|
|
println!("3 - Button 3 (CC 22): Solo Track");
|
|
println!("4 - Button 4 (CC 23): Overdub");
|
|
println!("5 - Button 5 (CC 24): Clear Track / Clear Column");
|
|
println!("6 - Button 6 (CC 25): Column 1 Select");
|
|
println!("7 - Button 7 (CC 26): Column 2 Select");
|
|
println!("8 - Button 8 (CC 27): Column 3 Select");
|
|
println!("9 - Button 9 (CC 28): Column 4 Select");
|
|
println!("0 - Button 10 (CC 29): Column 5 Select");
|
|
println!("u - UP (CC 30): Row Up / Mode Switch");
|
|
println!("d - DOWN (CC 31): Row Down / Mode Switch");
|
|
println!("e - Expression A (CC 1): Track/Click Volume (0-127)");
|
|
println!("m - Master Volume (CC 7): Master Volume (0-127)");
|
|
println!("h - Show this help");
|
|
println!("q - Quit\n");
|
|
}
|
|
|
|
fn get_expression_value(prompt: &str) -> Result<u8, Box<dyn std::error::Error>> {
|
|
print!("{}", prompt);
|
|
io::stdout().flush()?;
|
|
|
|
let mut input = String::new();
|
|
io::stdin().read_line(&mut input)?;
|
|
|
|
let value: u8 = input
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| "Invalid number. Please enter 0-127")?;
|
|
|
|
if value > 127 {
|
|
return Err("Value must be 0-127".into());
|
|
}
|
|
|
|
Ok(value)
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("Starting FCB1010 MIDI Simulator...");
|
|
|
|
// Create JACK client
|
|
let (client, status) = jack::Client::new("FCB1010", jack::ClientOptions::NO_START_SERVER)?;
|
|
if !status.is_empty() {
|
|
eprintln!("JACK client status: {:?}", status);
|
|
}
|
|
|
|
// Create command channel
|
|
let (command_sender, command_receiver): (Sender<MidiCommand>, Receiver<MidiCommand>) =
|
|
kanal::bounded(32);
|
|
|
|
// Create MIDI sender
|
|
let midi_sender = MidiSender::new(&client, command_receiver)?;
|
|
|
|
// Activate client
|
|
let _active_client = client.activate_async((), midi_sender)?;
|
|
|
|
show_help();
|
|
|
|
// Main input loop
|
|
loop {
|
|
print!("Command (h for help): ");
|
|
io::stdout().flush()?;
|
|
|
|
let mut input = String::new();
|
|
io::stdin().read_line(&mut input)?;
|
|
let input = input.trim().to_lowercase();
|
|
|
|
if input.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let first_char = input.chars().next().unwrap();
|
|
|
|
let command = match first_char {
|
|
'1' => Some(MidiCommand::ControlChange { cc: 20, value: 127 }),
|
|
'2' => Some(MidiCommand::ControlChange { cc: 21, value: 127 }),
|
|
'3' => Some(MidiCommand::ControlChange { cc: 22, value: 127 }),
|
|
'4' => Some(MidiCommand::ControlChange { cc: 23, value: 127 }),
|
|
'5' => Some(MidiCommand::ControlChange { cc: 24, value: 127 }),
|
|
'6' => Some(MidiCommand::ControlChange { cc: 25, value: 127 }),
|
|
'7' => Some(MidiCommand::ControlChange { cc: 26, value: 127 }),
|
|
'8' => Some(MidiCommand::ControlChange { cc: 27, value: 127 }),
|
|
'9' => Some(MidiCommand::ControlChange { cc: 28, value: 127 }),
|
|
'0' => Some(MidiCommand::ControlChange { cc: 29, value: 127 }),
|
|
'u' => Some(MidiCommand::ControlChange { cc: 30, value: 127 }),
|
|
'd' => Some(MidiCommand::ControlChange { cc: 31, value: 127 }),
|
|
'e' => match get_expression_value("Expression A value (0-127): ") {
|
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 1, value }),
|
|
Err(e) => {
|
|
eprintln!("Error: {}", e);
|
|
None
|
|
}
|
|
},
|
|
'm' => match get_expression_value("Master volume value (0-127): ") {
|
|
Ok(value) => Some(MidiCommand::ControlChange { cc: 7, value }),
|
|
Err(e) => {
|
|
eprintln!("Error: {}", e);
|
|
None
|
|
}
|
|
},
|
|
'h' => {
|
|
show_help();
|
|
None
|
|
}
|
|
'q' => {
|
|
println!("Exiting...");
|
|
command_sender.send(MidiCommand::Quit)?;
|
|
break;
|
|
}
|
|
_ => {
|
|
println!("Unknown command '{}'. Press 'h' for help.", first_char);
|
|
None
|
|
}
|
|
};
|
|
|
|
if let Some(cmd) = command {
|
|
if let MidiCommand::ControlChange { cc, value } = &cmd {
|
|
println!("Sending CC {} with value {}", cc, value);
|
|
}
|
|
command_sender.send(cmd)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|