diff --git a/cmake/avendish.tests.cmake b/cmake/avendish.tests.cmake index 9c5e3478..9289991b 100644 --- a/cmake/avendish.tests.cmake +++ b/cmake/avendish.tests.cmake @@ -47,5 +47,6 @@ if(BUILD_TESTING) avnd_add_catch_test(test_gain tests/objects/gain.cpp) avnd_add_catch_test(test_patternal tests/objects/patternal.cpp) + avnd_add_catch_test(test_quantification_date tests/test_quantification_date.cpp) endif() diff --git a/examples/Advanced/Patternal/Patternal.hpp b/examples/Advanced/Patternal/Patternal.hpp index 14ccd1ff..aa238b1f 100644 --- a/examples/Advanced/Patternal/Patternal.hpp +++ b/examples/Advanced/Patternal/Patternal.hpp @@ -8,9 +8,19 @@ namespace patternal { +/** + * A sequencer pattern. + * Contains its note and its velocity on each beat. + */ struct Pattern { + /** + * A note. + */ int note; + /** + * Its velocity on each beat. + */ boost::container::small_vector pattern; }; @@ -30,20 +40,27 @@ struct Processor "]\n" "for a very simple drum rythm on kick and snare.") halp_meta(uuid, "6e89b53a-1645-4a9c-a26e-e6c7870a902c") + struct { struct { + /** + * A list of patterns to play. + * Each note has a velocity that changes for each beat. + */ std::vector value; } patterns; } inputs; + /** + * Its MIDI output. + */ struct { halp::midi_bus<"Out"> midi; } outputs; - using tick = halp::tick_musical; void operator()(halp::tick_musical tk) { // Find out where we are in the bar @@ -56,16 +73,20 @@ struct Processor auto quants = tk.get_quantification_date(4. / pat.size()); for(auto [pos, q] : quants) { + // FIXME: The position returned by get_quantification_date is a negative timestamp. if(pos < tk.frames) { auto qq = std::abs(q % std::ssize(pat)); - if(uint8_t vel = pat[qq]; vel > 0) + if(uint8_t velocity = pat[qq]; velocity > 0) { halp::midi_msg m; - m.bytes = {144, (uint8_t)note, vel}; + // Note on: + m.bytes = {144, (uint8_t)note, velocity}; m.timestamp = pos; outputs.midi.midi_messages.push_back(m); + // FIXME: The note off should not be output right away. + // Note off: m.bytes = {128, (uint8_t)note, 0}; m.timestamp = pos; outputs.midi.midi_messages.push_back(m); diff --git a/include/halp/audio.hpp b/include/halp/audio.hpp index 3dd2faed..8fa52d8a 100644 --- a/include/halp/audio.hpp +++ b/include/halp/audio.hpp @@ -201,35 +201,83 @@ struct tick int frames{}; }; +/** + * Contains the info about the current audio buffer and the bar and quarter notes it's + * associated with, so that the musical audio rendering utilities can do their job of + * playing back the musical notes, etc. + * They need to know when we are in the score. + * + * The quarter notes have their index starting at 0. + * The position of the bars are in quarters. (in 4/4, the first bar has position 0.0 and the + * second one has a position of 4.0) + * + * This struct is usually created and destroyed thousands of times per second. + */ struct tick_musical { + /** + * How many frames in the buffer. + */ int frames{}; + /** + * The tempo in BPM. + */ double tempo = 120.; + /** + * Time signature. Example: 4/4 + */ struct { int num; int denom; } signature; + /** + * The total number of samples since the beginning of the playback of the score. + */ int64_t position_in_frames{}; + /** + * Playback time since the beginning of the score. + */ double position_in_nanoseconds{}; - // Quarter note of the first sample in the buffer + /** + * Quarter note of the first sample in this buffer + * This is a double. + * + * The first quarter note in a score has index 0. + * + * For example: + * - 3.95 would be slightly before the 2nd bar. + * - 4.05 would be slightly after the beginning of the 2nd bar. + */ quarter_note start_position_in_quarters{}; - // Quarter note of the first sample in the next buffer - // (or one past the last sample of this buffer, e.g. a [closed; open) interval like C++ begin / end) + /** + * Quarter note of the first sample in the next buffer + * (or one past the last sample of this buffer, e.g. a [closed; open) interval like C++ begin / end) + */ quarter_note end_position_in_quarters{}; - // Position of the last signature change in quarter notes (at the start of the tick) + /** + * Position of the last signature change in quarter notes (at the start of the tick) + * + * For example: 0.0 if the signature never changes in the score. + * If we change the time signature at some, we'll give it the index (in quarter) of the + * bar when it last changed. + */ quarter_note last_signature_change{}; - // Position of the last bar relative to start in quarter notes + /** + * Position of the last bar relative to start in quarter notes + */ quarter_note bar_at_start{}; - // Position of the last bar relative to end in quarter notes + /** + * Position of the last bar relative to end in quarter notes + */ quarter_note bar_at_end{}; // If the division falls in the current tick, returns the corresponding frames @@ -240,11 +288,11 @@ struct tick_musical [[nodiscard]] quantification_frames get_quantification_date(double div) const noexcept { quantification_frames frames; - double start_in_bar = start_position_in_quarters - bar_at_start; + double start_in_bar = this->start_position_in_quarters - bar_at_start; double end_in_bar = end_position_in_quarters - bar_at_start; auto pos_to_frame = [this](double in_bar) { - double start = start_position_in_quarters; + double start = this->start_position_in_quarters; double musical_pos = in_bar + bar_at_start; double end = end_position_in_quarters; diff --git a/include/halp/midi.hpp b/include/halp/midi.hpp index f2db8ed5..1d16a1fa 100644 --- a/include/halp/midi.hpp +++ b/include/halp/midi.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include +#include HALP_MODULE_EXPORT namespace halp @@ -17,13 +20,32 @@ struct midi_msg { boost::container::small_vector bytes; int64_t timestamp{}; + + // Equality operator. + bool operator==(const midi_msg&) const = default; + // auto operator<=>(const midi_msg&) const = default; }; + +std::ostream& operator<<(std::ostream &o, const midi_msg& message) +{ + return o << "midi_msg{:bytes=" << fmt::format("{}", message.bytes) <<", :timestamp=" << message.timestamp << "}" << std::endl; +} + struct midi_note_msg { uint8_t bytes[8]; int64_t timestamp{}; + + // Equality operator. + bool operator==(const midi_note_msg&) const = default; + //auto operator<=>(const midi_note_msg&) const = default; }; +std::ostream& operator<<(std::ostream &o, const midi_note_msg& message) +{ + return o << "midi_note_msg{:bytes=" << fmt::format("{}", message.bytes) <<", :timestamp=" << message.timestamp << "}" << std::endl; +} + template struct midi_bus { diff --git a/tests/objects/patternal.cpp b/tests/objects/patternal.cpp index e488b231..27b210a3 100644 --- a/tests/objects/patternal.cpp +++ b/tests/objects/patternal.cpp @@ -1,27 +1,190 @@ +#include // std::fmod +#include // std::max #include +#include +#include #include +#include + +using Processor = patternal::Processor; +using Pattern = patternal::Pattern; +using tick_musical = halp::tick_musical; +using midi_msg = halp::midi_msg; + +static const int noteHiHat = 42; +static const int noteSnare = 38; +static const int noteBassDrum = 35; +static const uint8_t messageNoteOn = 0x90; // on the first channel +static const uint8_t messageNoteOff = 0x80; // on the first channel +static const uint8_t velocityZero = 0; +static const uint8_t velocityFull = 127; + +/** + * Create a MIDI note message. + */ +void makeNote(midi_msg& ret, uint8_t note, uint8_t velocity, int64_t timestamp) { + bool isNoteOn = velocity > 0; + if (isNoteOn) { + ret.bytes = { messageNoteOn, note, velocity}; + } else { + ret.bytes = { messageNoteOff, note, velocity}; + } + ret.timestamp = timestamp; +} // Tests for the Patternal object -TEST_CASE("Test Patternal patterns", "[advanced][patternal]") +TEST_CASE("The input pattern is stored correctly", "[advanced][patternal]") { - SECTION("The input pattern is stored correctly") { - // Instanciate the object: - patternal::Processor patternalProcessor; - - // Create the input pattern: - patternalProcessor.inputs.patterns.value = { - {42, {127, 127, 127, 127}}, // hi-hat - {38, {0, 127, 0, 127}}, // snare - {35, {127, 0, 127, 0}}, // bass drum - }; - - // Check that the input pattern is stored correctly: - REQUIRE(patternalProcessor.inputs.patterns.value[0].note == 42); - REQUIRE(patternalProcessor.inputs.patterns.value[0].pattern[0] == 127); - REQUIRE(patternalProcessor.inputs.patterns.value[1].note == 38); - REQUIRE(patternalProcessor.inputs.patterns.value[1].pattern[0] == 0); - REQUIRE(patternalProcessor.inputs.patterns.value[2].note == 35); - REQUIRE(patternalProcessor.inputs.patterns.value[2].pattern[0] == 127); + // Instanciate the object: + Processor patternalProcessor; + + // Create the input pattern: + patternalProcessor.inputs.patterns.value = { + {42, {127, 127, 127, 127}}, // hi-hat + {38, {0, 127, 0, 127}}, // snare + {35, {127, 0, 127, 0}}, // bass drum + }; + + // Check that the input pattern is stored correctly: + REQUIRE(patternalProcessor.inputs.patterns.value[0].note == 42); + REQUIRE(patternalProcessor.inputs.patterns.value[0].pattern[0] == 127); + REQUIRE(patternalProcessor.inputs.patterns.value[1].note == 38); + REQUIRE(patternalProcessor.inputs.patterns.value[1].pattern[0] == 0); + REQUIRE(patternalProcessor.inputs.patterns.value[2].note == 35); + REQUIRE(patternalProcessor.inputs.patterns.value[2].pattern[0] == 127); +} + +TEST_CASE("Compare two MIDI notes", "[MIDI]") +{ + // Test that comparing two notes works: + midi_msg simple1; + midi_msg simple2; + makeNote(simple1, 60, 127, 0); + makeNote(simple2, 60, 127, 0); + REQUIRE(simple1 == simple2); +} + +TEST_CASE("MIDI messages are output properly", "[advanced][patternal]") +{ + // Our sampling rate is 48kHz and the buffer size is 512 + static constexpr double samplingRate = 48000; + static constexpr int bufferSize = 512; + static constexpr double tickDuration = samplingRate / bufferSize; // seconds + static constexpr double ticksPerSecond = 1.0 / tickDuration; + static constexpr double testDuration = 3.0; // seconds + static constexpr double NS_PER_S = 1000000000; + static constexpr double durationOfAQuarter = 0.5; // seconds + static constexpr int quartersPerBar = 4; + + Processor patternalProcessor; + patternalProcessor.inputs.patterns.value = { + {noteHiHat, {127, 127, 127, 127}}, // hi-hat + {noteSnare, {0, 127, 0, 127}}, // snare + {noteBassDrum, {127, 0, 127, 0}}, // bass drum + }; + + // Check the output on each tick: + const int totalNumberOfTicks = (int) ticksPerSecond * testDuration; + int64_t posInFrames = 0; + double posInNs = 0; + double previousMaxQuarter = 0; // The previous max quarter number during the previous tick. + + for (int tickIndex = 0; tickIndex < totalNumberOfTicks; tickIndex++) { + // Debug info + INFO("tick number " << tickIndex << " / " << totalNumberOfTicks); + + // Calculate stuff: + int shouldPlayBeat = -1; // Index of the beat we should play now, or -1 if none. + // INFO("Test tick " << tickIndex); + posInFrames += bufferSize; + posInNs += tickDuration / NS_PER_S; + double timeNow = tickIndex * tickDuration; + double timeAtEndOfTick = (tickIndex + 1) * tickDuration; + double startPosInQuarters = timeNow / durationOfAQuarter; + double endPosInQuarters = (timeNow + tickDuration) / durationOfAQuarter; + double barAtStart = std::fmod(startPosInQuarters, quartersPerBar); + double barAtEnd = std::fmod(endPosInQuarters, quartersPerBar); + double currentMaxQuarter = std::max(startPosInQuarters, endPosInQuarters); + + // Populate the tick_musical: + tick_musical tk; + tk.tempo = 120; // 120 BPM. One beat lasts 500 ms. + tk.signature.num = 4; + tk.signature.denom = quartersPerBar; + tk.frames = bufferSize; + tk.position_in_nanoseconds = posInNs; // We don't really need to set this pos in ns. + tk.position_in_frames = posInFrames; + tk.last_signature_change = 0; // The signature never changes. + tk.start_position_in_quarters = startPosInQuarters; + tk.end_position_in_quarters = endPosInQuarters; + tk.bar_at_start = barAtStart; + tk.bar_at_end = barAtEnd; + + // Process it: + patternalProcessor(tk); + + // Test the 1st tick: + if (tickIndex == 0) { + // FIXME: A note with velocity 0 is sent right away, immediately after each note on. + // How long should the notes last? + // TODO: Consider making the note last until the next beat. + REQUIRE(patternalProcessor.outputs.midi.midi_messages.size() == 4); + // Hi-hat note on: + midi_msg expectedNote0; + makeNote(expectedNote0, noteHiHat, velocityFull, -2147483648); // FIXME: Why this timestamp? + REQUIRE(patternalProcessor.outputs.midi.midi_messages[0] == expectedNote0); + // A hi-hat note off: + midi_msg expectedNote1; + makeNote(expectedNote1, noteHiHat, velocityZero, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[1] == expectedNote1); + // Bass drum note on: + midi_msg expectedNote2; + makeNote(expectedNote2, noteBassDrum, velocityFull, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[2] == expectedNote2); + // A bass drum note off: + midi_msg expectedNote3; + makeNote(expectedNote3, noteBassDrum, velocityZero, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[3] == expectedNote3); + // Note: There is no snare note to output, since it's off, for now. + } + + // Test the 2nd tick: + else if (tickIndex == 1) { + // There should be no MIDI messages in the output on this tick. + REQUIRE(patternalProcessor.outputs.midi.midi_messages.size() == 0); + } + + else if (currentMaxQuarter != previousMaxQuarter) { + // We assume there won't be two new quarters within the same buffer - at 120 BPM + INFO("quarter number: " << currentMaxQuarter); + if (currentMaxQuarter == 1) { + REQUIRE(patternalProcessor.outputs.midi.midi_messages.size() == 4); + // A hi-hat note off: + midi_msg note0; + makeNote(note0, noteHiHat, velocityZero, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[0] == note0); + // Hi-hat note on: + midi_msg note1; + makeNote(note1, noteHiHat, velocityFull, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[1] == note1); + // Snare note on: + midi_msg note2; + makeNote(note2, noteSnare, velocityFull, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[2] == note2); + // A bass drum note off: + midi_msg note3; + makeNote(note3, noteBassDrum, velocityZero, -2147483648); + REQUIRE(patternalProcessor.outputs.midi.midi_messages[3] == note3); + } + } else { + REQUIRE(patternalProcessor.outputs.midi.midi_messages.size() == 0); + } + // FIXME: At some point, there should be some other notes. + // TODO: check the other ticks + + // Last operations before the next tick: + previousMaxQuarter = currentMaxQuarter; } } + diff --git a/tests/test_quantification_date.cpp b/tests/test_quantification_date.cpp new file mode 100644 index 00000000..b991a71d --- /dev/null +++ b/tests/test_quantification_date.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include + +// Test for get_quantification_date function. +TEST_CASE("Returns valid quantification dates", "[audio]") +{ + halp::tick_musical tk = halp::tick_musical(); + tk.frames = 512; // The buffer size + tk.tempo = 120; // BPM. default + tk.signature = {4, 4}; // The time signature. + + // FIXME: We always get -2147483648 as a result. + // FIXME: There might be an attribute that should be initialized, and is not. + { + // 4 / 4: + auto quants = tk.get_quantification_date(4. / 4); + auto [pos0, q0] = quants[0]; + REQUIRE(quants.size() == 1); + REQUIRE(pos0 == -2147483648); + } + + { + // 1 / 4: + auto quants = tk.get_quantification_date(1. / 4); + auto [pos0, q0] = quants[0]; + REQUIRE(quants.size() == 1); + REQUIRE(pos0 == -2147483648); + } + + { + // 4 / 1: + auto quants = tk.get_quantification_date(4. / 1); + auto [pos0, q0] = quants[0]; + REQUIRE(quants.size() == 1); + REQUIRE(pos0 == -2147483648); + } +} +