From ab9ee2e49367868582310e3ebe1760bd4aa02e21 Mon Sep 17 00:00:00 2001 From: Jono Targett Date: Fri, 24 May 2024 23:24:24 +0930 Subject: [PATCH] Added proper command line args --- CMakeLists.txt | 2 + README.md | 1 + src/code.h | 36 ++++++++++ src/main.cpp | 180 ++++++++++++++++++++++++++++--------------------- src/synth.h | 4 +- 5 files changed, 144 insertions(+), 79 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 67ebc1c..e7bd365 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17) add_subdirectory(fluidsynth) add_subdirectory(magic_enum) +add_subdirectory(argparse) file(GLOB_RECURSE sources src/*.cpp src/*.h) file(GLOB_RECURSE data resources/*) @@ -13,3 +14,4 @@ add_executable(tone-generator ${sources} ${data}) target_link_libraries(tone-generator PRIVATE libfluidsynth) target_link_libraries(tone-generator PRIVATE magic_enum::magic_enum) +target_link_libraries(tone-generator PRIVATE argparse) \ No newline at end of file diff --git a/README.md b/README.md index cd3fd7b..9f9ca8b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Assumes that the fluidsynth sources are in the fluidsynth/ directory. ``` git clone https://github.com/FluidSynth/fluidsynth.git git clone https://github.com/Neargye/magic_enum.git +git clone https://github.com/p-ranav/argparse.git mkdir build cd build cmake .. diff --git a/src/code.h b/src/code.h index 7a3c061..070bf78 100644 --- a/src/code.h +++ b/src/code.h @@ -3,6 +3,7 @@ #include #include #include +#include namespace SELCAL { @@ -45,6 +46,41 @@ enum class Key : char { KEY_Z = 'Z', }; +const std::map KeyFrequencies = { + {Key::KEY_1, 680.0}, + {Key::KEY_2, 754.2}, + {Key::KEY_3, 836.6}, + {Key::KEY_4, 927.9}, + {Key::KEY_5, 1029.2}, + {Key::KEY_6, 1141.6}, + {Key::KEY_7, 1266.2}, + {Key::KEY_8, 1404.4}, + {Key::KEY_9, 1557.8}, + {Key::KEY_A, 312.6}, + {Key::KEY_B, 346.7}, + {Key::KEY_C, 384.6}, + {Key::KEY_D, 426.6}, + {Key::KEY_E, 473.2}, + {Key::KEY_F, 524.8}, + {Key::KEY_G, 582.1}, + {Key::KEY_H, 645.7}, + {Key::KEY_J, 716.1}, + {Key::KEY_K, 794.3}, + {Key::KEY_L, 881.0}, + {Key::KEY_M, 977.2}, + {Key::KEY_P, 1083.9}, + {Key::KEY_Q, 1202.3}, + {Key::KEY_R, 1333.5}, + {Key::KEY_S, 1479.1}, + {Key::KEY_T, 329.2}, + {Key::KEY_U, 365.2}, + {Key::KEY_V, 405.0}, + {Key::KEY_W, 449.3}, + {Key::KEY_X, 498.3}, + {Key::KEY_Y, 552.7}, + {Key::KEY_Z, 613.1}, +}; + typedef std::array Group; class Code { diff --git a/src/main.cpp b/src/main.cpp index 2ea8abd..6de93f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,84 +4,31 @@ #include #include #include "midi.h" +#include #include #include -int main(int argc, char** argv) { - - SELCAL::Code code{argv[1]}; - - for (auto& group : code.getGroups()) { - std::cout - << magic_enum::enum_name(group[0]) << " + " - << magic_enum::enum_name(group[1]) - << std::endl; - } +// TODO JMT: Does fluidsynth define this itself? +constexpr int ALL_CHANNELS = -1; +constexpr double DEFAULT_TONE_DURATION = 1.0; +constexpr double DEFAULT_SILENCE_DURATION = 0.2; - Synth synth; - synth.loadSoundfont("../soundfonts/square-equaltemperament.sf2"); - //synth.loadSoundfont("../soundfonts/TimGM6mb.sf2"); - synth.loadSoundfont("../soundfonts/GU-1.471.sf2"); - - /* - for(int i = 1; i < argc; ++i) { - synth.loadSoundfont(argv[i]); - } - */ - - for (const Synth::Program& program : synth.getPrograms()) { - std::cout << program.name << " " << program.soundfontId - << ":(" << program.bankNumber << ", " << program.programNumber << ")" - << std::endl; - - if (program.name == "Night Vision") { - std::cout << "Loading program!" << std::endl; - synth.loadProgram(program); - } - } - - std::map keyFrequencies = { - {SELCAL::Key::KEY_1, 680.0}, - {SELCAL::Key::KEY_2, 754.2}, - {SELCAL::Key::KEY_3, 836.6}, - {SELCAL::Key::KEY_4, 927.9}, - {SELCAL::Key::KEY_5, 1029.2}, - {SELCAL::Key::KEY_6, 1141.6}, - {SELCAL::Key::KEY_7, 1266.2}, - {SELCAL::Key::KEY_8, 1404.4}, - {SELCAL::Key::KEY_9, 1557.8}, - {SELCAL::Key::KEY_A, 312.6}, - {SELCAL::Key::KEY_B, 346.7}, - {SELCAL::Key::KEY_C, 384.6}, - {SELCAL::Key::KEY_D, 426.6}, - {SELCAL::Key::KEY_E, 473.2}, - {SELCAL::Key::KEY_F, 524.8}, - {SELCAL::Key::KEY_G, 582.1}, - {SELCAL::Key::KEY_H, 645.7}, - {SELCAL::Key::KEY_J, 716.1}, - {SELCAL::Key::KEY_K, 794.3}, - {SELCAL::Key::KEY_L, 881.0}, - {SELCAL::Key::KEY_M, 977.2}, - {SELCAL::Key::KEY_P, 1083.9}, - {SELCAL::Key::KEY_Q, 1202.3}, - {SELCAL::Key::KEY_R, 1333.5}, - {SELCAL::Key::KEY_S, 1479.1}, - {SELCAL::Key::KEY_T, 329.2}, - {SELCAL::Key::KEY_U, 365.2}, - {SELCAL::Key::KEY_V, 405.0}, - {SELCAL::Key::KEY_W, 449.3}, - {SELCAL::Key::KEY_X, 498.3}, - {SELCAL::Key::KEY_Y, 552.7}, - {SELCAL::Key::KEY_Z, 613.1}, - }; - - std::array tunings; +/** + * The MIDI format only has a fixed number of notes it can play - 128 individual frequencies. + * The default tuning is an equal temperament around middle C, but that means that it is impossible + * to exactly play the the SELCAL tones. Worse again, using the nearest standard MIDI note available + * results in collisions for SELCAL tones. + * The way around this is to define a custom MIDI tuning. We're still limited to 128 frequencies, + * but we can make those frequencies anything we want. This method generates a tuning in the format + * that fluidsynth accepts which _only_ contains the SELCAL tones. + */ +Tuning getSelcalTunings() { + Tuning tunings; tunings.fill(0); - - for (auto pair : keyFrequencies) { + for (auto pair : SELCAL::KeyFrequencies) { // fluidsynth wants tunings in MIDI cents, which effectively means MIDI key * 100. // As we only care about the SELCAL 32 tones, only tune those specific keys. We don't // need to tune the full 128-key keyboard. @@ -90,24 +37,101 @@ int main(int argc, char** argv) { tunings[keyIndex] = frequencyToMidi(pair.second) * MIDI_CENT_SCALE; } + return tunings; +} + + +int main(int argc, char** argv) { + double toneDuration = DEFAULT_TONE_DURATION; + double silenceDuration = DEFAULT_SILENCE_DURATION; + bool listInstruments = false; + + argparse::ArgumentParser parser("tone-generator"); + + parser.add_argument("-d", "--duration") + .default_value(DEFAULT_TONE_DURATION) + .store_into(toneDuration) + .help("Duration of the tone for each group in the SELCAL code."); + + parser.add_argument("-s", "--silence") + .default_value(DEFAULT_SILENCE_DURATION) + .store_into(silenceDuration) + .help("Duration of the silence between tone groups."); + + parser.add_argument("--soundfont") + .nargs(argparse::nargs_pattern::any) + .help("Additional soundfont(s) to load into the synth. Accepts files in .sf2 format."); + + parser.add_argument("--instrument") + .default_value("midi") + .help("The soundfont synth profile to use when generating tones."); + + parser.add_argument("--list-instruments") + .flag() + .store_into(listInstruments) + .help("List available instrument profiles and exit."); + + parser.add_argument("code") + .help("Selcal code in the format AB-CD."); + + try { + parser.parse_args(argc, argv); + } + catch (const std::exception& err) { + std::cerr << err.what() << std::endl; + std::cerr << parser; + return 1; + } + + + Synth synth; + + auto tunings = getSelcalTunings(); + if (!synth.setTuning(tunings)) { + std::cerr << "Failed to set SELCAL tuning on synth" << std::endl; + return 1; + } + + auto files = parser.get>("--soundfont"); + for (auto file : files) { + std::cout << "Loading soundfont from " << file << std::endl; + synth.loadSoundfont(file); + } + + if (listInstruments) { + for (const Synth::Program& program : synth.getPrograms()) { + std::cout << program.name << ", "; + } + std::cout << std::endl; + return 0; + } + else { + for (const Synth::Program& program : synth.getPrograms()) { + if (program.name == parser.get("--instrument")) { + std::cout << "Loading program " << program.name << std::endl; + synth.loadProgram(program); + break; + } + } + } + + + // TODO JMT: Hacky, find a better way to interact with the synth fluid_synth_t* raw_synth = synth.getSynth(); - constexpr int ALL_CHANNELS = -1; - bool success; - using namespace std::chrono_literals; - - success = synth.setTuning(tunings); - std::cout << "Set tuning: " << (success ? "success" : "failed") << std::endl; + SELCAL::Code code{parser.get("code")}; for (auto& group : code.getGroups()) { fluid_synth_noteon(raw_synth, 0, static_cast(group[0]), 127); fluid_synth_noteon(raw_synth, 1, static_cast(group[1]), 127); - std::this_thread::sleep_for(1s); + std::this_thread::sleep_for(std::chrono::duration(toneDuration)); // Silence between tone groups fluid_synth_all_notes_off(raw_synth, ALL_CHANNELS); - std::this_thread::sleep_for(200ms); + std::this_thread::sleep_for(std::chrono::duration(silenceDuration)); } + // Wait long enough for the full tone to play before tearing down. + using namespace std::chrono_literals; std::this_thread::sleep_for(500ms); } diff --git a/src/synth.h b/src/synth.h index 38d3981..eb7a575 100644 --- a/src/synth.h +++ b/src/synth.h @@ -7,6 +7,8 @@ constexpr int NUM_MIDI_NOTES = 128; +typedef std::array Tuning; + /** * C++ wrapper around the fluidsynth API. */ @@ -30,7 +32,7 @@ public: bool loadSoundfont(const std::string& filepath); bool loadProgram(const Program& program); - bool setTuning(const std::array& pitches); + bool setTuning(const Tuning& pitches); bool resetTuning(); const std::vector& getPrograms() const;