diff --git a/scripts/raw-to-wav.sh b/scripts/raw-to-wav.sh index e66d024..0341479 100755 --- a/scripts/raw-to-wav.sh +++ b/scripts/raw-to-wav.sh @@ -4,5 +4,10 @@ set -eux input_file="$1" output_file="$2" +tempfile=$(mktemp /tmp/stereoXXXXXX.wav) -sox -r 44100 -e signed-integer -b 16 -c 1 ${input_file} ${output_file} \ No newline at end of file +# Two step process: +# 1. Convert s16 PCM to a stereo wav file +# 2. Convert stereo wav file to mono +sox -r 44100 -e signed-integer -b 16 -c 2 ${input_file} ${tempfile} +sox ${tempfile} -c 1 ${output_file} diff --git a/soundfonts/basic.sf2 b/soundfonts/basic.sf2 index 06c11ac..feb3c89 100644 Binary files a/soundfonts/basic.sf2 and b/soundfonts/basic.sf2 differ diff --git a/src/main.cpp b/src/main.cpp index 44b4127..df44bbd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,10 +7,12 @@ #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; @@ -24,24 +26,27 @@ static auto logger = log4cxx::Logger::getLogger("tone-generator"); * 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. + * but we can make those frequencies anything we want. */ -Tuning getSelcalTunings() { - Tuning tunings; - tunings.fill(0); +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) { - // 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; + 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) { @@ -89,12 +94,13 @@ int main(int argc, char** argv) { return 1; } - + // Handle fluidsynth log messages before touching anything else in the lib + redirect_fluid_logs(); Synth synth; - auto tunings = getSelcalTunings(); - if (!synth.setTuning(tunings)) { + auto tuning = getSelcalFrequencies(); + if (!synth.setTuning(tuning)) { LOG4CXX_ERROR(logger, "Failed to set SELCAL tuning on synth"); return 1; } @@ -130,10 +136,9 @@ int main(int argc, char** argv) { for (auto& group : code.getGroups()) { - int midi = static_cast(group[0]); - std::cout << "Playing MIDI " << midi << std::endl; - fluid_synth_noteon(raw_synth, 0, static_cast(group[0]), 127); - fluid_synth_noteon(raw_synth, 1, static_cast(group[1]), 127); + 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 @@ -141,16 +146,6 @@ int main(int argc, char** argv) { std::this_thread::sleep_for(std::chrono::duration(silenceDuration)); } - /* - for (int i = 65; i < 90; ++i) { - fluid_synth_noteon(raw_synth, 0, i, 80); // Soundfont root key is TWELVE OUT?? - std::this_thread::sleep_for(std::chrono::duration(0.1)); - - // Silence between tone groups - fluid_synth_all_notes_off(raw_synth, ALL_CHANNELS); - std::this_thread::sleep_for(std::chrono::duration(0.01)); - }*/ - // 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/midi.h b/src/midi.h index 7ba544d..43ce974 100644 --- a/src/midi.h +++ b/src/midi.h @@ -1,11 +1,26 @@ #pragma once #include +#include -double frequencyToMidi(double f) { - return ((12 * std::log(f / 220.0) / std::log(2.0)) + 57.01); +// Applicable for standard (even temperament) MIDI tuning only. +// The formula connecting the MIDI note number and the base frequency assume equal tuning based +// on A4 = 440 Hz. +constexpr int NUM_MIDI_KEYS = 128; // Also inclusive of 0 & 128, apparently. +constexpr double A4_FREQUENCY = 440.0; // Hz +constexpr int A4_MIDI_KEY = 69; // Hehe +constexpr int KEYS_PER_OCTAVE = 12; + +// Template parameter allows for (non-standard) fractional keys. +template +constexpr double MidiKeyToFrequency(T key) { + return A4_FREQUENCY * std::pow(2, (key - A4_MIDI_KEY) / KEYS_PER_OCTAVE); } -int frequencyToNearestMidi(double f) { - return (int) frequencyToMidi(f); +constexpr double frequencyToMidiKey(double f) { + return ((KEYS_PER_OCTAVE * std::log2(f / A4_FREQUENCY)) + A4_MIDI_KEY); +} + +constexpr int frequencyToNearestMidiKey(double f) { + return std::clamp(static_cast(std::round(frequencyToMidiKey(f))), 0, NUM_MIDI_KEYS); } diff --git a/src/synth.cpp b/src/synth.cpp index 54bf8c8..1a901a2 100644 --- a/src/synth.cpp +++ b/src/synth.cpp @@ -1,6 +1,18 @@ #include "synth.h" #include +#include +#include + +namespace { +// Helpers for the C API stuff +constexpr int TRUE = 1; +constexpr int FALSE = 0; + +static auto logger = log4cxx::Logger::getLogger("synth"); +static auto fluidLogger = log4cxx::Logger::getLogger("synth.fluid"); +} + Synth::Synth() { try { @@ -20,8 +32,8 @@ Synth::Synth() { fluid_settings_setstr(settings, "audio.file.type", "raw"); fluid_settings_setnum(settings, "synth.sample-rate", 44100); fluid_settings_setnum(settings, "synth.gain", 1.0); - fluid_settings_setint(settings, "synth.chorus.active", 0); - fluid_settings_setint(settings, "synth.reverb.active", 0); + //fluid_settings_setint(settings, "synth.chorus.active", 0); + //fluid_settings_setint(settings, "synth.reverb.active", 0); adriver = new_fluid_audio_driver(settings, synth); if (adriver == nullptr) { @@ -78,33 +90,35 @@ bool Synth::loadProgram(const Program& program) { return true; } -bool Synth::setTuning(const std::array& pitches) { - int result = fluid_synth_activate_key_tuning( - synth, - 0, 0, - "default", - pitches.data(), - 1 +bool Synth::setTuning(const TuningFrequencies& frequencies) { + // fluidsynth wants tunings in MIDI cents, which effectively means MIDI key * 100. + // For example: normally note 0 is 0.0, 1 is 100.0, 60 is 6000.0, etc. + constexpr double MIDI_CENT_SCALE = 100.0; + std::array pitchCents; + std::transform( + frequencies.begin(), + frequencies.end(), + pitchCents.begin(), + [] (double frequency) { + // Remap 0 Hz to MIDI 0, otherwise we get -inf which causes some interpolations between + // notes to result in zero sound. + return frequency == 0.0 ? 0.0 : frequencyToMidiKey(frequency) * MIDI_CENT_SCALE; + } ); - if (result == FLUID_OK) { - result = fluid_synth_activate_tuning(synth, 0, 0, 0, 1); - result = fluid_synth_activate_tuning(synth, 1, 0, 0, 1); + std::stringstream ss; + for (auto p : pitchCents) { + ss << p << ", "; } + LOG4CXX_DEBUG(logger, "Tuning pitches [" << ss.str() << "]"); - return result == FLUID_OK; + auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", pitchCents.data(), FALSE); + return (result == FLUID_OK) ? activateTuning() : false; } bool Synth::resetTuning() { - int result = fluid_synth_activate_key_tuning( - synth, - 0, 0, - "default", - nullptr, - 1 - ); - - return result == FLUID_OK; + auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", nullptr, 1); + return (result == FLUID_OK) ? activateTuning() : false; } const std::vector& Synth::getPrograms() const { @@ -114,3 +128,46 @@ const std::vector& Synth::getPrograms() const { fluid_synth_t* Synth::getSynth() { return synth; } + + +bool Synth::activateTuning() { + int channelIndex = 0; + int result; + + do { + result = fluid_synth_activate_tuning(synth, channelIndex++, 0, 0, TRUE); + } while (result == FLUID_OK && channelIndex < NUM_MIDI_CHANNELS); + + LOG4CXX_DEBUG(logger, "Tuning activation result: " << result); + + return result == FLUID_OK; +} + +// Fluid logging redirector. +void fluid_log_function(int level, const char* message, void* data) { + switch (level) { + case FLUID_PANIC: + LOG4CXX_FATAL(fluidLogger, message); + break; + case FLUID_ERR: + LOG4CXX_ERROR(fluidLogger, message); + break; + case FLUID_WARN: + LOG4CXX_WARN(fluidLogger, message); + break; + case FLUID_INFO: + LOG4CXX_INFO(fluidLogger, message); + break; + case FLUID_DBG: + LOG4CXX_DEBUG(fluidLogger, message); + break; + } +} + +void redirect_fluid_logs() { + fluid_set_log_function(FLUID_PANIC, fluid_log_function, NULL); + fluid_set_log_function(FLUID_ERR, fluid_log_function, NULL); + fluid_set_log_function(FLUID_WARN, fluid_log_function, NULL); + fluid_set_log_function(FLUID_INFO, fluid_log_function, NULL); + fluid_set_log_function(FLUID_DBG, fluid_log_function, NULL); +} \ No newline at end of file diff --git a/src/synth.h b/src/synth.h index eb7a575..f0d780b 100644 --- a/src/synth.h +++ b/src/synth.h @@ -5,9 +5,13 @@ #include #include -constexpr int NUM_MIDI_NOTES = 128; +#include "midi.h" -typedef std::array Tuning; +namespace { +constexpr int NUM_MIDI_CHANNELS = 2; +} + +typedef std::array TuningFrequencies; /** * C++ wrapper around the fluidsynth API. @@ -32,7 +36,7 @@ public: bool loadSoundfont(const std::string& filepath); bool loadProgram(const Program& program); - bool setTuning(const Tuning& pitches); + bool setTuning(const TuningFrequencies& frequencies); bool resetTuning(); const std::vector& getPrograms() const; @@ -41,7 +45,7 @@ public: fluid_synth_t* getSynth(); private: - + bool activateTuning(); std::vector soundfonts; std::vector programs; @@ -50,3 +54,8 @@ private: fluid_synth_t* synth = NULL; fluid_audio_driver_t* adriver = NULL; }; + + +// Fluid logging redirector. +void fluid_log_function(int level, const char* message, void* data); +void redirect_fluid_logs();