diff --git a/CMakeLists.txt b/CMakeLists.txt index 69992c3..67ebc1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(SELCALToneGenerator VERSION 0.1.0) set(CMAKE_CXX_STANDARD 17) add_subdirectory(fluidsynth) +add_subdirectory(magic_enum) file(GLOB_RECURSE sources src/*.cpp src/*.h) file(GLOB_RECURSE data resources/*) @@ -11,3 +12,4 @@ file(GLOB_RECURSE data resources/*) add_executable(tone-generator ${sources} ${data}) target_link_libraries(tone-generator PRIVATE libfluidsynth) +target_link_libraries(tone-generator PRIVATE magic_enum::magic_enum) diff --git a/README.md b/README.md index 793c8f5..cd3fd7b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,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 mkdir build cd build cmake .. @@ -35,4 +36,4 @@ fluid-soundfont-gm/focal,focal 3.1-5.1 all fluid-soundfont-gs/focal,focal 3.1-5.1 all Fluid (R3) General MIDI SoundFont (GS) -``` \ No newline at end of file +``` diff --git a/src/code.cpp b/src/code.cpp new file mode 100644 index 0000000..427b94e --- /dev/null +++ b/src/code.cpp @@ -0,0 +1,14 @@ +#include "code.h" + +namespace SELCAL { + +Code::Code(std::string s) : code(s) { + +} + +bool Code::isValid() const { + // TODO JMT + return false; +} + +} // SELCAL diff --git a/src/code.h b/src/code.h new file mode 100644 index 0000000..ed3609d --- /dev/null +++ b/src/code.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + + +namespace SELCAL { + +constexpr int KEYS_PER_GROUP = 2; +constexpr char GROUP_SEPARATOR = '-'; + +enum class Key : char { + KEY_1 = '1', + KEY_2 = '2', + KEY_3 = '3', + KEY_4 = '4', + KEY_5 = '5', + KEY_6 = '6', + KEY_7 = '7', + KEY_8 = '8', + KEY_9 = '9', + KEY_A = 'A', + KEY_B = 'B', + KEY_C = 'C', + KEY_D = 'D', + KEY_E = 'E', + KEY_F = 'F', + KEY_G = 'G', + KEY_H = 'H', + KEY_J = 'J', + KEY_K = 'K', + KEY_L = 'L', + KEY_M = 'M', + KEY_P = 'P', + KEY_Q = 'Q', + KEY_R = 'R', + KEY_S = 'S', + KEY_T = 'T', + KEY_U = 'U', + KEY_V = 'V', + KEY_W = 'W', + KEY_X = 'X', + KEY_Y = 'Y', + KEY_Z = 'Z', +}; + +typedef std::array Group; + +class Code { +public: + Code(std::string s); + +private: + bool isValid() const; + + const std::string code; + const std::vector groups; +}; + +} // SELCAL diff --git a/src/main.cpp b/src/main.cpp index bddb1d2..eecf55d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,126 +1,107 @@ -/* - - An example of how to use FluidSynth. - - To compile it on Linux: - $ gcc -o example example.c `pkg-config fluidsynth --libs` - - To compile it on Windows: - ... - - - Author: Peter Hanappe. - This code is in the public domain. Use it as you like. - -*/ - -#include +#include "synth.h" +#include "code.h" #include +#include +#include +#include "midi.h" -#if defined(_WIN32) -#include -#define sleep(_t) Sleep(_t * 1000) -#include -#define getpid _getpid -#else -#include -#include -#endif +#include +#include + +int main(int argc, char** argv) { + + Synth synth; + + 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 == "Tuba") { + 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; + tunings.fill(0); + + + for (auto pair : keyFrequencies) { + std::cout + << magic_enum::enum_name(pair.first) << " = " + << pair.second << " Hz " + << " or MIDI key " << frequencyToMidi(pair.second) + << std::endl; + + // 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. + constexpr double MIDI_CENT_SCALE = 100.0; + const int keyIndex = static_cast(pair.first); + tunings[keyIndex] = frequencyToMidi(pair.second * MIDI_CENT_SCALE); + } + + bool success = synth.setTuning(tunings); + std::cout << "Set tuning: " << (success ? "success" : "failed") << std::endl; + + + fluid_synth_t* raw_synth = synth.getSynth(); + using namespace std::chrono_literals; -int main(int argc, char **argv) -{ - fluid_settings_t *settings = NULL; - fluid_synth_t *synth = NULL; - fluid_audio_driver_t *adriver = NULL; - int sfont_id; int i, key; - - fluid_sfont_t* sfont = nullptr; - fluid_preset_t* preset = nullptr; - - /* Create the settings. */ - settings = new_fluid_settings(); - if(settings == NULL) - { - puts("Failed to create the settings!"); - goto err; - } - - /* Change the settings if necessary*/ - - /* Create the synthesizer. */ - synth = new_fluid_synth(settings); - if(synth == NULL) - { - puts("Failed to create the synth!"); - goto err; - } - - /* Load a SoundFont and reset presets (so that new instruments - * get used from the SoundFont) - * Depending on the size of the SoundFont, this will take some time to complete... - */ - sfont_id = fluid_synth_sfload(synth, argv[1], 1); - if(sfont_id == FLUID_FAILED) - { - puts("Loading the SoundFont failed!"); - goto err; - } - - // Get the SoundFont - sfont = fluid_synth_get_sfont_by_id(synth, sfont_id); - - if (!sfont) { - std::cerr << "Failed to get SoundFont by ID" << std::endl; - delete_fluid_synth(synth); - delete_fluid_settings(settings); - return 1; - } - - // Enumerate the presets (programs) in the SoundFont - - std::cout << "Programs in SoundFont " << argv[1] << ":" << std::endl; - fluid_sfont_iteration_start(sfont); - while ((preset = fluid_sfont_iteration_next(sfont)) != nullptr) { - std::cout << "Bank: " << fluid_preset_get_banknum(preset) - << ", Program: " << fluid_preset_get_num(preset) - << ", Name: " << fluid_preset_get_name(preset) << std::endl; - } - - fluid_synth_program_select(synth, 0, sfont_id, 0, 0); - - /* Create the audio driver. The synthesizer starts playing as soon - as the driver is created. */ - adriver = new_fluid_audio_driver(settings, synth); - if(adriver == NULL) - { - puts("Failed to create the audio driver!"); - goto err; - } - - /* Initialize the random number generator */ - srand(getpid()); - for(i = 0; i < 12; i++) { /* Generate a random key */ - key = 60 + (int)(12.0f * rand() / (float) RAND_MAX); + key = 30 + (int)(12.0f * rand() / (float) RAND_MAX); /* Play a note */ - fluid_synth_noteon(synth, 0, key, 100); + fluid_synth_noteon(raw_synth, 0, key, 100); /* Sleep for 1 second */ - sleep(1); + std::this_thread::sleep_for(1s); /* Stop the note */ - fluid_synth_noteoff(synth, 0, key); + fluid_synth_noteoff(raw_synth, 0, key); } -err: - /* Clean up */ - delete_fluid_audio_driver(adriver); - delete_fluid_synth(synth); - delete_fluid_settings(settings); - - return 0; -} \ No newline at end of file +} diff --git a/src/midi.h b/src/midi.h new file mode 100644 index 0000000..7ba544d --- /dev/null +++ b/src/midi.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +double frequencyToMidi(double f) { + return ((12 * std::log(f / 220.0) / std::log(2.0)) + 57.01); +} + +int frequencyToNearestMidi(double f) { + return (int) frequencyToMidi(f); +} diff --git a/src/synth.cpp b/src/synth.cpp new file mode 100644 index 0000000..112ef27 --- /dev/null +++ b/src/synth.cpp @@ -0,0 +1,101 @@ +#include "synth.h" + +#include + +Synth::Synth() { + try { + settings = new_fluid_settings(); + if (settings == nullptr) { + throw new std::runtime_error("Unable to create fluid settings"); + } + + synth = new_fluid_synth(settings); + if (synth == nullptr) { + throw new std::runtime_error("Unable to create fluid synth"); + } + + adriver = new_fluid_audio_driver(settings, synth); + if (adriver == nullptr) { + throw new std::runtime_error("Unable to create audio driver"); + } + } + catch (std::runtime_error& e) { + // Do a mem-safe cleanup and rethrow. + // TODO JMT: Do these handle receiving nullptr for deletes? + delete_fluid_audio_driver(adriver); + delete_fluid_synth(synth); + delete_fluid_settings(settings); + throw; + } +} + +Synth::~Synth() { + // TODO JMT: Do I need to manually clear all of the soundfonts and programs I have handles for? + delete_fluid_audio_driver(adriver); + delete_fluid_synth(synth); + delete_fluid_settings(settings); +} + +bool Synth::loadSoundfont(const std::string& filepath) { + int soundfontId = fluid_synth_sfload(synth, filepath.c_str(), 1); + if(soundfontId == FLUID_FAILED) { + return false; + } + + fluid_sfont_t* soundfontHandle = fluid_synth_get_sfont_by_id(synth, soundfontId); + const std::string soundfontName{fluid_sfont_get_name(soundfontHandle)}; + + Synth::Soundfont soundfont{soundfontName, soundfontId}; + soundfonts.push_back(soundfont); + + fluid_preset_t* preset = nullptr; + fluid_sfont_iteration_start(soundfontHandle); + while ((preset = fluid_sfont_iteration_next(soundfontHandle)) != nullptr) { + const std::string presetName{fluid_preset_get_name(preset)}; + const int bankNumber = fluid_preset_get_banknum(preset); + const int programNumber = fluid_preset_get_num(preset); + + Synth::Program program {presetName, soundfontId, bankNumber, programNumber}; + programs.push_back(program); + } + + return true; +} + +bool Synth::loadProgram(const Program& program) { + // TODO JMT: fix this + fluid_synth_program_select(synth, 0, program.soundfontId, program.bankNumber, program.programNumber); + return true; +} + +bool Synth::setTuning(const std::array& pitches) { + int result = fluid_synth_activate_key_tuning( + synth, + 0, 0, + "default", + pitches.data(), + 1 + ); + + return result == FLUID_OK; +} + +bool Synth::resetTuning() { + int result = fluid_synth_activate_key_tuning( + synth, + 0, 0, + "default", + nullptr, + 1 + ); + + return result == FLUID_OK; +} + +const std::vector& Synth::getPrograms() const { + return programs; +} + +fluid_synth_t* Synth::getSynth() { + return synth; +} diff --git a/src/synth.h b/src/synth.h new file mode 100644 index 0000000..38d3981 --- /dev/null +++ b/src/synth.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +constexpr int NUM_MIDI_NOTES = 128; + +/** + * C++ wrapper around the fluidsynth API. + */ +class Synth { +public: + Synth(); + ~Synth(); + + struct Soundfont { + std::string name; + int id; + }; + + struct Program { + std::string name; + int soundfontId; + int bankNumber; + int programNumber; + }; + + bool loadSoundfont(const std::string& filepath); + bool loadProgram(const Program& program); + + bool setTuning(const std::array& pitches); + bool resetTuning(); + + const std::vector& getPrograms() const; + + // TODO JMT: remove this + fluid_synth_t* getSynth(); + +private: + + + std::vector soundfonts; + std::vector programs; + + fluid_settings_t* settings = NULL; + fluid_synth_t* synth = NULL; + fluid_audio_driver_t* adriver = NULL; +};