153 lines
4.9 KiB
C++
153 lines
4.9 KiB
C++
#include "synth.h"
|
|
#include "code.h"
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <magic_enum.hpp>
|
|
#include "midi.h"
|
|
#include <argparse/argparse.hpp>
|
|
#include <log4cxx/logger.h>
|
|
#include <log4cxx/basicconfigurator.h>
|
|
#include <sstream>
|
|
|
|
#include <thread>
|
|
#include <chrono>
|
|
|
|
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<int>(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<std::vector<std::string>>("--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<std::string>("code")};
|
|
|
|
|
|
for (auto& group : code.getGroups()) {
|
|
LOG4CXX_INFO(logger, "Playing MIDI " << static_cast<int>(group[0]) << " & " << static_cast<int>(group[1]));
|
|
fluid_synth_noteon(raw_synth, 0, static_cast<int>(group[0]), 80);
|
|
fluid_synth_noteon(raw_synth, 1, static_cast<int>(group[1]), 80);
|
|
std::this_thread::sleep_for(std::chrono::duration<double>(toneDuration));
|
|
|
|
// Silence between tone groups
|
|
fluid_synth_all_notes_off(raw_synth, ALL_CHANNELS);
|
|
std::this_thread::sleep_for(std::chrono::duration<double>(silenceDuration));
|
|
}
|
|
|
|
// Wait long enough for the full tone to play before tearing down.
|
|
using namespace std::chrono_literals;
|
|
//std::this_thread::sleep_for(500ms);
|
|
}
|