use std::{collections::HashMap, error::Error};
use midly::live::LiveEvent;
use serde::{Deserialize, Serialize};
use super::midi::{self, ToMidiEvent};
pub const DEFAULT_GRPC_PORT: u16 = 43234;
pub const DEFAULT_OSC_PORT: u16 = 43235;
fn default_osc_play() -> String {
"/mtrack/play".to_string()
}
fn default_osc_prev() -> String {
"/mtrack/prev".to_string()
}
fn default_osc_next() -> String {
"/mtrack/next".to_string()
}
fn default_osc_stop() -> String {
"/mtrack/stop".to_string()
}
fn default_osc_all_songs() -> String {
"/mtrack/all_songs".to_string()
}
fn default_osc_playlist() -> String {
"/mtrack/playlist".to_string()
}
fn default_osc_stop_samples() -> String {
"/mtrack/samples/stop".to_string()
}
fn default_osc_section_ack() -> String {
"/mtrack/section_ack".to_string()
}
fn default_osc_stop_section_loop() -> String {
"/mtrack/stop_section_loop".to_string()
}
fn default_osc_loop_section() -> String {
"/mtrack/loop_section".to_string()
}
fn default_osc_status() -> String {
"/mtrack/status".to_string()
}
fn default_osc_playlist_current() -> String {
"/mtrack/playlist/current".to_string()
}
fn default_osc_playlist_current_song() -> String {
"/mtrack/playlist/current_song".to_string()
}
fn default_osc_playlist_current_song_elapsed() -> String {
"/mtrack/playlist/current_song/elapsed".to_string()
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Controller {
Grpc(GrpcController),
Midi(MidiController),
Multi(HashMap<String, Controller>),
Osc(Box<OscController>),
}
#[derive(Deserialize, Serialize, Clone)]
pub struct MidiController {
play: midi::Event,
prev: midi::Event,
next: midi::Event,
stop: midi::Event,
all_songs: midi::Event,
playlist: midi::Event,
#[serde(default)]
section_ack: Option<midi::Event>,
#[serde(default)]
stop_section_loop: Option<midi::Event>,
#[serde(default)]
morningstar: Option<MorningstarConfig>,
}
impl MidiController {
#[cfg(test)]
pub fn new(
play: midi::Event,
prev: midi::Event,
next: midi::Event,
stop: midi::Event,
all_songs: midi::Event,
playlist: midi::Event,
) -> MidiController {
MidiController {
play,
prev,
next,
stop,
all_songs,
playlist,
section_ack: None,
stop_section_loop: None,
morningstar: None,
}
}
pub fn play(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.play.to_midi_event()
}
pub fn prev(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.prev.to_midi_event()
}
pub fn next(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.next.to_midi_event()
}
pub fn stop(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.stop.to_midi_event()
}
pub fn all_songs(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.all_songs.to_midi_event()
}
pub fn playlist(&self) -> Result<LiveEvent<'static>, Box<dyn Error>> {
self.playlist.to_midi_event()
}
pub fn section_ack(&self) -> Result<Option<LiveEvent<'static>>, Box<dyn Error>> {
self.section_ack
.as_ref()
.map(|e| e.to_midi_event())
.transpose()
}
pub fn stop_section_loop(&self) -> Result<Option<LiveEvent<'static>>, Box<dyn Error>> {
self.stop_section_loop
.as_ref()
.map(|e| e.to_midi_event())
.transpose()
}
pub fn morningstar(&self) -> Option<&MorningstarConfig> {
self.morningstar.as_ref()
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct GrpcController {
port: Option<u16>,
}
impl GrpcController {
#[cfg(test)]
pub fn new(port: u16) -> GrpcController {
GrpcController { port: Some(port) }
}
pub fn port(&self) -> u16 {
self.port.unwrap_or(DEFAULT_GRPC_PORT)
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct OscController {
#[serde(default = "default_osc_port")]
port: u16,
#[serde(default)]
broadcast_addresses: Vec<String>,
#[serde(default = "default_osc_play")]
play: String,
#[serde(default = "default_osc_prev")]
prev: String,
#[serde(default = "default_osc_next")]
next: String,
#[serde(default = "default_osc_stop")]
stop: String,
#[serde(default = "default_osc_all_songs")]
all_songs: String,
#[serde(default = "default_osc_playlist")]
playlist: String,
#[serde(default = "default_osc_stop_samples")]
stop_samples: String,
#[serde(default = "default_osc_section_ack")]
section_ack: String,
#[serde(default = "default_osc_stop_section_loop")]
stop_section_loop: String,
#[serde(default = "default_osc_loop_section")]
loop_section: String,
#[serde(default = "default_osc_status")]
status: String,
#[serde(default = "default_osc_playlist_current")]
playlist_current: String,
#[serde(default = "default_osc_playlist_current_song")]
playlist_current_song: String,
#[serde(default = "default_osc_playlist_current_song_elapsed")]
playlist_current_song_elapsed: String,
}
fn default_osc_port() -> u16 {
DEFAULT_OSC_PORT
}
impl Default for OscController {
fn default() -> Self {
OscController {
port: DEFAULT_OSC_PORT,
broadcast_addresses: Vec::new(),
play: default_osc_play(),
prev: default_osc_prev(),
next: default_osc_next(),
stop: default_osc_stop(),
all_songs: default_osc_all_songs(),
playlist: default_osc_playlist(),
stop_samples: default_osc_stop_samples(),
section_ack: default_osc_section_ack(),
stop_section_loop: default_osc_stop_section_loop(),
loop_section: default_osc_loop_section(),
status: default_osc_status(),
playlist_current: default_osc_playlist_current(),
playlist_current_song: default_osc_playlist_current_song(),
playlist_current_song_elapsed: default_osc_playlist_current_song_elapsed(),
}
}
}
impl OscController {
#[cfg(test)]
pub fn new() -> OscController {
OscController::default()
}
pub fn port(&self) -> u16 {
self.port
}
pub fn broadcast_addresses(&self) -> &[String] {
&self.broadcast_addresses
}
pub fn play(&self) -> &str {
&self.play
}
pub fn prev(&self) -> &str {
&self.prev
}
pub fn next(&self) -> &str {
&self.next
}
pub fn stop(&self) -> &str {
&self.stop
}
pub fn all_songs(&self) -> &str {
&self.all_songs
}
pub fn playlist(&self) -> &str {
&self.playlist
}
pub fn stop_samples(&self) -> &str {
&self.stop_samples
}
pub fn section_ack(&self) -> &str {
&self.section_ack
}
pub fn stop_section_loop(&self) -> &str {
&self.stop_section_loop
}
pub fn loop_section(&self) -> &str {
&self.loop_section
}
pub fn status(&self) -> &str {
&self.status
}
pub fn playlist_current(&self) -> &str {
&self.playlist_current
}
pub fn playlist_current_song(&self) -> &str {
&self.playlist_current_song
}
pub fn playlist_current_song_elapsed(&self) -> &str {
&self.playlist_current_song_elapsed
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MorningstarConfig {
model: MorningstarModel,
#[serde(default)]
save: bool,
}
impl MorningstarConfig {
#[cfg(test)]
pub fn new(model: MorningstarModel, save: bool) -> MorningstarConfig {
MorningstarConfig { model, save }
}
pub fn model_id(&self) -> u8 {
self.model.device_id()
}
pub fn save(&self) -> bool {
self.save
}
pub fn name_length(&self) -> usize {
self.model.name_length()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum MorningstarModel {
MC3,
MC6,
MC8,
#[serde(rename = "mc6pro")]
MC6Pro,
#[serde(rename = "mc8pro")]
MC8Pro,
#[serde(rename = "mc4pro")]
MC4Pro,
Custom(CustomModel),
}
impl MorningstarModel {
fn device_id(&self) -> u8 {
match self {
MorningstarModel::MC3 => 0x05,
MorningstarModel::MC6 => 0x03,
MorningstarModel::MC8 => 0x04,
MorningstarModel::MC6Pro => 0x06,
MorningstarModel::MC8Pro => 0x08,
MorningstarModel::MC4Pro => 0x09,
MorningstarModel::Custom(c) => c.model_id,
}
}
fn name_length(&self) -> usize {
match self {
MorningstarModel::MC3 => 16,
MorningstarModel::MC6 | MorningstarModel::MC8 => 24,
MorningstarModel::MC6Pro | MorningstarModel::MC8Pro | MorningstarModel::MC4Pro => 32,
MorningstarModel::Custom(_) => 32,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct CustomModel {
pub model_id: u8,
}
#[cfg(test)]
mod test {
use std::error::Error;
use config::{Config, File, FileFormat};
use super::*;
#[test]
fn grpc_default_port() {
let grpc = GrpcController::default();
assert_eq!(grpc.port(), DEFAULT_GRPC_PORT);
}
#[test]
fn grpc_custom_port() {
let grpc = GrpcController::new(9999);
assert_eq!(grpc.port(), 9999);
}
#[test]
fn grpc_serde_default() -> Result<(), Box<dyn Error>> {
let grpc: GrpcController = Config::builder()
.add_source(File::from_str("{}", FileFormat::Yaml))
.build()?
.try_deserialize()?;
assert_eq!(grpc.port(), DEFAULT_GRPC_PORT);
Ok(())
}
#[test]
fn grpc_serde_custom_port() -> Result<(), Box<dyn Error>> {
let grpc: GrpcController = Config::builder()
.add_source(File::from_str("port: 5000", FileFormat::Yaml))
.build()?
.try_deserialize()?;
assert_eq!(grpc.port(), 5000);
Ok(())
}
#[test]
fn osc_defaults() {
let osc = OscController::default();
assert_eq!(osc.port(), DEFAULT_OSC_PORT);
assert!(osc.broadcast_addresses().is_empty());
assert_eq!(osc.play(), "/mtrack/play");
assert_eq!(osc.prev(), "/mtrack/prev");
assert_eq!(osc.next(), "/mtrack/next");
assert_eq!(osc.stop(), "/mtrack/stop");
assert_eq!(osc.all_songs(), "/mtrack/all_songs");
assert_eq!(osc.playlist(), "/mtrack/playlist");
assert_eq!(osc.stop_samples(), "/mtrack/samples/stop");
assert_eq!(osc.status(), "/mtrack/status");
assert_eq!(osc.playlist_current(), "/mtrack/playlist/current");
assert_eq!(osc.playlist_current_song(), "/mtrack/playlist/current_song");
assert_eq!(
osc.playlist_current_song_elapsed(),
"/mtrack/playlist/current_song/elapsed"
);
}
#[test]
fn osc_serde_custom_addresses() -> Result<(), Box<dyn Error>> {
let osc: OscController = Config::builder()
.add_source(File::from_str(
r#"
port: 9000
play: /custom/play
stop: /custom/stop
broadcast_addresses:
- "192.168.1.100:9000"
"#,
FileFormat::Yaml,
))
.build()?
.try_deserialize()?;
assert_eq!(osc.port(), 9000);
assert_eq!(osc.play(), "/custom/play");
assert_eq!(osc.stop(), "/custom/stop");
assert_eq!(osc.next(), "/mtrack/next");
assert_eq!(osc.prev(), "/mtrack/prev");
assert_eq!(
osc.broadcast_addresses(),
&["192.168.1.100:9000".to_string()]
);
Ok(())
}
#[test]
fn midi_controller_events() -> Result<(), Box<dyn Error>> {
let mc = MidiController::new(
midi::note_on(1, 60, 127),
midi::note_on(1, 61, 127),
midi::note_on(1, 62, 127),
midi::note_on(1, 63, 127),
midi::note_on(1, 64, 127),
midi::note_on(1, 65, 127),
);
mc.play()?;
mc.prev()?;
mc.next()?;
mc.stop()?;
mc.all_songs()?;
mc.playlist()?;
Ok(())
}
#[test]
fn controller_enum_grpc_serde() -> Result<(), Box<dyn Error>> {
let controller: Controller = Config::builder()
.add_source(File::from_str(
r#"
kind: grpc
port: 1234
"#,
FileFormat::Yaml,
))
.build()?
.try_deserialize()?;
match controller {
Controller::Grpc(grpc) => assert_eq!(grpc.port(), 1234),
_ => panic!("expected Grpc variant"),
}
Ok(())
}
#[test]
fn controller_enum_osc_serde() -> Result<(), Box<dyn Error>> {
let controller: Controller = Config::builder()
.add_source(File::from_str(
r#"
kind: osc
port: 5555
"#,
FileFormat::Yaml,
))
.build()?
.try_deserialize()?;
match controller {
Controller::Osc(osc) => assert_eq!(osc.port(), 5555),
_ => panic!("expected Osc variant"),
}
Ok(())
}
}