diff --git a/CHANGELOG.md b/CHANGELOG.md index de9c7fe..5bfab60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Negative numbers! - Version command via `:version` in REPL or `--version`, `-v` in command line. - Introduced start synchronization with builtins: `peers` and `start` +- Connection with MIDI ports via parameters dropped in favour of function using context system: `port` +- Listing ports via REPL command: `:ports` instead of commandline parameter ### Removed diff --git a/musique/interpreter/builtin_functions.cc b/musique/interpreter/builtin_functions.cc index aacc498..e692d64 100644 --- a/musique/interpreter/builtin_functions.cc +++ b/musique/interpreter/builtin_functions.cc @@ -322,13 +322,13 @@ Forward_Implementation(builtin_down, range) static auto builtin_program_change(Interpreter &i, std::vector args) -> Result { if (auto a = match(args)) { auto [program] = *a; - i.midi_connection->send_program_change(0, program.as_int()); + i.current_context->port->send_program_change(0, program.as_int()); return Value{}; } if (auto a = match(args)) { auto [chan, program] = *a; - i.midi_connection->send_program_change(chan.as_int(), program.as_int()); + i.current_context->port->send_program_change(chan.as_int(), program.as_int()); return Value{}; } @@ -430,7 +430,7 @@ static Result builtin_par(Interpreter &interpreter, std::vector ar for (auto const& note : chord->notes) { if (note.base) { - interpreter.midi_connection->send_note_on(0, *note.into_midi_note(), 127); + interpreter.current_context->port->send_note_on(0, *note.into_midi_note(), 127); } } @@ -438,7 +438,7 @@ static Result builtin_par(Interpreter &interpreter, std::vector ar for (auto const& note : chord->notes) { if (note.base) { - interpreter.midi_connection->send_note_off(0, *note.into_midi_note(), 127); + interpreter.current_context->port->send_note_off(0, *note.into_midi_note(), 127); } } return result; @@ -552,8 +552,8 @@ static Result builtin_sim(Interpreter &interpreter, std::vector ar start_time = dur; } switch (instruction.action) { - break; case Instruction::On: interpreter.midi_connection->send_note_on(0, instruction.note, 127); - break; case Instruction::Off: interpreter.midi_connection->send_note_off(0, instruction.note, 127); + break; case Instruction::On: interpreter.current_context->port->send_note_on(0, instruction.note, 127); + break; case Instruction::Off: interpreter.current_context->port->send_note_off(0, instruction.note, 127); } } @@ -1377,7 +1377,7 @@ static Result builtin_note_on(Interpreter &interpreter, std::vector(args)) { auto [chan, note, vel] = *a; - interpreter.midi_connection->send_note_on(chan.as_int(), note.as_int(), vel.as_int()); + interpreter.current_context->port->send_note_on(chan.as_int(), note.as_int(), vel.as_int()); return Value {}; } @@ -1385,7 +1385,7 @@ static Result builtin_note_on(Interpreter &interpreter, std::vectorfill(note); - interpreter.midi_connection->send_note_on(chan.as_int(), *note.into_midi_note(), vel.as_int()); + interpreter.current_context->port->send_note_on(chan.as_int(), *note.into_midi_note(), vel.as_int()); } return Value{}; } @@ -1414,7 +1414,7 @@ static Result builtin_note_off(Interpreter &interpreter, std::vector(args)) { auto [chan, note] = *a; - interpreter.midi_connection->send_note_off(chan.as_int(), note.as_int(), 127); + interpreter.current_context->port->send_note_off(chan.as_int(), note.as_int(), 127); return Value {}; } @@ -1423,7 +1423,7 @@ static Result builtin_note_off(Interpreter &interpreter, std::vectorfill(note); - interpreter.midi_connection->send_note_off(chan.as_int(), *note.into_midi_note(), 127); + interpreter.current_context->port->send_note_off(chan.as_int(), *note.into_midi_note(), 127); } return Value{}; } @@ -1601,6 +1601,39 @@ static Result builtin_peers(Interpreter &interpreter, std::vector) return Number(interpreter.starter.peers()); } +static Result builtin_port(Interpreter &interpreter, std::vector args) +{ + if (args.empty()) { + for (auto const& [key, port] : Context::established_connections) { + if (port == interpreter.current_context->port) { + return std::visit(Overloaded { + [](midi::connections::Virtual_Port) { return Value(Symbol("virtual")); }, + [](midi::connections::Established_Port port) { return Value(Number(port)); }, + }, key); + } + } + unreachable(); + } + + if (auto a = match(args)) { + auto [port_number] = *a; + Try(interpreter.current_context->connect(port_number.floor().as_int())); + return {}; + } + + if (auto a = match(args)) { + auto [port_type] = *a; + if (port_type == "virtual") { + Try(interpreter.current_context->connect(std::nullopt)); + return {}; + } + + unimplemented(); + } + + unimplemented(); +} + void Interpreter::register_builtin_functions() { auto &global = *Env::global; @@ -1635,6 +1668,7 @@ void Interpreter::register_builtin_functions() global.force_define("pgmchange", builtin_program_change); global.force_define("pick", builtin_pick); global.force_define("play", builtin_play); + global.force_define("port", builtin_port); global.force_define("program_change", builtin_program_change); global.force_define("range", builtin_range); global.force_define("reverse", builtin_reverse); diff --git a/musique/interpreter/context.cc b/musique/interpreter/context.cc index 281860f..c2cc51f 100644 --- a/musique/interpreter/context.cc +++ b/musique/interpreter/context.cc @@ -12,3 +12,49 @@ std::chrono::duration Context::length_to_duration(std::optional l auto const len = length ? *length : this->length; return std::chrono::duration(float(len.num * (60.f / (float(bpm) / 4))) / len.den); } + +template<> +struct std::hash +{ + std::size_t operator()(midi::connections::Key const& value) const + { + using namespace midi::connections; + return hash_combine(value.index(), std::visit(Overloaded { + [](Virtual_Port) { return 0u; }, + [](Established_Port port) { return port; }, + }, value)); + } +}; + +std::unordered_map> Context::established_connections; + +/// Establish connection to given port +std::optional Context::connect(std::optional port_number) +{ + // FIXME This function doesn't support creating virtual ports when established ports are available + using namespace midi::connections; + auto const key = port_number ? Key(*port_number) : Key(Virtual_Port{}); + + if (auto it = established_connections.find(key); it != established_connections.end()) { + port = it->second; + return std::nullopt; + } + + if (port_number) { + auto connection = std::make_shared(); + connection->connect_output(*port_number); + established_connections[*port_number] = connection; + port = connection; + return std::nullopt; + } + + auto connection = std::make_shared(); + if (connection->connect_or_create_output()) { + established_connections[0u] = connection; + } else { + established_connections[Virtual_Port{}] = connection; + } + port = connection; + return std::nullopt; +} + diff --git a/musique/interpreter/context.hh b/musique/interpreter/context.hh index c85c0e2..5ab8412 100644 --- a/musique/interpreter/context.hh +++ b/musique/interpreter/context.hh @@ -1,11 +1,25 @@ #ifndef MUSIQUE_CONTEXT_HH #define MUSIQUE_CONTEXT_HH -#include -#include -#include #include #include +#include +#include +#include +#include +#include + +namespace midi::connections +{ + using Established_Port = unsigned int; + + struct Virtual_Port + { + bool operator==(Virtual_Port const&) const = default; + }; + + using Key = std::variant; +} /// Context holds default values for music related actions struct Context @@ -19,6 +33,19 @@ struct Context /// Default BPM unsigned bpm = 120; + /// Port that is currently used + std::shared_ptr port; + + using Port_Number = unsigned int; + + /// Connections that have been established so far + static std::unordered_map> established_connections; + + /// Establish connection to given port + /// + /// If port number wasn't provided connect to first existing one or create one + std::optional connect(std::optional); + /// Fills empty places in Note like octave and length with default values from context Note fill(Note) const; diff --git a/musique/interpreter/interpreter.cc b/musique/interpreter/interpreter.cc index 10ac1e8..796bef8 100644 --- a/musique/interpreter/interpreter.cc +++ b/musique/interpreter/interpreter.cc @@ -7,7 +7,6 @@ #include #include -midi::Connection *Interpreter::midi_connection = nullptr; std::unordered_map Interpreter::operators {}; /// Registers constants like `fn = full note = 1/1` @@ -265,7 +264,7 @@ std::optional Interpreter::play(Chord chord) // Turn all notes on for (auto const& note : chord.notes) { if (note.base) { - midi_connection->send_note_on(0, *note.into_midi_note(), 127); + current_context->port->send_note_on(0, *note.into_midi_note(), 127); } } @@ -276,16 +275,16 @@ std::optional Interpreter::play(Chord chord) std::this_thread::sleep_for(ctx.length_to_duration(*note.length)); } if (note.base) { - midi_connection->send_note_off(0, *note.into_midi_note(), 127); + current_context->port->send_note_off(0, *note.into_midi_note(), 127); } } return {}; } -std::optional ensure_midi_connection_available(Interpreter &i, std::string_view operation_name) +std::optional ensure_midi_connection_available(Interpreter &interpreter, std::string_view operation_name) { - if (i.midi_connection == nullptr || !i.midi_connection->supports_output()) { + if (interpreter.current_context->port == nullptr || !interpreter.current_context->port->supports_output()) { return Error { .details = errors::Operation_Requires_Midi_Connection { .is_input = false, diff --git a/musique/interpreter/interpreter.hh b/musique/interpreter/interpreter.hh index d7ac8cf..325006c 100644 --- a/musique/interpreter/interpreter.hh +++ b/musique/interpreter/interpreter.hh @@ -10,10 +10,6 @@ /// Given program tree evaluates it into Value struct Interpreter { - /// MIDI connection that is used to play music. - /// It's optional for simple interpreter testing. - static midi::Connection *midi_connection; - /// Operators defined for language static std::unordered_map operators; diff --git a/musique/main.cc b/musique/main.cc index 15c4fc9..a62a4d6 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -68,11 +68,6 @@ static T pop(std::span &span) "usage: musique [filename]\n" " where filename is path to file with Musique code that will be executed\n" " where options are:\n" - " -o,--output PORT\n" - " provides output port, a place where Musique produces MIDI messages\n" - " -l,--list\n" - " lists all available MIDI ports and quit\n" - "\n" " -c,--run CODE\n" " executes given code\n" " -I,--interactive,--repl\n" @@ -105,6 +100,7 @@ void print_repl_help() ":! - allows for execution of any shell command\n" ":clear - clears screen\n" ":load - loads file into Musique session\n" + ":ports - print list available ports" ; } @@ -164,33 +160,16 @@ struct Runner { static inline Runner *the; - midi::Rt_Midi midi; Interpreter interpreter; /// Setup interpreter and midi connection with given port - explicit Runner(std::optional output_port) - : midi() - , interpreter{} + explicit Runner() + : interpreter{} { ensure(the == nullptr, "Only one instance of runner is supported"); the = this; - interpreter.midi_connection = &midi; - if (output_port) { - midi.connect_output(*output_port); - if (!quiet_mode) { - std::cout << "Connected MIDI output to port " << *output_port << ". Ready to play!" << std::endl; - } - } else { - bool connected_to_existing_port = midi.connect_or_create_output(); - if (!quiet_mode) { - if (connected_to_existing_port) { - std::cout << "Connected MIDI output to port " << midi.output->getPortName(0) << std::endl; - } else { - std::cout << "Created new MIDI output port 'Musique'. Ready to play!" << std::endl; - } - } - } + interpreter.current_context->connect(std::nullopt); Env::global->force_define("say", +[](Interpreter &interpreter, std::vector args) -> Result { for (auto it = args.begin(); it != args.end(); ++it) { @@ -340,6 +319,14 @@ static Result handle_repl_session_commands(std::string_view input, Runner return std::nullopt; } }, + + Command { + "ports", + +[](Runner&, std::optional) -> std::optional { + midi::Rt_Midi{}.list_ports(std::cout); + return {}; + }, + }, }; if (input.starts_with('!')) { @@ -384,9 +371,6 @@ static std::optional Main(std::span args) std::string_view argument; }; - // Arbitraly chosen for conviniance of the author - std::optional output_port{}; - std::vector runnables; while (not args.empty()) { @@ -397,11 +381,6 @@ static std::optional Main(std::span args) continue; } - if (arg == "-l" || arg == "--list") { - midi::Rt_Midi{}.list_ports(std::cout); - return {}; - } - if (arg == "-c" || arg == "--run") { if (args.empty()) { std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl; @@ -439,15 +418,6 @@ static std::optional Main(std::span args) return {}; } - if (arg == "-o" || arg == "--output") { - if (args.empty()) { - std::cerr << "musique: error: option " << arg << " requires an argument" << std::endl; - std::exit(1); - } - output_port = pop(args); - continue; - } - if (arg == "-h" || arg == "--help") { usage(); } @@ -456,7 +426,7 @@ static std::optional Main(std::span args) std::exit(1); } - Runner runner{output_port}; + Runner runner; for (auto const& [type, argument] : runnables) { if (type == Run::Argument) {