From 9a63041a5e0ff716aab2bff8617b8b663e158d80 Mon Sep 17 00:00:00 2001 From: Jono Targett Date: Sun, 26 May 2024 00:00:49 +0930 Subject: [PATCH] Fixed weird stuff about tones not registering properly - was an audio conversion stereo/mono bug --- scripts/raw-to-wav.sh | 7 ++- soundfonts/basic.sf2 | Bin 88784 -> 177238 bytes src/main.cpp | 51 ++++++++++----------- src/midi.h | 23 ++++++++-- src/synth.cpp | 101 +++++++++++++++++++++++++++++++++--------- src/synth.h | 17 +++++-- 6 files changed, 140 insertions(+), 59 deletions(-) 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 06c11ac333521dfa8b290101e06b52148927789d..feb3c8986cf3449ac6ce35a0dc173285854bd2e1 100644 GIT binary patch literal 177238 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 aS}>-R&R^eeRek@zX$w31|D3`0kN*HYF&}^c delta 285 zcmccig6qOoR`wuIH@6c}j1$=d1wKSDG8CtjBo^lujW65dq#!e#pB z2*$|i`kR #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();