#include "synth.h" #include "code.h" #include #include #include #include "midi.h" #include #include #include #include #include #include namespace { // 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; static auto logger = log4cxx::Logger::getLogger("tone-generator"); /** * 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. */ TuningFrequencies getSelcalFrequencies() { TuningFrequencies tunings; tunings.fill(0.0); // 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. std::stringstream ss; for (auto pair : SELCAL::KeyFrequencies) { auto value = static_cast(pair.first); tunings[value] = pair.second; ss << value << ":" << pair.second << ", "; } LOG4CXX_DEBUG(logger, "SELCAL freqs {" << ss.str() << "}"); return tunings; } } // anonymous int main(int argc, char** argv) { // Required to be up front before any logging occurs. log4cxx::BasicConfigurator::configure(); 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; } // Handle fluidsynth log messages before touching anything else in the lib redirect_fluid_logs(); Synth synth; auto tuning = getSelcalFrequencies(); if (!synth.setTuning(tuning)) { LOG4CXX_ERROR(logger, "Failed to set SELCAL tuning on synth"); return 1; } auto files = parser.get>("--soundfont"); for (auto file : files) { LOG4CXX_INFO(logger, "Loading soundfont from " << file); 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")) { LOG4CXX_INFO(logger, "Loading program " << program.name); synth.loadProgram(program); break; } } } // TODO JMT: Hacky, find a better way to interact with the synth fluid_synth_t* raw_synth = synth.getSynth(); SELCAL::Code code{parser.get("code")}; for (auto& group : code.getGroups()) { LOG4CXX_INFO(logger, "Playing MIDI " << static_cast(group[0]) << " & " << static_cast(group[1])); fluid_synth_noteon(raw_synth, 0, static_cast(group[0]), 80); fluid_synth_noteon(raw_synth, 1, static_cast(group[1]), 80); 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(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); }