diff --git a/modules/yup_audio_basics/buffers/yup_AudioChannelSet.cpp b/modules/yup_audio_basics/buffers/yup_AudioChannelSet.cpp index 5b131ab8f..60f1d397a 100644 --- a/modules/yup_audio_basics/buffers/yup_AudioChannelSet.cpp +++ b/modules/yup_audio_basics/buffers/yup_AudioChannelSet.cpp @@ -1159,100 +1159,4 @@ int AudioChannelSet::getAmbisonicOrderForNumChannels (int numChannels, int maxOr return -1; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class AudioChannelSetUnitTest final : public UnitTest -{ -public: - AudioChannelSetUnitTest() - : UnitTest ("AudioChannelSetUnitTest", UnitTestCategories::audio) - { - } - - void runTest() override - { - auto max = AudioChannelSet::maxChannelsOfNamedLayout; - - beginTest ("maxChannelsOfNamedLayout is non-discrete"); - expect (AudioChannelSet::channelSetsWithNumberOfChannels (max).size() >= 2); - - beginTest ("channelSetsWithNumberOfChannels returns correct speaker count"); - { - for (auto ch = 1; ch <= max; ++ch) - { - auto channelSets = AudioChannelSet::channelSetsWithNumberOfChannels (ch); - - for (auto set : channelSets) - expect (set.size() == ch); - } - } - - beginTest ("Ambisonics"); - { - uint64 mask = 0; - - mask |= (1ull << AudioChannelSet::ambisonicACN0); - checkAmbisonic (mask, 0, "0th Order Ambisonics"); - - mask |= (1ull << AudioChannelSet::ambisonicACN1) | (1ull << AudioChannelSet::ambisonicACN2) | (1ull << AudioChannelSet::ambisonicACN3); - checkAmbisonic (mask, 1, "1st Order Ambisonics"); - - mask |= (1ull << AudioChannelSet::ambisonicACN4) | (1ull << AudioChannelSet::ambisonicACN5) | (1ull << AudioChannelSet::ambisonicACN6) - | (1ull << AudioChannelSet::ambisonicACN7) | (1ull << AudioChannelSet::ambisonicACN8); - checkAmbisonic (mask, 2, "2nd Order Ambisonics"); - - mask |= (1ull << AudioChannelSet::ambisonicACN9) | (1ull << AudioChannelSet::ambisonicACN10) | (1ull << AudioChannelSet::ambisonicACN11) - | (1ull << AudioChannelSet::ambisonicACN12) | (1ull << AudioChannelSet::ambisonicACN13) | (1ull << AudioChannelSet::ambisonicACN14) - | (1ull << AudioChannelSet::ambisonicACN15); - checkAmbisonic (mask, 3, "3rd Order Ambisonics"); - - mask |= (1ull << AudioChannelSet::ambisonicACN16) | (1ull << AudioChannelSet::ambisonicACN17) | (1ull << AudioChannelSet::ambisonicACN18) - | (1ull << AudioChannelSet::ambisonicACN19) | (1ull << AudioChannelSet::ambisonicACN20) | (1ull << AudioChannelSet::ambisonicACN21) - | (1ull << AudioChannelSet::ambisonicACN22) | (1ull << AudioChannelSet::ambisonicACN23) | (1ull << AudioChannelSet::ambisonicACN24); - checkAmbisonic (mask, 4, "4th Order Ambisonics"); - - mask |= (1ull << AudioChannelSet::ambisonicACN25) | (1ull << AudioChannelSet::ambisonicACN26) | (1ull << AudioChannelSet::ambisonicACN27) - | (1ull << AudioChannelSet::ambisonicACN28) | (1ull << AudioChannelSet::ambisonicACN29) | (1ull << AudioChannelSet::ambisonicACN30) - | (1ull << AudioChannelSet::ambisonicACN31) | (1ull << AudioChannelSet::ambisonicACN32) | (1ull << AudioChannelSet::ambisonicACN33) - | (1ull << AudioChannelSet::ambisonicACN34) | (1ull << AudioChannelSet::ambisonicACN35); - checkAmbisonic (mask, 5, "5th Order Ambisonics"); - } - } - -private: - void checkAmbisonic (uint64 mask, int order, const char* layoutName) - { - auto expected = AudioChannelSet::ambisonic (order); - auto numChannels = expected.size(); - - expect (numChannels == BigInteger ((int64) mask).countNumberOfSetBits()); - expect (channelSetFromMask (mask) == expected); - - expect (order == expected.getAmbisonicOrder()); - expect (expected.getDescription() == layoutName); - - auto layouts = AudioChannelSet::channelSetsWithNumberOfChannels (numChannels); - expect (layouts.contains (expected)); - - for (auto layout : layouts) - expect (layout.getAmbisonicOrder() == (layout == expected ? order : -1)); - } - - static AudioChannelSet channelSetFromMask (uint64 mask) - { - Array channels; - for (int bit = 0; bit <= 62; ++bit) - if ((mask & (1ull << bit)) != 0) - channels.add (static_cast (bit)); - - return AudioChannelSet::channelSetWithChannels (channels); - } -}; - -static AudioChannelSetUnitTest audioChannelSetUnitTest; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/buffers/yup_AudioDataConverters.cpp b/modules/yup_audio_basics/buffers/yup_AudioDataConverters.cpp index ee3eb05b9..d8c70b6ee 100644 --- a/modules/yup_audio_basics/buffers/yup_AudioDataConverters.cpp +++ b/modules/yup_audio_basics/buffers/yup_AudioDataConverters.cpp @@ -501,188 +501,6 @@ void AudioDataConverters::deinterleaveSamples (const float* source, float** dest numSamples); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class AudioConversionTests final : public UnitTest -{ -public: - AudioConversionTests() - : UnitTest ("Audio data conversion", UnitTestCategories::audio) - { - } - - template - struct Test5 - { - static void test (UnitTest& unitTest, Random& r) - { - test (unitTest, false, r); - test (unitTest, true, r); - } - - YUP_BEGIN_IGNORE_WARNINGS_MSVC (6262) - - static void test (UnitTest& unitTest, bool inPlace, Random& r) - { - const int numSamples = 2048; - int32 original[(size_t) numSamples], - converted[(size_t) numSamples], - reversed[(size_t) numSamples]; - - { - AudioData::Pointer d (original); - bool clippingFailed = false; - - for (int i = 0; i < numSamples / 2; ++i) - { - d.setAsFloat (r.nextFloat() * 2.2f - 1.1f); - - if (! d.isFloatingPoint()) - clippingFailed = d.getAsFloat() > 1.0f || d.getAsFloat() < -1.0f || clippingFailed; - - ++d; - d.setAsInt32 (r.nextInt()); - ++d; - } - - unitTest.expect (! clippingFailed); - } - - // convert data from the source to dest format.. - std::unique_ptr conv (new AudioData::ConverterInstance, - AudioData::Pointer>()); - conv->convertSamples (inPlace ? reversed : converted, original, numSamples); - - // ..and back again.. - conv.reset (new AudioData::ConverterInstance, - AudioData::Pointer>()); - if (! inPlace) - zeromem (reversed, sizeof (reversed)); - - conv->convertSamples (reversed, inPlace ? reversed : converted, numSamples); - - { - int biggestDiff = 0; - AudioData::Pointer d1 (original); - AudioData::Pointer d2 (reversed); - - const int errorMargin = 2 * AudioData::Pointer::get32BitResolution() - + AudioData::Pointer::get32BitResolution(); - - for (int i = 0; i < numSamples; ++i) - { - biggestDiff = jmax (biggestDiff, std::abs (d1.getAsInt32() - d2.getAsInt32())); - ++d1; - ++d2; - } - - unitTest.expect (biggestDiff <= errorMargin); - } - } - - YUP_END_IGNORE_WARNINGS_MSVC - }; - - template - struct Test3 - { - static void test (UnitTest& unitTest, Random& r) - { - Test5::test (unitTest, r); - Test5::test (unitTest, r); - } - }; - - template - struct Test2 - { - static void test (UnitTest& unitTest, Random& r) - { - Test3::test (unitTest, r); - Test3::test (unitTest, r); - Test3::test (unitTest, r); - Test3::test (unitTest, r); - Test3::test (unitTest, r); - Test3::test (unitTest, r); - } - }; - - template - struct Test1 - { - static void test (UnitTest& unitTest, Random& r) - { - Test2::test (unitTest, r); - Test2::test (unitTest, r); - } - }; - - void runTest() override - { - auto r = getRandom(); - beginTest ("Round-trip conversion: Int8"); - Test1::test (*this, r); - beginTest ("Round-trip conversion: Int16"); - Test1::test (*this, r); - beginTest ("Round-trip conversion: Int24"); - Test1::test (*this, r); - beginTest ("Round-trip conversion: Int32"); - Test1::test (*this, r); - beginTest ("Round-trip conversion: Float32"); - Test1::test (*this, r); - - using Format = AudioData::Format; - - beginTest ("Interleaving"); - { - constexpr auto numChannels = 4; - constexpr auto numSamples = 512; - - AudioBuffer sourceBuffer { numChannels, numSamples }, - destBuffer { 1, numChannels * numSamples }; - - for (int ch = 0; ch < numChannels; ++ch) - for (int i = 0; i < numSamples; ++i) - sourceBuffer.setSample (ch, i, r.nextFloat()); - - AudioData::interleaveSamples (AudioData::NonInterleavedSource { sourceBuffer.getArrayOfReadPointers(), numChannels }, - AudioData::InterleavedDest { destBuffer.getWritePointer (0), numChannels }, - numSamples); - - for (int ch = 0; ch < numChannels; ++ch) - for (int i = 0; i < numSamples; ++i) - expectEquals (destBuffer.getSample (0, ch + (i * numChannels)), sourceBuffer.getSample (ch, i)); - } - - beginTest ("Deinterleaving"); - { - constexpr auto numChannels = 4; - constexpr auto numSamples = 512; - - AudioBuffer sourceBuffer { 1, numChannels * numSamples }, - destBuffer { numChannels, numSamples }; - - for (int ch = 0; ch < numChannels; ++ch) - for (int i = 0; i < numSamples; ++i) - sourceBuffer.setSample (0, ch + (i * numChannels), r.nextFloat()); - - AudioData::deinterleaveSamples (AudioData::InterleavedSource { sourceBuffer.getReadPointer (0), numChannels }, - AudioData::NonInterleavedDest { destBuffer.getArrayOfWritePointers(), numChannels }, - numSamples); - - for (int ch = 0; ch < numChannels; ++ch) - for (int i = 0; i < numSamples; ++i) - expectEquals (sourceBuffer.getSample (0, ch + (i * numChannels)), destBuffer.getSample (ch, i)); - } - } -}; - -static AudioConversionTests audioConversionUnitTests; - -#endif - YUP_END_IGNORE_DEPRECATION_WARNINGS } // namespace yup diff --git a/modules/yup_audio_basics/midi/ump/yup_UMP_test.cpp b/modules/yup_audio_basics/midi/ump/yup_UMP_test.cpp deleted file mode 100644 index 2a87ac70f..000000000 --- a/modules/yup_audio_basics/midi/ump/yup_UMP_test.cpp +++ /dev/null @@ -1,1059 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup::universal_midi_packets -{ - -constexpr uint8_t operator""_u8 (unsigned long long int i) { return static_cast (i); } - -constexpr uint16_t operator""_u16 (unsigned long long int i) { return static_cast (i); } - -constexpr uint32_t operator""_u32 (unsigned long long int i) { return static_cast (i); } - -constexpr uint64_t operator""_u64 (unsigned long long int i) { return static_cast (i); } - -class UniversalMidiPacketTests final : public UnitTest -{ -public: - UniversalMidiPacketTests() - : UnitTest ("Universal MIDI Packet", UnitTestCategories::midi) - { - } - - void runTest() override - { - auto random = getRandom(); - - beginTest ("Short bytestream midi messages can be round-tripped through the UMP converter"); - { - Midi1ToBytestreamTranslator translator (0); - - forEachNonSysExTestMessage (random, [&] (const MidiMessage& m) - { - const auto packets = toMidi1 (m); - expect (packets.size() == 1); - - // Make sure that the message type is correct - const auto msgType = Utils::getMessageType (packets.data()[0]); - expect (msgType == ((m.getRawData()[0] >> 0x4) == 0xf ? 0x1 : 0x2)); - - translator.dispatch (View { packets.data() }, - 0, - [&] (const BytestreamMidiView& roundTripped) - { - expect (equal (m, roundTripped.getMessage())); - }); - }); - } - - beginTest ("Bytestream SysEx converts to universal packets"); - { - { - // Zero length message - const auto packets = toMidi1 (createRandomSysEx (random, 0)); - expect (packets.size() == 2); - - expect (packets.data()[0] == 0x30000000); - expect (packets.data()[1] == 0x00000000); - } - - { - const auto message = createRandomSysEx (random, 1); - const auto packets = toMidi1 (message); - expect (packets.size() == 2); - - const auto* sysEx = message.getSysExData(); - expect (packets.data()[0] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x01 }, std::byte { sysEx[0] }, std::byte { 0 })); - expect (packets.data()[1] == 0x00000000); - } - - { - const auto message = createRandomSysEx (random, 6); - const auto packets = toMidi1 (message); - expect (packets.size() == 2); - - const auto* sysEx = message.getSysExData(); - expect (packets.data()[0] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x06 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); - expect (packets.data()[1] == Utils::bytesToWord (std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); - } - - { - const auto message = createRandomSysEx (random, 12); - const auto packets = toMidi1 (message); - expect (packets.size() == 4); - - const auto* sysEx = message.getSysExData(); - expect (packets.data()[0] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x16 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); - expect (packets.data()[1] == Utils::bytesToWord (std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); - expect (packets.data()[2] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x36 }, std::byte { sysEx[6] }, std::byte { sysEx[7] })); - expect (packets.data()[3] == Utils::bytesToWord (std::byte { sysEx[8] }, std::byte { sysEx[9] }, std::byte { sysEx[10] }, std::byte { sysEx[11] })); - } - - { - const auto message = createRandomSysEx (random, 13); - const auto packets = toMidi1 (message); - expect (packets.size() == 6); - - const auto* sysEx = message.getSysExData(); - expect (packets.data()[0] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x16 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); - expect (packets.data()[1] == Utils::bytesToWord (std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); - expect (packets.data()[2] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x26 }, std::byte { sysEx[6] }, std::byte { sysEx[7] })); - expect (packets.data()[3] == Utils::bytesToWord (std::byte { sysEx[8] }, std::byte { sysEx[9] }, std::byte { sysEx[10] }, std::byte { sysEx[11] })); - expect (packets.data()[4] == Utils::bytesToWord (std::byte { 0x30 }, std::byte { 0x31 }, std::byte { sysEx[12] }, std::byte { 0 })); - expect (packets.data()[5] == 0x00000000); - } - } - - ToBytestreamDispatcher converter (0); - Packets packets; - - const auto checkRoundTrip = [&] (const MidiBuffer& expected) - { - for (const auto meta : expected) - Conversion::toMidi1 (ump::BytestreamMidiView (meta), [&] (const auto p) - { - packets.add (p); - }); - - MidiBuffer output; - converter.dispatch (packets.data(), - packets.data() + packets.size(), - 0, - [&] (const BytestreamMidiView& roundTripped) - { - output.addEvent (roundTripped.getMessage(), int (roundTripped.timestamp)); - }); - packets.clear(); - - expect (equal (expected, output)); - }; - - beginTest ("Long SysEx bytestream midi messages can be round-tripped through the UMP converter"); - { - for (auto length : { 0, 1, 2, 3, 4, 5, 6, 7, 13, 20, 100, 1000 }) - { - MidiBuffer expected; - expected.addEvent (createRandomSysEx (random, size_t (length)), 0); - checkRoundTrip (expected); - } - } - - beginTest ("UMP SysEx7 messages interspersed with utility messages convert to bytestream"); - { - const auto sysEx = createRandomSysEx (random, 100); - const auto originalPackets = toMidi1 (sysEx); - - Packets modifiedPackets; - - const auto addRandomUtilityUMP = [&] - { - const auto newPacket = createRandomUtilityUMP (random); - modifiedPackets.add (View (newPacket.data())); - }; - - for (const auto& packet : originalPackets) - { - addRandomUtilityUMP(); - modifiedPackets.add (packet); - addRandomUtilityUMP(); - } - - MidiBuffer output; - converter.dispatch (modifiedPackets.data(), - modifiedPackets.data() + modifiedPackets.size(), - 0, - [&] (const BytestreamMidiView& roundTripped) - { - output.addEvent (roundTripped.getMessage(), int (roundTripped.timestamp)); - }); - - // All Utility messages should have been ignored - expect (output.getNumEvents() == 1); - - for (const auto meta : output) - expect (equal (meta.getMessage(), sysEx)); - } - - beginTest ("UMP SysEx7 messages interspersed with System Realtime messages convert to bytestream"); - { - const auto sysEx = createRandomSysEx (random, 200); - const auto originalPackets = toMidi1 (sysEx); - - Packets modifiedPackets; - MidiBuffer realtimeMessages; - - const auto addRandomRealtimeUMP = [&] - { - const auto newPacket = createRandomRealtimeUMP (random); - modifiedPackets.add (View (newPacket.data())); - realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); - }; - - for (const auto& packet : originalPackets) - { - addRandomRealtimeUMP(); - modifiedPackets.add (packet); - addRandomRealtimeUMP(); - } - - MidiBuffer output; - converter.dispatch (modifiedPackets.data(), - modifiedPackets.data() + modifiedPackets.size(), - 0, - [&] (const BytestreamMidiView& roundTripped) - { - output.addEvent (roundTripped.getMessage(), int (roundTripped.timestamp)); - }); - - const auto numOutputs = output.getNumEvents(); - const auto numInputs = realtimeMessages.getNumEvents(); - expect (numOutputs == numInputs + 1); - - if (numOutputs == numInputs + 1) - { - const auto isMetadataEquivalent = [] (const MidiMessageMetadata& a, - const MidiMessageMetadata& b) - { - return equal (a.getMessage(), b.getMessage()); - }; - - auto it = output.begin(); - - for (const auto meta : realtimeMessages) - { - if (! isMetadataEquivalent (*it, meta)) - { - expect (equal ((*it).getMessage(), sysEx)); - ++it; - } - - expect (isMetadataEquivalent (*it, meta)); - ++it; - } - } - } - - beginTest ("UMP SysEx7 messages interspersed with System Realtime and Utility messages convert to bytestream"); - { - const auto sysEx = createRandomSysEx (random, 300); - const auto originalPackets = toMidi1 (sysEx); - - Packets modifiedPackets; - MidiBuffer realtimeMessages; - - const auto addRandomRealtimeUMP = [&] - { - const auto newPacket = createRandomRealtimeUMP (random); - modifiedPackets.add (View (newPacket.data())); - realtimeMessages.addEvent (Midi1ToBytestreamTranslator::fromUmp (newPacket), 0); - }; - - const auto addRandomUtilityUMP = [&] - { - const auto newPacket = createRandomUtilityUMP (random); - modifiedPackets.add (View (newPacket.data())); - }; - - for (const auto& packet : originalPackets) - { - addRandomRealtimeUMP(); - addRandomUtilityUMP(); - modifiedPackets.add (packet); - addRandomRealtimeUMP(); - addRandomUtilityUMP(); - } - - MidiBuffer output; - converter.dispatch (modifiedPackets.data(), - modifiedPackets.data() + modifiedPackets.size(), - 0, - [&] (const BytestreamMidiView& roundTripped) - { - output.addEvent (roundTripped.getMessage(), int (roundTripped.timestamp)); - }); - - const auto numOutputs = output.getNumEvents(); - const auto numInputs = realtimeMessages.getNumEvents(); - expect (numOutputs == numInputs + 1); - - if (numOutputs == numInputs + 1) - { - const auto isMetadataEquivalent = [] (const MidiMessageMetadata& a, const MidiMessageMetadata& b) - { - return equal (a.getMessage(), b.getMessage()); - }; - - auto it = output.begin(); - - for (const auto meta : realtimeMessages) - { - if (! isMetadataEquivalent (*it, meta)) - { - expect (equal ((*it).getMessage(), sysEx)); - ++it; - } - - expect (isMetadataEquivalent (*it, meta)); - ++it; - } - } - } - - beginTest ("SysEx messages are terminated by non-Utility, non-Realtime messages"); - { - const auto noteOn = [&] - { - MidiBuffer b; - b.addEvent (MidiMessage::noteOn (1, uint8_t (64), uint8_t (64)), 0); - return b; - }(); - - const auto noteOnPackets = [&] - { - Packets p; - - for (const auto meta : noteOn) - Conversion::toMidi1 (ump::BytestreamMidiView (meta), [&] (const auto packet) - { - p.add (packet); - }); - - return p; - }(); - - const auto sysEx = createRandomSysEx (random, 300); - - const auto originalPackets = toMidi1 (sysEx); - - const auto modifiedPackets = [&] - { - Packets p; - - const auto insertionPoint = std::next (originalPackets.begin(), 10); - std::for_each (originalPackets.begin(), - insertionPoint, - [&] (const View& view) - { - p.add (view); - }); - - for (const auto& view : noteOnPackets) - p.add (view); - - std::for_each (insertionPoint, - originalPackets.end(), - [&] (const View& view) - { - p.add (view); - }); - - return p; - }(); - - // modifiedPackets now contains some SysEx packets interrupted by a MIDI 1 noteOn - - MidiBuffer output; - - const auto pushToOutput = [&] (const Packets& p) - { - converter.dispatch (p.data(), - p.data() + p.size(), - 0, - [&] (const BytestreamMidiView& roundTripped) - { - output.addEvent (roundTripped.getMessage(), int (roundTripped.timestamp)); - }); - }; - - pushToOutput (modifiedPackets); - - // Interrupted sysEx shouldn't be present - expect (equal (output, noteOn)); - - const auto newSysEx = createRandomSysEx (random, 300); - const auto newSysExPackets = toMidi1 (newSysEx); - - // If we push another midi event without interrupting it, - // it should get through without being modified, - // and it shouldn't be affected by the previous (interrupted) sysex. - - output.clear(); - pushToOutput (newSysExPackets); - - expect (output.getNumEvents() == 1); - - for (const auto meta : output) - expect (equal (meta.getMessage(), newSysEx)); - } - - beginTest ("Widening conversions work"); - { - // This is similar to the 'slow' example code from the MIDI 2.0 spec - const auto baselineScale = [] (uint32_t srcVal, uint32_t srcBits, uint32_t dstBits) - { - const auto scaleBits = (uint32_t) (dstBits - srcBits); - - auto bitShiftedValue = (uint32_t) (srcVal << scaleBits); - - const auto srcCenter = (uint32_t) (1 << (srcBits - 1)); - - if (srcVal <= srcCenter) - return bitShiftedValue; - - const auto repeatBits = (uint32_t) (srcBits - 1); - const auto repeatMask = (uint32_t) ((1 << repeatBits) - 1); - - auto repeatValue = (uint32_t) (srcVal & repeatMask); - - if (scaleBits > repeatBits) - repeatValue <<= scaleBits - repeatBits; - else - repeatValue >>= repeatBits - scaleBits; - - while (repeatValue != 0) - { - bitShiftedValue |= repeatValue; - repeatValue >>= repeatBits; - } - - return bitShiftedValue; - }; - - const auto baselineScale7To8 = [&] (uint8_t in) - { - return baselineScale (in, 7, 8); - }; - - const auto baselineScale7To16 = [&] (uint8_t in) - { - return baselineScale (in, 7, 16); - }; - - const auto baselineScale14To16 = [&] (uint16_t in) - { - return baselineScale (in, 14, 16); - }; - - const auto baselineScale7To32 = [&] (uint8_t in) - { - return baselineScale (in, 7, 32); - }; - - const auto baselineScale14To32 = [&] (uint16_t in) - { - return baselineScale (in, 14, 32); - }; - - for (auto i = 0; i != 100; ++i) - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals ((int64_t) Conversion::scaleTo8 (rand), - (int64_t) baselineScale7To8 (rand)); - } - - expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x00), (int64_t) 0x0000); - expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x0a), (int64_t) 0x1400); - expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x40), (int64_t) 0x8000); - expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x57), (int64_t) 0xaeba); - expectEquals ((int64_t) Conversion::scaleTo16 ((uint8_t) 0x7f), (int64_t) 0xffff); - - for (auto i = 0; i != 100; ++i) - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals ((int64_t) Conversion::scaleTo16 (rand), - (int64_t) baselineScale7To16 (rand)); - } - - for (auto i = 0; i != 100; ++i) - { - const auto rand = (uint16_t) random.nextInt (0x4000); - expectEquals ((int64_t) Conversion::scaleTo16 (rand), - (int64_t) baselineScale14To16 (rand)); - } - - for (auto i = 0; i != 100; ++i) - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals ((int64_t) Conversion::scaleTo32 (rand), - (int64_t) baselineScale7To32 (rand)); - } - - expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x0000), (int64_t) 0x00000000); - expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x2000), (int64_t) 0x80000000); - expectEquals ((int64_t) Conversion::scaleTo32 ((uint16_t) 0x3fff), (int64_t) 0xffffffff); - - for (auto i = 0; i != 100; ++i) - { - const auto rand = (uint16_t) random.nextInt (0x4000); - expectEquals ((int64_t) Conversion::scaleTo32 (rand), - (int64_t) baselineScale14To32 (rand)); - } - } - - beginTest ("Round-trip widening/narrowing conversions work"); - { - for (auto i = 0; i != 100; ++i) - { - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals (Conversion::scaleTo7 (Conversion::scaleTo8 (rand)), rand); - } - - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals (Conversion::scaleTo7 (Conversion::scaleTo16 (rand)), rand); - } - - { - const auto rand = (uint8_t) random.nextInt (0x80); - expectEquals (Conversion::scaleTo7 (Conversion::scaleTo32 (rand)), rand); - } - - { - const auto rand = (uint16_t) random.nextInt (0x4000); - expectEquals ((uint64_t) Conversion::scaleTo14 (Conversion::scaleTo16 (rand)), (uint64_t) rand); - } - - { - const auto rand = (uint16_t) random.nextInt (0x4000); - expectEquals ((uint64_t) Conversion::scaleTo14 (Conversion::scaleTo32 (rand)), (uint64_t) rand); - } - } - } - - beginTest ("MIDI 2 -> 1 note on conversions"); - { - { - Packets midi2; - midi2.add (PacketX2 { 0x41946410, 0x12345678 }); - - Packets midi1; - midi1.add (PacketX1 { 0x21946409 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - { - // If the velocity is close to 0, the output velocity should still be 1 - Packets midi2; - midi2.add (PacketX2 { 0x4295327f, 0x00345678 }); - - Packets midi1; - midi1.add (PacketX1 { 0x22953201 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - } - - beginTest ("MIDI 2 -> 1 note off conversion"); - { - Packets midi2; - midi2.add (PacketX2 { 0x448b0520, 0xfedcba98 }); - - Packets midi1; - midi1.add (PacketX1 { 0x248b057f }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - beginTest ("MIDI 2 -> 1 poly pressure conversion"); - { - Packets midi2; - midi2.add (PacketX2 { 0x49af0520, 0x80dcba98 }); - - Packets midi1; - midi1.add (PacketX1 { 0x29af0540 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - beginTest ("MIDI 2 -> 1 control change conversion"); - { - Packets midi2; - midi2.add (PacketX2 { 0x49b00520, 0x80dcba98 }); - - Packets midi1; - midi1.add (PacketX1 { 0x29b00540 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - beginTest ("MIDI 2 -> 1 channel pressure conversion"); - { - Packets midi2; - midi2.add (PacketX2 { 0x40d20520, 0x80dcba98 }); - - Packets midi1; - midi1.add (PacketX1 { 0x20d24000 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - beginTest ("MIDI 2 -> 1 nrpn rpn conversion"); - { - { - Packets midi2; - midi2.add (PacketX2 { 0x44240123, 0x456789ab }); - - Packets midi1; - midi1.add (PacketX1 { 0x24b46501 }); - midi1.add (PacketX1 { 0x24b46423 }); - midi1.add (PacketX1 { 0x24b40622 }); - midi1.add (PacketX1 { 0x24b42659 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - { - Packets midi2; - midi2.add (PacketX2 { 0x48347f7f, 0xffffffff }); - - Packets midi1; - midi1.add (PacketX1 { 0x28b4637f }); - midi1.add (PacketX1 { 0x28b4627f }); - midi1.add (PacketX1 { 0x28b4067f }); - midi1.add (PacketX1 { 0x28b4267f }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - } - - beginTest ("MIDI 2 -> 1 program change and bank select conversion"); - { - { - // If the bank valid bit is 0, just emit a program change - Packets midi2; - midi2.add (PacketX2 { 0x4cc10000, 0x70004020 }); - - Packets midi1; - midi1.add (PacketX1 { 0x2cc17000 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - { - // If the bank valid bit is 1, emit bank select control changes and a program change - Packets midi2; - midi2.add (PacketX2 { 0x4bc20001, 0x70004020 }); - - Packets midi1; - midi1.add (PacketX1 { 0x2bb20040 }); - midi1.add (PacketX1 { 0x2bb22020 }); - midi1.add (PacketX1 { 0x2bc27000 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - } - - beginTest ("MIDI 2 -> 1 pitch bend conversion"); - { - Packets midi2; - midi2.add (PacketX2 { 0x4eee0000, 0x12340000 }); - - Packets midi1; - midi1.add (PacketX1 { 0x2eee0d09 }); - - checkMidi2ToMidi1Conversion (midi2, midi1); - } - - beginTest ("MIDI 2 -> 1 messages which don't convert"); - { - const std::byte opcodes[] { std::byte { 0x0 }, - std::byte { 0x1 }, - std::byte { 0x4 }, - std::byte { 0x5 }, - std::byte { 0x6 }, - std::byte { 0xf } }; - - for (const auto opcode : opcodes) - { - Packets midi2; - midi2.add (PacketX2 { Utils::bytesToWord (std::byte { 0x40 }, std::byte { opcode << 0x4 }, std::byte { 0 }, std::byte { 0 }), 0x0 }); - checkMidi2ToMidi1Conversion (midi2, {}); - } - } - - beginTest ("MIDI 2 -> 1 messages which are passed through"); - { - const uint8_t typecodesX1[] { 0x0, 0x1, 0x2 }; - - for (const auto typecode : typecodesX1) - { - Packets p; - p.add (PacketX1 { (uint32_t) ((int64_t) typecode << 0x1c | (random.nextInt64() & 0xffffff)) }); - - checkMidi2ToMidi1Conversion (p, p); - } - - { - Packets p; - p.add (PacketX2 { (uint32_t) (0x3 << 0x1c | (random.nextInt64() & 0xffffff)), - (uint32_t) (random.nextInt64() & 0xffffffff) }); - - checkMidi2ToMidi1Conversion (p, p); - } - - { - Packets p; - p.add (PacketX4 { (uint32_t) (0x5 << 0x1c | (random.nextInt64() & 0xffffff)), - (uint32_t) (random.nextInt64() & 0xffffffff), - (uint32_t) (random.nextInt64() & 0xffffffff), - (uint32_t) (random.nextInt64() & 0xffffffff) }); - - checkMidi2ToMidi1Conversion (p, p); - } - } - - beginTest ("MIDI 2 -> 1 control changes which should be ignored"); - { - const uint8_t CCs[] { 6, 38, 98, 99, 100, 101, 0, 32 }; - - for (const auto cc : CCs) - { - Packets midi2; - midi2.add (PacketX2 { (uint32_t) (0x40b00000 | (cc << 0x8)), 0x00000000 }); - - checkMidi2ToMidi1Conversion (midi2, {}); - } - } - - beginTest ("MIDI 1 -> 2 note on conversions"); - { - { - Packets midi1; - midi1.add (PacketX1 { 0x20904040 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40904000, static_cast (Conversion::scaleTo16 (0x40_u8)) << 0x10 }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - // If velocity is 0, convert to a note-off - { - Packets midi1; - midi1.add (PacketX1 { 0x23935100 }); - - Packets midi2; - midi2.add (PacketX2 { 0x43835100, 0x0 }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - } - - beginTest ("MIDI 1 -> 2 note off conversions"); - { - Packets midi1; - midi1.add (PacketX1 { 0x21831020 }); - - Packets midi2; - midi2.add (PacketX2 { 0x41831000, static_cast (Conversion::scaleTo16 (0x20_u8)) << 0x10 }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - beginTest ("MIDI 1 -> 2 poly pressure conversions"); - { - Packets midi1; - midi1.add (PacketX1 { 0x20af7330 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40af7300, Conversion::scaleTo32 (0x30_u8) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - beginTest ("individual MIDI 1 -> 2 control changes which should be ignored"); - { - const uint8_t CCs[] { 6, 38, 98, 99, 100, 101, 0, 32 }; - - for (const auto cc : CCs) - { - Packets midi1; - midi1.add (PacketX1 { Utils::bytesToWord (std::byte { 0x20 }, std::byte { 0xb0 }, std::byte { cc }, std::byte { 0x00 }) }); - - checkMidi1ToMidi2Conversion (midi1, {}); - } - } - - beginTest ("MIDI 1 -> 2 control change conversions"); - { - // normal control change - { - Packets midi1; - midi1.add (PacketX1 { 0x29b1017f }); - - Packets midi2; - midi2.add (PacketX2 { 0x49b10100, Conversion::scaleTo32 (0x7f_u8) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - // nrpn - { - Packets midi1; - midi1.add (PacketX1 { 0x20b06301 }); - midi1.add (PacketX1 { 0x20b06223 }); - midi1.add (PacketX1 { 0x20b00645 }); - midi1.add (PacketX1 { 0x20b02667 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40300123, Conversion::scaleTo32 (static_cast ((0x45 << 7) | 0x67)) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - // rpn - { - Packets midi1; - midi1.add (PacketX1 { 0x20b06543 }); - midi1.add (PacketX1 { 0x20b06421 }); - midi1.add (PacketX1 { 0x20b00601 }); - midi1.add (PacketX1 { 0x20b02623 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40204321, Conversion::scaleTo32 (static_cast ((0x01 << 7) | 0x23)) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - } - - beginTest ("MIDI 1 -> MIDI 2 program change and bank select"); - { - Packets midi1; - // program change with bank - midi1.add (PacketX1 { 0x2bb20030 }); - midi1.add (PacketX1 { 0x2bb22010 }); - midi1.add (PacketX1 { 0x2bc24000 }); - // program change without bank (different group and channel) - midi1.add (PacketX1 { 0x20c01000 }); - - Packets midi2; - midi2.add (PacketX2 { 0x4bc20001, 0x40003010 }); - midi2.add (PacketX2 { 0x40c00000, 0x10000000 }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - beginTest ("MIDI 1 -> MIDI 2 channel pressure conversions"); - { - Packets midi1; - midi1.add (PacketX1 { 0x20df3000 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40df0000, Conversion::scaleTo32 (0x30_u8) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - - beginTest ("MIDI 1 -> MIDI 2 pitch bend conversions"); - { - Packets midi1; - midi1.add (PacketX1 { 0x20e74567 }); - - Packets midi2; - midi2.add (PacketX2 { 0x40e70000, Conversion::scaleTo32 (static_cast ((0x67 << 7) | 0x45)) }); - - checkMidi1ToMidi2Conversion (midi1, midi2); - } - } - -private: - static Packets toMidi1 (const MidiMessage& msg) - { - Packets packets; - Conversion::toMidi1 (ump::BytestreamMidiView (&msg), [&] (const auto p) - { - packets.add (p); - }); - return packets; - } - - static Packets convertMidi2ToMidi1 (const Packets& midi2) - { - Packets r; - - for (const auto& packet : midi2) - Conversion::midi2ToMidi1DefaultTranslation (packet, [&r] (const View& v) - { - r.add (v); - }); - - return r; - } - - static Packets convertMidi1ToMidi2 (const Packets& midi1) - { - Packets r; - Midi1ToMidi2DefaultTranslator translator; - - for (const auto& packet : midi1) - translator.dispatch (packet, [&r] (const View& v) - { - r.add (v); - }); - - return r; - } - - void checkBytestreamConversion (const Packets& actual, const Packets& expected) - { - expectEquals ((int) actual.size(), (int) expected.size()); - - if (actual.size() != expected.size()) - return; - - auto actualPtr = actual.data(); - - std::for_each (expected.data(), - expected.data() + expected.size(), - [&] (const uint32_t word) - { - expectEquals ((uint64_t) *actualPtr++, (uint64_t) word); - }); - } - - void checkMidi2ToMidi1Conversion (const Packets& midi2, const Packets& expected) - { - checkBytestreamConversion (convertMidi2ToMidi1 (midi2), expected); - } - - void checkMidi1ToMidi2Conversion (const Packets& midi1, const Packets& expected) - { - checkBytestreamConversion (convertMidi1ToMidi2 (midi1), expected); - } - - MidiMessage createRandomSysEx (Random& random, size_t sysExBytes) - { - std::vector data; - data.reserve (sysExBytes); - - for (size_t i = 0; i != sysExBytes; ++i) - data.push_back (uint8_t (random.nextInt (0x80))); - - return MidiMessage::createSysExMessage (data.data(), int (data.size())); - } - - PacketX1 createRandomUtilityUMP (Random& random) - { - const auto status = random.nextInt (3); - - return PacketX1 { Utils::bytesToWord (std::byte { 0 }, - std::byte (status << 0x4), - std::byte (status == 0 ? 0 : random.nextInt (0x100)), - std::byte (status == 0 ? 0 : random.nextInt (0x100))) }; - } - - PacketX1 createRandomRealtimeUMP (Random& random) - { - const auto status = [&] - { - switch (random.nextInt (6)) - { - case 0: - return std::byte { 0xf8 }; - case 1: - return std::byte { 0xfa }; - case 2: - return std::byte { 0xfb }; - case 3: - return std::byte { 0xfc }; - case 4: - return std::byte { 0xfe }; - case 5: - return std::byte { 0xff }; - } - - jassertfalse; - return std::byte { 0x00 }; - }(); - - return PacketX1 { Utils::bytesToWord (std::byte { 0x10 }, status, std::byte { 0x00 }, std::byte { 0x00 }) }; - } - - template - void forEachNonSysExTestMessage (Random& random, Fn&& fn) - { - for (uint16_t counter = 0x80; counter != 0x100; ++counter) - { - const auto firstByte = (uint8_t) counter; - - if (firstByte == 0xf0 || firstByte == 0xf7) - continue; // sysEx is tested separately - - const auto length = MidiMessage::getMessageLengthFromFirstByte (firstByte); - const auto getDataByte = [&] - { - return uint8_t (random.nextInt (256) & 0x7f); - }; - - const auto message = [&] - { - switch (length) - { - case 1: - return MidiMessage (firstByte); - case 2: - return MidiMessage (firstByte, getDataByte()); - case 3: - return MidiMessage (firstByte, getDataByte(), getDataByte()); - } - - return MidiMessage(); - }(); - - fn (message); - } - } - - static bool equal (const MidiMessage& a, const MidiMessage& b) noexcept - { - return a.getRawDataSize() == b.getRawDataSize() - && std::equal (a.getRawData(), a.getRawData() + a.getRawDataSize(), b.getRawData()); - } - - static bool equal (const MidiBuffer& a, const MidiBuffer& b) noexcept - { - return a.data == b.data; - } -}; - -static UniversalMidiPacketTests universalMidiPacketTests; - -} // namespace yup::universal_midi_packets diff --git a/modules/yup_audio_basics/midi/yup_MidiFile.cpp b/modules/yup_audio_basics/midi/yup_MidiFile.cpp index 3a25255fe..133ccb168 100644 --- a/modules/yup_audio_basics/midi/yup_MidiFile.cpp +++ b/modules/yup_audio_basics/midi/yup_MidiFile.cpp @@ -560,270 +560,4 @@ bool MidiFile::writeTrack (OutputStream& mainOut, const MidiMessageSequence& ms) return true; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct MidiFileTest final : public UnitTest -{ - MidiFileTest() - : UnitTest ("MidiFile", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("ReadTrack respects running status"); - { - const auto sequence = parseSequence ([] (OutputStream& os) - { - MidiFileHelpers::writeVariableLengthInt (os, 100); - writeBytes (os, { 0x90, 0x40, 0x40 }); - MidiFileHelpers::writeVariableLengthInt (os, 200); - writeBytes (os, { 0x40, 0x40 }); - MidiFileHelpers::writeVariableLengthInt (os, 300); - writeBytes (os, { 0xff, 0x2f, 0x00 }); - }); - - expectEquals (sequence.getNumEvents(), 3); - expect (sequence.getEventPointer (0)->message.isNoteOn()); - expect (sequence.getEventPointer (1)->message.isNoteOn()); - expect (sequence.getEventPointer (2)->message.isEndOfTrackMetaEvent()); - } - - beginTest ("ReadTrack returns available messages if input is truncated"); - { - { - const auto sequence = parseSequence ([] (OutputStream& os) - { - // Incomplete delta time - writeBytes (os, { 0xff }); - }); - - expectEquals (sequence.getNumEvents(), 0); - } - - { - const auto sequence = parseSequence ([] (OutputStream& os) - { - // Complete delta with no following event - MidiFileHelpers::writeVariableLengthInt (os, 0xffff); - }); - - expectEquals (sequence.getNumEvents(), 0); - } - - { - const auto sequence = parseSequence ([] (OutputStream& os) - { - // Complete delta with malformed following event - MidiFileHelpers::writeVariableLengthInt (os, 0xffff); - writeBytes (os, { 0x90, 0x40 }); - }); - - expectEquals (sequence.getNumEvents(), 1); - expect (sequence.getEventPointer (0)->message.isNoteOff()); - expectEquals (sequence.getEventPointer (0)->message.getNoteNumber(), 0x40); - expectEquals (sequence.getEventPointer (0)->message.getVelocity(), (uint8) 0x00); - } - } - - beginTest ("Header parsing works"); - { - { - // No data - const auto header = parseHeader ([] (OutputStream&) {}); - expect (! header.hasValue()); - } - - { - // Invalid initial byte - const auto header = parseHeader ([] (OutputStream& os) - { - writeBytes (os, { 0xff }); - }); - - expect (! header.hasValue()); - } - - { - // Type block, but no header data - const auto header = parseHeader ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd' }); - }); - - expect (! header.hasValue()); - } - - { - // We (ll-formed header, but track type is 0 and channels != 1 - const auto header = parseHeader ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 }); - }); - - expect (! header.hasValue()); - } - - { - // Well-formed header, but track type is 5 - const auto header = parseHeader ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 }); - }); - - expect (! header.hasValue()); - } - - { - // Well-formed header - const auto header = parseHeader ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 }); - }); - - expect (header.hasValue()); - - expectEquals (header->fileType, (short) 1); - expectEquals (header->numberOfTracks, (short) 16); - expectEquals (header->timeFormat, (short) 1); - expectEquals ((int) header->bytesRead, 14); - } - } - - beginTest ("Read from stream"); - { - { - // Empty input - const auto file = parseFile ([] (OutputStream&) {}); - expect (! file.hasValue()); - } - - { - // Malformed header - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd' }); - }); - - expect (! file.hasValue()); - } - - { - // Header, no channels - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 }); - }); - - expect (file.hasValue()); - expectEquals (file->getNumTracks(), 0); - } - - { - // Header, one malformed channel - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); - writeBytes (os, { 'M', 'T', 'r', '?' }); - }); - - expect (! file.hasValue()); - } - - { - // Header, one channel with malformed message - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); - writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 1, 0xff }); - }); - - expect (file.hasValue()); - expectEquals (file->getNumTracks(), 1); - expectEquals (file->getTrack (0)->getNumEvents(), 0); - } - - { - // Header, one channel with incorrect length message - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); - writeBytes (os, { 'M', 'T', 'r', 'k', 0x0f, 0, 0, 0, 0xff }); - }); - - expect (! file.hasValue()); - } - - { - // Header, one channel, all well-formed - const auto file = parseFile ([] (OutputStream& os) - { - writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); - writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 4 }); - - MidiFileHelpers::writeVariableLengthInt (os, 0x0f); - writeBytes (os, { 0x80, 0x00, 0x00 }); - }); - - expect (file.hasValue()); - expectEquals (file->getNumTracks(), 1); - - auto& track = *file->getTrack (0); - expectEquals (track.getNumEvents(), 1); - expect (track.getEventPointer (0)->message.isNoteOff()); - expectEquals (track.getEventPointer (0)->message.getTimeStamp(), (double) 0x0f); - } - } - } - - template - static MidiMessageSequence parseSequence (Fn&& fn) - { - MemoryOutputStream os; - fn (os); - - return MidiFileHelpers::readTrack (reinterpret_cast (os.getData()), - (int) os.getDataSize()); - } - - template - static Optional parseHeader (Fn&& fn) - { - MemoryOutputStream os; - fn (os); - - return MidiFileHelpers::parseMidiHeader (reinterpret_cast (os.getData()), - os.getDataSize()); - } - - template - static Optional parseFile (Fn&& fn) - { - MemoryOutputStream os; - fn (os); - - MemoryInputStream is (os.getData(), os.getDataSize(), false); - MidiFile mf; - - int fileType = 0; - - if (mf.readFrom (is, true, &fileType)) - return mf; - - return {}; - } - - static void writeBytes (OutputStream& os, const std::vector& bytes) - { - for (const auto& byte : bytes) - os.writeByte ((char) byte); - } -}; - -static MidiFileTest midiFileTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/midi/yup_MidiMessageSequence.cpp b/modules/yup_audio_basics/midi/yup_MidiMessageSequence.cpp index df07fdab3..fb9ce8a47 100644 --- a/modules/yup_audio_basics/midi/yup_MidiMessageSequence.cpp +++ b/modules/yup_audio_basics/midi/yup_MidiMessageSequence.cpp @@ -522,403 +522,4 @@ void MidiMessageSequence::createControllerUpdatesForTime (int channel, double ti parameterNumberState.sendIfNecessary (channel, time, dest); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct MidiMessageSequenceTest final : public UnitTest -{ - MidiMessageSequenceTest() - : UnitTest ("MidiMessageSequence", UnitTestCategories::midi) - { - } - - void runTest() override - { - MidiMessageSequence s; - - s.addEvent (MidiMessage::noteOn (1, 60, 0.5f).withTimeStamp (0.0)); - s.addEvent (MidiMessage::noteOff (1, 60, 0.5f).withTimeStamp (4.0)); - s.addEvent (MidiMessage::noteOn (1, 30, 0.5f).withTimeStamp (2.0)); - s.addEvent (MidiMessage::noteOff (1, 30, 0.5f).withTimeStamp (8.0)); - - beginTest ("Start & end time"); - expectEquals (s.getStartTime(), 0.0); - expectEquals (s.getEndTime(), 8.0); - expectEquals (s.getEventTime (1), 2.0); - - beginTest ("Matching note off & ons"); - s.updateMatchedPairs(); - expectEquals (s.getTimeOfMatchingKeyUp (0), 4.0); - expectEquals (s.getTimeOfMatchingKeyUp (1), 8.0); - expectEquals (s.getIndexOfMatchingKeyUp (0), 2); - expectEquals (s.getIndexOfMatchingKeyUp (1), 3); - - beginTest ("Time & indices"); - expectEquals (s.getNextIndexAtTime (0.5), 1); - expectEquals (s.getNextIndexAtTime (2.5), 2); - expectEquals (s.getNextIndexAtTime (9.0), 4); - - beginTest ("Deleting events"); - s.deleteEvent (0, true); - expectEquals (s.getNumEvents(), 2); - - beginTest ("Merging sequences"); - MidiMessageSequence s2; - s2.addEvent (MidiMessage::noteOn (2, 25, 0.5f).withTimeStamp (0.0)); - s2.addEvent (MidiMessage::noteOn (2, 40, 0.5f).withTimeStamp (1.0)); - s2.addEvent (MidiMessage::noteOff (2, 40, 0.5f).withTimeStamp (5.0)); - s2.addEvent (MidiMessage::noteOn (2, 80, 0.5f).withTimeStamp (3.0)); - s2.addEvent (MidiMessage::noteOff (2, 80, 0.5f).withTimeStamp (7.0)); - s2.addEvent (MidiMessage::noteOff (2, 25, 0.5f).withTimeStamp (9.0)); - - s.addSequence (s2, 0.0, 0.0, 8.0); // Intentionally cut off the last note off - s.updateMatchedPairs(); - - expectEquals (s.getNumEvents(), 7); - expectEquals (s.getIndexOfMatchingKeyUp (0), -1); // Truncated note, should be no note off - expectEquals (s.getTimeOfMatchingKeyUp (1), 5.0); - - struct ControlValue - { - int control, value; - }; - - struct DataEntry - { - int controllerBase, channel, parameter, value; - double time; - - std::array getControlValues() const - { - return { { { controllerBase + 1, (parameter >> 7) & 0x7f }, - { controllerBase + 0, (parameter >> 0) & 0x7f }, - { 0x06, (value >> 7) & 0x7f }, - { 0x26, (value >> 0) & 0x7f } } }; - } - - void addToSequence (MidiMessageSequence& s) const - { - for (const auto& pair : getControlValues()) - s.addEvent (MidiMessage::controllerEvent (channel, pair.control, pair.value), time); - } - - bool matches (const MidiMessage* begin, const MidiMessage* end) const - { - const auto isEqual = [this] (const ControlValue& cv, const MidiMessage& msg) - { - return exactlyEqual (msg.getTimeStamp(), time) - && msg.isController() - && msg.getChannel() == channel - && msg.getControllerNumber() == cv.control - && msg.getControllerValue() == cv.value; - }; - - const auto pairs = getControlValues(); - return std::equal (pairs.begin(), pairs.end(), begin, end, isEqual); - } - }; - - const auto addNrpn = [&] (MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0) - { - DataEntry { 0x62, channel, parameter, value, time }.addToSequence (seq); - }; - - const auto addRpn = [&] (MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0) - { - DataEntry { 0x64, channel, parameter, value, time }.addToSequence (seq); - }; - - const auto checkNrpn = [&] (const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0) - { - expect (DataEntry { 0x62, channel, parameter, value, time }.matches (begin, end)); - }; - - const auto checkRpn = [&] (const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0) - { - expect (DataEntry { 0x64, channel, parameter, value, time }.matches (begin, end)); - }; - - beginTest ("createControllerUpdatesForTime should emit (N)RPN components in the correct order"); - { - const auto channel = 1; - const auto number = 200; - const auto value = 300; - - MidiMessageSequence sequence; - addNrpn (sequence, channel, number, value); - - Array m; - sequence.createControllerUpdatesForTime (channel, 1.0, m); - - checkNrpn (m.begin(), m.end(), channel, number, value); - } - - beginTest ("createControllerUpdatesForTime ignores (N)RPNs after the final requested time"); - { - const auto channel = 2; - const auto number = 123; - const auto value = 456; - - MidiMessageSequence sequence; - addRpn (sequence, channel, number, value, 0.5); - addRpn (sequence, channel, 111, 222, 1.5); - addRpn (sequence, channel, 333, 444, 2.5); - - Array m; - sequence.createControllerUpdatesForTime (channel, 1.0, m); - - checkRpn (m.begin(), std::next (m.begin(), 4), channel, number, value, 0.5); - } - - beginTest ("createControllerUpdatesForTime should emit separate (N)RPN messages when appropriate"); - { - const auto channel = 2; - const auto numberA = 1111; - const auto valueA = 9999; - - const auto numberB = 8888; - const auto valueB = 2222; - - const auto numberC = 7777; - const auto valueC = 3333; - - const auto numberD = 6666; - const auto valueD = 4444; - - const auto time = 0.5; - - MidiMessageSequence sequence; - addRpn (sequence, channel, numberA, valueA, time); - addRpn (sequence, channel, numberB, valueB, time); - addNrpn (sequence, channel, numberC, valueC, time); - addNrpn (sequence, channel, numberD, valueD, time); - - Array m; - sequence.createControllerUpdatesForTime (channel, time * 2, m); - - checkRpn (std::next (m.begin(), 0), std::next (m.begin(), 4), channel, numberA, valueA, time); - checkRpn (std::next (m.begin(), 4), std::next (m.begin(), 8), channel, numberB, valueB, time); - checkNrpn (std::next (m.begin(), 8), std::next (m.begin(), 12), channel, numberC, valueC, time); - checkNrpn (std::next (m.begin(), 12), std::next (m.begin(), 16), channel, numberD, valueD, time); - } - - beginTest ("createControllerUpdatesForTime correctly emits (N)RPN messages on multiple channels"); - { - struct Info - { - int channel, number, value; - }; - - const Info infos[] { { 2, 1111, 9999 }, - { 8, 8888, 2222 }, - { 5, 7777, 3333 }, - { 1, 6666, 4444 } }; - - const auto time = 0.5; - - MidiMessageSequence sequence; - - for (const auto& info : infos) - addRpn (sequence, info.channel, info.number, info.value, time); - - for (const auto& info : infos) - { - Array m; - sequence.createControllerUpdatesForTime (info.channel, time * 2, m); - checkRpn (std::next (m.begin(), 0), std::next (m.begin(), 4), info.channel, info.number, info.value, time); - } - } - - const auto messagesAreEqual = [] (const MidiMessage& a, const MidiMessage& b) - { - return std::equal (a.getRawData(), a.getRawData() + a.getRawDataSize(), b.getRawData(), b.getRawData() + b.getRawDataSize()); - }; - - beginTest ("createControllerUpdatesForTime sends bank select messages when the next program is in a new bank"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - sequence.addEvent (MidiMessage::programChange (channel, 5), time); - - sequence.addEvent (MidiMessage::controllerEvent (channel, 0x00, 128), time); - sequence.addEvent (MidiMessage::controllerEvent (channel, 0x20, 64), time); - sequence.addEvent (MidiMessage::programChange (channel, 63), time); - - const Array finalEvents { MidiMessage::controllerEvent (channel, 0x00, 50), - MidiMessage::controllerEvent (channel, 0x20, 40), - MidiMessage::programChange (channel, 30) }; - - for (const auto& e : finalEvents) - sequence.addEvent (e); - - Array m; - sequence.createControllerUpdatesForTime (channel, 1.0, m); - - expect (std::equal (m.begin(), m.end(), finalEvents.begin(), finalEvents.end(), messagesAreEqual)); - } - - beginTest ("createControllerUpdatesForTime preserves all Data Increment and Data Decrement messages"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - const Array messages { MidiMessage::controllerEvent (channel, 0x60, 0), - MidiMessage::controllerEvent (channel, 0x06, 100), - MidiMessage::controllerEvent (channel, 0x26, 50), - MidiMessage::controllerEvent (channel, 0x60, 10), - MidiMessage::controllerEvent (channel, 0x61, 10), - MidiMessage::controllerEvent (channel, 0x06, 20), - MidiMessage::controllerEvent (channel, 0x26, 30), - MidiMessage::controllerEvent (channel, 0x61, 10), - MidiMessage::controllerEvent (channel, 0x61, 20) }; - - for (const auto& m : messages) - sequence.addEvent (m, time); - - Array m; - sequence.createControllerUpdatesForTime (channel, 1.0, m); - - expect (std::equal (m.begin(), m.end(), messages.begin(), messages.end(), messagesAreEqual)); - } - - beginTest ("createControllerUpdatesForTime does not emit redundant parameter number changes"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - const Array messages { MidiMessage::controllerEvent (channel, 0x65, 0), - MidiMessage::controllerEvent (channel, 0x64, 100), - MidiMessage::controllerEvent (channel, 0x63, 50), - MidiMessage::controllerEvent (channel, 0x62, 10), - MidiMessage::controllerEvent (channel, 0x06, 10) }; - - for (const auto& m : messages) - sequence.addEvent (m, time); - - Array m; - sequence.createControllerUpdatesForTime (channel, 1.0, m); - - const Array expected { MidiMessage::controllerEvent (channel, 0x63, 50), - MidiMessage::controllerEvent (channel, 0x62, 10), - MidiMessage::controllerEvent (channel, 0x06, 10) }; - - expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual)); - } - - beginTest ("createControllerUpdatesForTime sets parameter number correctly at end of sequence"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - const Array messages { MidiMessage::controllerEvent (channel, 0x65, 0), - MidiMessage::controllerEvent (channel, 0x64, 100), - MidiMessage::controllerEvent (channel, 0x63, 50), - MidiMessage::controllerEvent (channel, 0x62, 10), - MidiMessage::controllerEvent (channel, 0x06, 10), - MidiMessage::controllerEvent (channel, 0x64, 5) }; - - for (const auto& m : messages) - sequence.addEvent (m, time); - - const auto finalTime = 1.0; - - Array m; - sequence.createControllerUpdatesForTime (channel, finalTime, m); - - const Array expected { MidiMessage::controllerEvent (channel, 0x63, 50), - MidiMessage::controllerEvent (channel, 0x62, 10), - MidiMessage::controllerEvent (channel, 0x06, 10), - // Note: we should send both the MSB and LSB! - MidiMessage::controllerEvent (channel, 0x65, 0).withTimeStamp (finalTime), - MidiMessage::controllerEvent (channel, 0x64, 5).withTimeStamp (finalTime) }; - - expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual)); - } - - beginTest ("createControllerUpdatesForTime does not emit duplicate parameter number change messages"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - const Array messages { MidiMessage::controllerEvent (channel, 0x65, 1), - MidiMessage::controllerEvent (channel, 0x64, 2), - MidiMessage::controllerEvent (channel, 0x63, 3), - MidiMessage::controllerEvent (channel, 0x62, 4), - MidiMessage::controllerEvent (channel, 0x06, 10), - MidiMessage::controllerEvent (channel, 0x63, 30), - MidiMessage::controllerEvent (channel, 0x62, 40), - MidiMessage::controllerEvent (channel, 0x63, 3), - MidiMessage::controllerEvent (channel, 0x62, 4), - MidiMessage::controllerEvent (channel, 0x60, 5), - MidiMessage::controllerEvent (channel, 0x65, 10) }; - - for (const auto& m : messages) - sequence.addEvent (m, time); - - const auto finalTime = 1.0; - - Array m; - sequence.createControllerUpdatesForTime (channel, finalTime, m); - - const Array expected { MidiMessage::controllerEvent (channel, 0x63, 3), - MidiMessage::controllerEvent (channel, 0x62, 4), - MidiMessage::controllerEvent (channel, 0x06, 10), - // Parameter number is set to (30, 40) then back to (3, 4), - // so there is no need to resend it - MidiMessage::controllerEvent (channel, 0x60, 5), - // Set parameter number to final value - MidiMessage::controllerEvent (channel, 0x65, 10).withTimeStamp (finalTime), - MidiMessage::controllerEvent (channel, 0x64, 2).withTimeStamp (finalTime) }; - - expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual)); - } - - beginTest ("createControllerUpdatesForTime emits bank change messages immediately before program change"); - { - MidiMessageSequence sequence; - - const auto time = 0.0; - const auto channel = 1; - - const Array messages { MidiMessage::controllerEvent (channel, 0x00, 1), - MidiMessage::controllerEvent (channel, 0x20, 2), - MidiMessage::controllerEvent (channel, 0x65, 0), - MidiMessage::controllerEvent (channel, 0x64, 0), - MidiMessage::programChange (channel, 5) }; - - for (const auto& m : messages) - sequence.addEvent (m, time); - - const auto finalTime = 1.0; - - Array m; - sequence.createControllerUpdatesForTime (channel, finalTime, m); - - const Array expected { MidiMessage::controllerEvent (channel, 0x00, 1), - MidiMessage::controllerEvent (channel, 0x20, 2), - MidiMessage::programChange (channel, 5), - MidiMessage::controllerEvent (channel, 0x65, 0).withTimeStamp (finalTime), - MidiMessage::controllerEvent (channel, 0x64, 0).withTimeStamp (finalTime) }; - - expect (std::equal (m.begin(), m.end(), expected.begin(), expected.end(), messagesAreEqual)); - } - } -}; - -static MidiMessageSequenceTest midiMessageSequenceTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/midi/yup_MidiRPN.cpp b/modules/yup_audio_basics/midi/yup_MidiRPN.cpp index 8d167d5e8..93f1b504a 100644 --- a/modules/yup_audio_basics/midi/yup_MidiRPN.cpp +++ b/modules/yup_audio_basics/midi/yup_MidiRPN.cpp @@ -177,284 +177,4 @@ MidiBuffer MidiRPNGenerator::generate (int midiChannel, return buffer; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MidiRPNDetectorTests final : public UnitTest -{ -public: - MidiRPNDetectorTests() - : UnitTest ("MidiRPNDetector class", UnitTestCategories::midi) - { - } - - void runTest() override - { - // From the MIDI 1.0 spec: - // If 128 steps of resolution is sufficient the second byte (LSB) of the data value can be - // omitted. If both the MSB and LSB are sent initially, a subsequent fine adjustment only - // requires the sending of the LSB. The MSB does not have to be retransmitted. If a - // subsequent major adjustment is necessary the MSB must be transmitted again. When an MSB - // is received, the receiver should set its concept of the LSB to zero. - - beginTest ("Individual MSB is parsed as 7-bit"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (2, 101, 0)); - expect (! detector.tryParse (2, 100, 7)); - - auto parsed = detector.tryParse (2, 6, 42); - expect (parsed.has_value()); - - expectEquals (parsed->channel, 2); - expectEquals (parsed->parameterNumber, 7); - expectEquals (parsed->value, 42); - expect (! parsed->isNRPN); - expect (! parsed->is14BitValue); - } - - beginTest ("LSB without preceding MSB is ignored"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (2, 101, 0)); - expect (! detector.tryParse (2, 100, 7)); - expect (! detector.tryParse (2, 38, 42)); - } - - beginTest ("LSB following MSB is parsed as 14-bit"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (1, 101, 2)); - expect (! detector.tryParse (1, 100, 44)); - - expect (detector.tryParse (1, 6, 1).has_value()); - - auto lsbParsed = detector.tryParse (1, 38, 94); - expect (lsbParsed.has_value()); - - expectEquals (lsbParsed->channel, 1); - expectEquals (lsbParsed->parameterNumber, 300); - expectEquals (lsbParsed->value, 222); - expect (! lsbParsed->isNRPN); - expect (lsbParsed->is14BitValue); - } - - beginTest ("Multiple LSB following MSB re-use the MSB"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (1, 101, 2)); - expect (! detector.tryParse (1, 100, 43)); - - expect (detector.tryParse (1, 6, 1).has_value()); - - expect (detector.tryParse (1, 38, 94).has_value()); - expect (detector.tryParse (1, 38, 95).has_value()); - expect (detector.tryParse (1, 38, 96).has_value()); - - auto lsbParsed = detector.tryParse (1, 38, 97); - expect (lsbParsed.has_value()); - - expectEquals (lsbParsed->channel, 1); - expectEquals (lsbParsed->parameterNumber, 299); - expectEquals (lsbParsed->value, 225); - expect (! lsbParsed->isNRPN); - expect (lsbParsed->is14BitValue); - } - - beginTest ("Sending a new MSB resets the LSB"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (1, 101, 3)); - expect (! detector.tryParse (1, 100, 43)); - - expect (detector.tryParse (1, 6, 1).has_value()); - expect (detector.tryParse (1, 38, 94).has_value()); - - auto newMsb = detector.tryParse (1, 6, 2); - expect (newMsb.has_value()); - - expectEquals (newMsb->channel, 1); - expectEquals (newMsb->parameterNumber, 427); - expectEquals (newMsb->value, 2); - expect (! newMsb->isNRPN); - expect (! newMsb->is14BitValue); - } - - beginTest ("RPNs on multiple channels simultaneously"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (1, 100, 44)); - expect (! detector.tryParse (2, 101, 0)); - expect (! detector.tryParse (1, 101, 2)); - expect (! detector.tryParse (2, 100, 7)); - expect (detector.tryParse (1, 6, 1).has_value()); - - auto channelTwo = detector.tryParse (2, 6, 42); - expect (channelTwo.has_value()); - - expectEquals (channelTwo->channel, 2); - expectEquals (channelTwo->parameterNumber, 7); - expectEquals (channelTwo->value, 42); - expect (! channelTwo->isNRPN); - expect (! channelTwo->is14BitValue); - - auto channelOne = detector.tryParse (1, 38, 94); - expect (channelOne.has_value()); - - expectEquals (channelOne->channel, 1); - expectEquals (channelOne->parameterNumber, 300); - expectEquals (channelOne->value, 222); - expect (! channelOne->isNRPN); - expect (channelOne->is14BitValue); - } - - beginTest ("14-bit RPN with value within 7-bit range"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (16, 100, 0)); - expect (! detector.tryParse (16, 101, 0)); - expect (detector.tryParse (16, 6, 0).has_value()); - - auto parsed = detector.tryParse (16, 38, 3); - expect (parsed.has_value()); - - expectEquals (parsed->channel, 16); - expectEquals (parsed->parameterNumber, 0); - expectEquals (parsed->value, 3); - expect (! parsed->isNRPN); - expect (parsed->is14BitValue); - } - - beginTest ("invalid RPN (wrong order)"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (2, 6, 42)); - expect (! detector.tryParse (2, 101, 0)); - expect (! detector.tryParse (2, 100, 7)); - } - - beginTest ("14-bit RPN interspersed with unrelated CC messages"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (16, 3, 80)); - expect (! detector.tryParse (16, 100, 0)); - expect (! detector.tryParse (16, 4, 81)); - expect (! detector.tryParse (16, 101, 0)); - expect (! detector.tryParse (16, 5, 82)); - expect (! detector.tryParse (16, 5, 83)); - expect (detector.tryParse (16, 6, 0).has_value()); - expect (! detector.tryParse (16, 4, 84).has_value()); - expect (! detector.tryParse (16, 3, 85).has_value()); - - auto parsed = detector.tryParse (16, 38, 3); - expect (parsed.has_value()); - - expectEquals (parsed->channel, 16); - expectEquals (parsed->parameterNumber, 0); - expectEquals (parsed->value, 3); - expect (! parsed->isNRPN); - expect (parsed->is14BitValue); - } - - beginTest ("14-bit NRPN"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (1, 98, 44)); - expect (! detector.tryParse (1, 99, 2)); - expect (detector.tryParse (1, 6, 1).has_value()); - - auto parsed = detector.tryParse (1, 38, 94); - expect (parsed.has_value()); - - expectEquals (parsed->channel, 1); - expectEquals (parsed->parameterNumber, 300); - expectEquals (parsed->value, 222); - expect (parsed->isNRPN); - expect (parsed->is14BitValue); - } - - beginTest ("reset"); - { - MidiRPNDetector detector; - expect (! detector.tryParse (2, 101, 0)); - detector.reset(); - expect (! detector.tryParse (2, 100, 7)); - expect (! detector.tryParse (2, 6, 42)); - } - } -}; - -static MidiRPNDetectorTests MidiRPNDetectorUnitTests; - -//============================================================================== -class MidiRPNGeneratorTests final : public UnitTest -{ -public: - MidiRPNGeneratorTests() - : UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("generating RPN/NRPN"); - { - { - MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true); - expectContainsRPN (buffer, 1, 23, 1337, true, true); - } - { - MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false); - expectContainsRPN (buffer, 16, 101, 34, false, false); - } - { - MidiRPNMessage message = { 16, 101, 34, false, false }; - MidiBuffer buffer = MidiRPNGenerator::generate (message); - expectContainsRPN (buffer, message); - } - } - } - -private: - //============================================================================== - void expectContainsRPN (const MidiBuffer& midiBuffer, - int channel, - int parameterNumber, - int value, - bool isNRPN, - bool is14BitValue) - { - MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue }; - expectContainsRPN (midiBuffer, expected); - } - - //============================================================================== - void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected) - { - std::optional result; - MidiRPNDetector detector; - - for (const auto metadata : midiBuffer) - { - const auto midiMessage = metadata.getMessage(); - - result = detector.tryParse (midiMessage.getChannel(), - midiMessage.getControllerNumber(), - midiMessage.getControllerValue()); - } - - expect (result.has_value()); - expectEquals (result->channel, expected.channel); - expectEquals (result->parameterNumber, expected.parameterNumber); - expectEquals (result->value, expected.value); - expect (result->isNRPN == expected.isNRPN); - expect (result->is14BitValue == expected.is14BitValue); - } -}; - -static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPEInstrument.cpp b/modules/yup_audio_basics/mpe/yup_MPEInstrument.cpp index dd1ebd3ce..44e490268 100644 --- a/modules/yup_audio_basics/mpe/yup_MPEInstrument.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPEInstrument.cpp @@ -968,1513 +968,4 @@ void MPEInstrument::Listener::noteReleased ([[maybe_unused]] MPENote finishedNot void MPEInstrument::Listener::zoneLayoutChanged() {} -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MPEInstrumentTests final : public UnitTest -{ -public: - MPEInstrumentTests() - : UnitTest ("MPEInstrument class", UnitTestCategories::midi) - { - // using lower and upper MPE zones with the following layout for testing - // - // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - // * ...................| |........................ * - - testLayout.setLowerZone (5); - testLayout.setUpperZone (6); - } - - YUP_BEGIN_IGNORE_WARNINGS_MSVC (6262) - - void runTest() override - { - beginTest ("initial zone layout"); - { - MPEInstrument test; - expect (! test.getZoneLayout().getLowerZone().isActive()); - expect (! test.getZoneLayout().getUpperZone().isActive()); - } - - beginTest ("get/setZoneLayout"); - { - MPEInstrument test; - test.setZoneLayout (testLayout); - - auto newLayout = test.getZoneLayout(); - - expect (test.getZoneLayout().getLowerZone().isActive()); - expect (test.getZoneLayout().getUpperZone().isActive()); - expectEquals (newLayout.getLowerZone().getMasterChannel(), 1); - expectEquals (newLayout.getLowerZone().numMemberChannels, 5); - expectEquals (newLayout.getUpperZone().getMasterChannel(), 16); - expectEquals (newLayout.getUpperZone().numMemberChannels, 6); - } - - beginTest ("noteOn / noteOff"); - { - { - MPEInstrument test; - test.setZoneLayout (testLayout); - expectEquals (test.getNumPlayingNotes(), 0); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // note-on on unused channel - ignore - test.noteOn (7, 60, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteAddedCallCounter, 0); - - // note-on on member channel - create new note - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 1); - expectEquals (test.noteAddedCallCounter, 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - - // note-off - test.noteOff (3, 60, MPEValue::from7BitInt (33)); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteReleasedCallCounter, 1); - expectHasFinishedNote (test, 3, 60, 33); - - // note-on on master channel - create new note - test.noteOn (1, 62, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 1); - expectEquals (test.noteAddedCallCounter, 2); - expectNote (test.getNote (1, 62), 100, 0, 8192, 64, MPENote::keyDown); - - // note-off - test.noteOff (1, 62, MPEValue::from7BitInt (33)); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteReleasedCallCounter, 2); - expectHasFinishedNote (test, 1, 62, 33); - } - - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - - // note off with non-matching note number shouldn't do anything - test.noteOff (3, 61, MPEValue::from7BitInt (33)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteReleasedCallCounter, 0); - - // note off with non-matching midi channel shouldn't do anything - test.noteOff (2, 60, MPEValue::from7BitInt (33)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteReleasedCallCounter, 0); - } - - { - // can have multiple notes on the same channel - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 0, MPEValue::from7BitInt (100)); - test.noteOn (3, 1, MPEValue::from7BitInt (100)); - test.noteOn (3, 2, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 3); - expectNote (test.getNote (3, 0), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 1), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 2), 100, 0, 8192, 64, MPENote::keyDown); - } - { - // pathological case: second note-on for same note should retrigger it. - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 0, MPEValue::from7BitInt (100)); - test.noteOn (3, 0, MPEValue::from7BitInt (60)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 0), 60, 0, 8192, 64, MPENote::keyDown); - } - } - - beginTest ("noteReleased after setZoneLayout"); - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 3); - expectEquals (test.noteReleasedCallCounter, 0); - - test.setZoneLayout (testLayout); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteReleasedCallCounter, 3); - } - - beginTest ("releaseAllNotes"); - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - test.noteOn (15, 62, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 3); - - test.releaseAllNotes(); - expectEquals (test.getNumPlayingNotes(), 0); - } - - beginTest ("sustainPedal"); - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in lower zone - test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in upper zone - - // sustain pedal on per-note channel shouldn't do anything. - test.sustainPedal (3, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 0); - - // sustain pedal on non-zone channel shouldn't do anything either. - test.sustainPedal (7, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 0); - - // sustain pedal on master channel should sustain notes on _that_ zone. - test.sustainPedal (1, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 1); - - // release - test.sustainPedal (1, false); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 2); - - // should also sustain new notes added after the press - test.sustainPedal (1, true); - expectEquals (test.noteKeyStateChangedCallCounter, 3); - test.noteOn (4, 51, MPEValue::from7BitInt (100)); - expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::keyDownAndSustained); - expectEquals (test.noteKeyStateChangedCallCounter, 3); - - // ...but only if that sustain came on the master channel of that zone! - test.sustainPedal (11, true); - test.noteOn (11, 52, MPEValue::from7BitInt (100)); - expectNote (test.getNote (11, 52), 100, 0, 8192, 64, MPENote::keyDown); - test.noteOff (11, 52, MPEValue::from7BitInt (100)); - expectEquals (test.noteReleasedCallCounter, 1); - - // note-off should not turn off sustained notes inside the same zone - test.noteOff (3, 60, MPEValue::from7BitInt (100)); - test.noteOff (4, 51, MPEValue::from7BitInt (100)); - test.noteOff (10, 60, MPEValue::from7BitInt (100)); // not affected! - expectEquals (test.getNumPlayingNotes(), 2); - expectEquals (test.noteReleasedCallCounter, 2); - expectEquals (test.noteKeyStateChangedCallCounter, 5); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); - expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::sustained); - - // notes should be turned off when pedal is released - test.sustainPedal (1, false); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteReleasedCallCounter, 4); - } - - beginTest ("sostenutoPedal"); - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); // note in lower zone - test.noteOn (10, 60, MPEValue::from7BitInt (100)); // note in upper zone - - // sostenuto pedal on per-note channel shouldn't do anything. - test.sostenutoPedal (3, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 0); - - // sostenuto pedal on non-zone channel shouldn't do anything either. - test.sostenutoPedal (9, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 0); - - // sostenuto pedal on master channel should sustain notes on *that* zone. - test.sostenutoPedal (1, true); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 1); - - // release - test.sostenutoPedal (1, false); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 2); - - // should only sustain notes turned on *before* the press (difference to sustain pedal) - test.sostenutoPedal (1, true); - expectEquals (test.noteKeyStateChangedCallCounter, 3); - test.noteOn (4, 51, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 3); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDownAndSustained); - expectNote (test.getNote (4, 51), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteKeyStateChangedCallCounter, 3); - - // note-off should not turn off sustained notes inside the same zone, - // but only if they were turned on *before* the sostenuto pedal (difference to sustain pedal) - test.noteOff (3, 60, MPEValue::from7BitInt (100)); - test.noteOff (4, 51, MPEValue::from7BitInt (100)); - test.noteOff (10, 60, MPEValue::from7BitInt (100)); // not affected! - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); - expectEquals (test.noteReleasedCallCounter, 2); - expectEquals (test.noteKeyStateChangedCallCounter, 4); - - // notes should be turned off when pedal is released - test.sustainPedal (1, false); - expectEquals (test.getNumPlayingNotes(), 0); - expectEquals (test.noteReleasedCallCounter, 3); - } - - beginTest ("getMostRecentNote"); - { - MPEInstrument test; - test.setZoneLayout (testLayout); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - - { - auto note = test.getMostRecentNote (2); - expect (! note.isValid()); - } - { - auto note = test.getMostRecentNote (3); - expect (note.isValid()); - expectEquals (int (note.midiChannel), 3); - expectEquals (int (note.initialNote), 61); - } - - test.sustainPedal (1, true); - test.noteOff (3, 61, MPEValue::from7BitInt (100)); - - { - auto note = test.getMostRecentNote (3); - expect (note.isValid()); - expectEquals (int (note.midiChannel), 3); - expectEquals (int (note.initialNote), 60); - } - - test.sustainPedal (1, false); - test.noteOff (3, 60, MPEValue::from7BitInt (100)); - - { - auto note = test.getMostRecentNote (3); - expect (! note.isValid()); - } - } - - beginTest ("getMostRecentNoteOtherThan"); - { - MPENote testNote (3, 60, MPEValue::centreValue(), MPEValue::centreValue(), MPEValue::centreValue(), MPEValue::centreValue()); - - { - // case 1: the note to exclude is not the most recent one. - - MPEInstrument test; - test.setZoneLayout (testLayout); - expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); - - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - expect (test.getMostRecentNoteOtherThan (testNote).isValid()); - expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); - expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); - } - { - // case 2: the note to exclude is the most recent one. - - MPEInstrument test; - test.setZoneLayout (testLayout); - expect (! test.getMostRecentNoteOtherThan (testNote).isValid()); - - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - expect (test.getMostRecentNoteOtherThan (testNote).isValid()); - expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); - expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expect (test.getMostRecentNoteOtherThan (testNote).isValid()); - expect (test.getMostRecentNoteOtherThan (testNote).midiChannel == 4); - expect (test.getMostRecentNoteOtherThan (testNote).initialNote == 61); - } - } - - beginTest ("pressure"); - { - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 60, MPEValue::from7BitInt (100)); - test.noteOn (10, 60, MPEValue::from7BitInt (100)); - - // applying pressure on a per-note channel should modulate one note - test.pressure (3, MPEValue::from7BitInt (33)); - expectNote (test.getNote (3, 60), 100, 33, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - - // applying pressure on a master channel should modulate all notes in this zone - test.pressure (1, MPEValue::from7BitInt (44)); - expectNote (test.getNote (3, 60), 100, 44, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 44, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 3); - - // applying pressure on an unrelated channel should be ignored - test.pressure (8, MPEValue::from7BitInt (55)); - expectNote (test.getNote (3, 60), 100, 44, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 44, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 3); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // two notes on same channel - only last added should be modulated - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (66)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 66, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // edge case: two notes on same channel, one gets released, - // then the other should be modulated - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.noteOff (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (77)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 77, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // if no pressure is sent before note-on, default = 0 should be used - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // if pressure is sent before note-on, use that - test.pressure (3, MPEValue::from7BitInt (77)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 77, 8192, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // if pressure is sent before note-on, but it belonged to another note - // on the same channel that has since been turned off, use default = 0 - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (77)); - test.noteOff (3, 61, MPEValue::from7BitInt (100)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // edge case: two notes on the same channel simultaneously. the second one should use - // pressure = 0 initially but then react to additional pressure messages - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (77)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - test.pressure (3, MPEValue::from7BitInt (78)); - expectNote (test.getNote (3, 60), 100, 78, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 77, 8192, 64, MPENote::keyDown); - } - - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // master channel will use poly-aftertouch for pressure - test.noteOn (16, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (16, 60), 100, 0, 8192, 64, MPENote::keyDown); - test.aftertouch (16, 60, MPEValue::from7BitInt (27)); - expectNote (test.getNote (16, 60), 100, 27, 8192, 64, MPENote::keyDown); - - // member channels will not respond to poly-aftertouch - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.aftertouch (3, 60, MPEValue::from7BitInt (50)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - } - - beginTest ("pitchbend"); - { - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 60, MPEValue::from7BitInt (100)); - test.noteOn (10, 60, MPEValue::from7BitInt (100)); - - // applying pitchbend on a per-note channel should modulate one note - test.pitchbend (3, MPEValue::from14BitInt (1111)); - expectNote (test.getNote (3, 60), 100, 0, 1111, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - - // applying pitchbend on a master channel should be ignored for the - // value of per-note pitchbend. Tests covering master pitchbend below. - // Note: noteChanged will be called anyway for notes in that zone - // because the total pitchbend for those notes has changed - test.pitchbend (1, MPEValue::from14BitInt (2222)); - expectNote (test.getNote (3, 60), 100, 0, 1111, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 3); - - // applying pitchbend on an unrelated channel should do nothing. - test.pitchbend (8, MPEValue::from14BitInt (3333)); - expectNote (test.getNote (3, 60), 100, 0, 1111, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 3); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // two notes on same channel - only last added should be bent - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (4444)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 4444, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // edge case: two notes on same channel, one gets released, - // then the other should be bent - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.noteOff (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (5555)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 5555, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // Richard's edge case: - // - press one note - // - press sustain (careful: must be sent on master channel) - // - release first note (is still sustained!) - // - press another note (happens to be on the same MIDI channel!) - // - pitchbend that other note - // - the first note should not be bent, only the second one. - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.sustainPedal (1, true); - test.noteOff (3, 60, MPEValue::from7BitInt (64)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); - expectEquals (test.noteKeyStateChangedCallCounter, 2); - - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (6666)); - expectEquals (test.getNumPlayingNotes(), 2); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::sustained); - expectNote (test.getNote (3, 61), 100, 0, 6666, 64, MPENote::keyDownAndSustained); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // Zsolt's edge case: - // - press one note - // - modulate pitchbend or timbre - // - release the note - // - press same note again without sending a pitchbend or timbre message before the note-on - // - the note should be turned on with a default value for pitchbend/timbre, - // and *not* the last value received on channel. - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (5555)); - expectNote (test.getNote (3, 60), 100, 0, 5555, 64, MPENote::keyDown); - - test.noteOff (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - { - // applying per-note pitchbend should set the note's totalPitchbendInSemitones - // correctly depending on the per-note pitchbend range of the zone. - UnitTestInstrument test; - - MPEZoneLayout layout = testLayout; - test.setZoneLayout (layout); // default should be +/- 48 semitones - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (4096)); - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -24.0, 0.01); - - layout.setLowerZone (5, 96); - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (0)); // -max - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); - - layout.setLowerZone (5, 1); - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (16383)); // +max - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01); - - layout.setLowerZone (5, 0); // pitchbendrange = 0 --> no pitchbend at all - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (12345)); - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01); - } - { - // applying master pitchbend should set the note's totalPitchbendInSemitones - // correctly depending on the master pitchbend range of the zone. - UnitTestInstrument test; - - MPEZoneLayout layout = testLayout; - test.setZoneLayout (layout); // default should be +/- 2 semitones - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (4096)); //halfway between -max and centre - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -1.0, 0.01); - - layout.setLowerZone (5, 48, 96); - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (0)); // -max - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -96.0, 0.01); - - layout.setLowerZone (5, 48, 1); - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (16383)); // +max - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 1.0, 0.01); - - layout.setLowerZone (5, 48, 0); // pitchbendrange = 0 --> no pitchbend at all - test.setZoneLayout (layout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (12345)); - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, 0.0, 0.01); - } - { - // applying both per-note and master pitchbend simultaneously should set - // the note's totalPitchbendInSemitones to the sum of both, correctly - // weighted with the per-note and master pitchbend range, respectively. - UnitTestInstrument test; - - MPEZoneLayout layout = testLayout; - layout.setLowerZone (5, 12, 1); - test.setZoneLayout (layout); - - test.pitchbend (1, MPEValue::from14BitInt (4096)); // master pitchbend 0.5 semitones down - test.pitchbend (3, MPEValue::from14BitInt (0)); // per-note pitchbend 12 semitones down - // additionally, note should react to both pitchbend messages - // correctly even if they arrived before the note-on. - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectDoubleWithinRelativeError (test.getMostRecentNote (3).totalPitchbendInSemitones, -12.5, 0.01); - } - } - - beginTest ("timbre"); - { - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 60, MPEValue::from7BitInt (100)); - test.noteOn (10, 60, MPEValue::from7BitInt (100)); - - // modulating timbre on a per-note channel should modulate one note - test.timbre (3, MPEValue::from7BitInt (33)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 33, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - - // modulating timbre on a master channel should modulate all notes in this zone - test.timbre (1, MPEValue::from7BitInt (44)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 44, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 44, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 3); - - // modulating timbre on an unrelated channel should be ignored - test.timbre (9, MPEValue::from7BitInt (55)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 44, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 44, MPENote::keyDown); - expectNote (test.getNote (10, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 3); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // two notes on same channel - only last added should be modulated - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (66)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 66, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // edge case: two notes on same channel, one gets released, - // then the other should be modulated - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.noteOff (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (77)); - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (3, 60), 100, 0, 8192, 77, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - } - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - // Zsolt's edge case for timbre - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (42)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 42, MPENote::keyDown); - - test.noteOff (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - } - - beginTest ("setPressureTrackingMode"); - { - { - // last note played (= default) - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPressureTrackingMode (MPEInstrument::lastNotePlayedOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 99, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - } - { - // lowest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPressureTrackingMode (MPEInstrument::lowestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 99, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - } - { - // highest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPressureTrackingMode (MPEInstrument::highestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 99, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 1); - } - { - // all notes - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPressureTrackingMode (MPEInstrument::allNotesOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pressure (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 99, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 99, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 99, 8192, 64, MPENote::keyDown); - expectEquals (test.notePressureChangedCallCounter, 3); - } - } - - beginTest ("setPitchbendTrackingMode"); - { - { - // last note played (= default) - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPitchbendTrackingMode (MPEInstrument::lastNotePlayedOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 9999, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - // lowest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPitchbendTrackingMode (MPEInstrument::lowestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (3, 60), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - // highest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPitchbendTrackingMode (MPEInstrument::highestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 1); - } - { - // all notes - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setPitchbendTrackingMode (MPEInstrument::allNotesOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.pitchbend (3, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (3, 60), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 9999, 64, MPENote::keyDown); - expectEquals (test.notePitchbendChangedCallCounter, 3); - } - } - - beginTest ("setTimbreTrackingMode"); - { - { - // last note played (= default) - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setTimbreTrackingMode (MPEInstrument::lastNotePlayedOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 99, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - } - { - // lowest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setTimbreTrackingMode (MPEInstrument::lowestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 99, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - } - { - // highest note - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setTimbreTrackingMode (MPEInstrument::highestNoteOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 99, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 1); - } - { - // all notes - UnitTestInstrument test; - test.setZoneLayout (testLayout); - - test.setTimbreTrackingMode (MPEInstrument::allNotesOnChannel); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 62, MPEValue::from7BitInt (100)); - test.noteOn (3, 61, MPEValue::from7BitInt (100)); - test.timbre (3, MPEValue::from7BitInt (99)); - expectNote (test.getNote (3, 60), 100, 0, 8192, 99, MPENote::keyDown); - expectNote (test.getNote (3, 62), 100, 0, 8192, 99, MPENote::keyDown); - expectNote (test.getNote (3, 61), 100, 0, 8192, 99, MPENote::keyDown); - expectEquals (test.noteTimbreChangedCallCounter, 3); - } - } - - beginTest ("processNextMidiEvent"); - { - UnitTestInstrument test; - - // note on should trigger noteOn method call - - test.processNextMidiEvent (MidiMessage::noteOn (3, 42, uint8 (92))); - expectEquals (test.noteOnCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 3); - expectEquals (test.lastMidiNoteNumberReceived, 42); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 92); - - // note off should trigger noteOff method call - - test.processNextMidiEvent (MidiMessage::noteOff (4, 12, uint8 (33))); - expectEquals (test.noteOffCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 4); - expectEquals (test.lastMidiNoteNumberReceived, 12); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 33); - - // note on with velocity = 0 should trigger noteOff method call - // with a note off velocity of 64 (centre value) - - test.processNextMidiEvent (MidiMessage::noteOn (5, 11, uint8 (0))); - expectEquals (test.noteOffCallCounter, 2); - expectEquals (test.lastMidiChannelReceived, 5); - expectEquals (test.lastMidiNoteNumberReceived, 11); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); - - // pitchwheel message should trigger pitchbend method call - - test.processNextMidiEvent (MidiMessage::pitchWheel (1, 3333)); - expectEquals (test.pitchbendCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 1); - expectEquals (test.lastMPEValueReceived.as14BitInt(), 3333); - - // pressure using channel pressure message (7-bit value) should - // trigger pressure method call - - test.processNextMidiEvent (MidiMessage::channelPressureChange (10, 35)); - expectEquals (test.pressureCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 10); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 35); - - // pressure using 14-bit value over CC70 and CC102 should trigger - // pressure method call after the MSB is sent - - // a) sending only the MSB - test.processNextMidiEvent (MidiMessage::controllerEvent (3, 70, 120)); - expectEquals (test.pressureCallCounter, 2); - expectEquals (test.lastMidiChannelReceived, 3); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 120); - - // b) sending LSB and MSB (only the MSB should trigger the call) - per MIDI channel! - test.processNextMidiEvent (MidiMessage::controllerEvent (4, 102, 121)); - expectEquals (test.pressureCallCounter, 2); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 102, 122)); - expectEquals (test.pressureCallCounter, 2); - test.processNextMidiEvent (MidiMessage::controllerEvent (4, 70, 123)); - expectEquals (test.pressureCallCounter, 3); - expectEquals (test.lastMidiChannelReceived, 4); - expectEquals (test.lastMPEValueReceived.as14BitInt(), 121 + (123 << 7)); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 70, 124)); - expectEquals (test.pressureCallCounter, 4); - expectEquals (test.lastMidiChannelReceived, 5); - expectEquals (test.lastMPEValueReceived.as14BitInt(), 122 + (124 << 7)); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 70, 64)); - expectEquals (test.pressureCallCounter, 5); - expectEquals (test.lastMidiChannelReceived, 5); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); - - // same for timbre 14-bit value over CC74 and CC106 - test.processNextMidiEvent (MidiMessage::controllerEvent (3, 74, 120)); - expectEquals (test.timbreCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 3); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 120); - test.processNextMidiEvent (MidiMessage::controllerEvent (4, 106, 121)); - expectEquals (test.timbreCallCounter, 1); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 106, 122)); - expectEquals (test.timbreCallCounter, 1); - test.processNextMidiEvent (MidiMessage::controllerEvent (4, 74, 123)); - expectEquals (test.timbreCallCounter, 2); - expectEquals (test.lastMidiChannelReceived, 4); - expectEquals (test.lastMPEValueReceived.as14BitInt(), 121 + (123 << 7)); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 74, 124)); - expectEquals (test.timbreCallCounter, 3); - expectEquals (test.lastMidiChannelReceived, 5); - expectEquals (test.lastMPEValueReceived.as14BitInt(), 122 + (124 << 7)); - test.processNextMidiEvent (MidiMessage::controllerEvent (5, 74, 64)); - expectEquals (test.timbreCallCounter, 4); - expectEquals (test.lastMidiChannelReceived, 5); - expectEquals (test.lastMPEValueReceived.as7BitInt(), 64); - - // sustain pedal message (CC64) should trigger sustainPedal method call - test.processNextMidiEvent (MidiMessage::controllerEvent (1, 64, 127)); - expectEquals (test.sustainPedalCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 1); - expect (test.lastSustainPedalValueReceived); - test.processNextMidiEvent (MidiMessage::controllerEvent (16, 64, 0)); - expectEquals (test.sustainPedalCallCounter, 2); - expectEquals (test.lastMidiChannelReceived, 16); - expect (! test.lastSustainPedalValueReceived); - - // sostenuto pedal message (CC66) should trigger sostenutoPedal method call - test.processNextMidiEvent (MidiMessage::controllerEvent (1, 66, 127)); - expectEquals (test.sostenutoPedalCallCounter, 1); - expectEquals (test.lastMidiChannelReceived, 1); - expect (test.lastSostenutoPedalValueReceived); - test.processNextMidiEvent (MidiMessage::controllerEvent (16, 66, 0)); - expectEquals (test.sostenutoPedalCallCounter, 2); - expectEquals (test.lastMidiChannelReceived, 16); - expect (! test.lastSostenutoPedalValueReceived); - } - { - // MIDI messages modifying the zone layout should be correctly - // forwarded to the internal zone layout and modify it. - // (testing the actual logic of the zone layout is done in the - // MPEZoneLayout unit tests) - MPEInstrument test; - - MidiBuffer buffer; - buffer.addEvents (MPEMessages::setLowerZone (5), 0, -1, 0); - buffer.addEvents (MPEMessages::setUpperZone (6), 0, -1, 0); - - for (const auto metadata : buffer) - test.processNextMidiEvent (metadata.getMessage()); - - expect (test.getZoneLayout().getLowerZone().isActive()); - expect (test.getZoneLayout().getUpperZone().isActive()); - expectEquals (test.getZoneLayout().getLowerZone().getMasterChannel(), 1); - expectEquals (test.getZoneLayout().getLowerZone().numMemberChannels, 5); - expectEquals (test.getZoneLayout().getUpperZone().getMasterChannel(), 16); - expectEquals (test.getZoneLayout().getUpperZone().numMemberChannels, 6); - } - - beginTest ("MIDI all notes off"); - { - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - test.noteOn (15, 62, MPEValue::from7BitInt (100)); - test.noteOn (15, 63, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 4); - - // on note channel: ignore. - test.processNextMidiEvent (MidiMessage::allControllersOff (3)); - expectEquals (test.getNumPlayingNotes(), 4); - - // on unused channel: ignore. - test.processNextMidiEvent (MidiMessage::allControllersOff (9)); - expectEquals (test.getNumPlayingNotes(), 4); - - // on master channel: release notes in that zone only. - test.processNextMidiEvent (MidiMessage::allControllersOff (1)); - expectEquals (test.getNumPlayingNotes(), 2); - test.processNextMidiEvent (MidiMessage::allControllersOff (16)); - expectEquals (test.getNumPlayingNotes(), 0); - } - - beginTest ("MIDI all notes off (legacy mode)"); - { - UnitTestInstrument test; - test.enableLegacyMode(); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - test.noteOn (4, 61, MPEValue::from7BitInt (100)); - test.noteOn (15, 62, MPEValue::from7BitInt (100)); - test.noteOn (15, 63, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 4); - - test.processNextMidiEvent (MidiMessage::allControllersOff (3)); - expectEquals (test.getNumPlayingNotes(), 3); - - test.processNextMidiEvent (MidiMessage::allControllersOff (15)); - expectEquals (test.getNumPlayingNotes(), 1); - - test.processNextMidiEvent (MidiMessage::allControllersOff (4)); - expectEquals (test.getNumPlayingNotes(), 0); - } - - beginTest ("default initial values for pitchbend and timbre"); - { - MPEInstrument test; - test.setZoneLayout (testLayout); - - test.pitchbend (3, MPEValue::from14BitInt (3333)); // use for next note-on on ch. 3 - test.pitchbend (2, MPEValue::from14BitInt (4444)); // ignore - test.pitchbend (2, MPEValue::from14BitInt (5555)); // ignore - - test.timbre (3, MPEValue::from7BitInt (66)); // use for next note-on on ch. 3 - test.timbre (2, MPEValue::from7BitInt (77)); // ignore - test.timbre (2, MPEValue::from7BitInt (88)); // ignore - - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - - expectNote (test.getMostRecentNote (3), 100, 0, 3333, 66, MPENote::keyDown); - } - - beginTest ("Legacy mode"); - { - { - // basic check - MPEInstrument test; - expect (! test.isLegacyModeEnabled()); - - test.setZoneLayout (testLayout); - expect (! test.isLegacyModeEnabled()); - - test.enableLegacyMode(); - expect (test.isLegacyModeEnabled()); - - test.setZoneLayout (testLayout); - expect (! test.isLegacyModeEnabled()); - } - { - // constructor w/o default arguments - MPEInstrument test; - test.enableLegacyMode (0, Range (1, 11)); - expectEquals (test.getLegacyModePitchbendRange(), 0); - expect (test.getLegacyModeChannelRange() == Range (1, 11)); - } - { - // getters and setters - MPEInstrument test; - test.enableLegacyMode(); - - expectEquals (test.getLegacyModePitchbendRange(), 2); - expect (test.getLegacyModeChannelRange() == Range (1, 17)); - - test.setLegacyModePitchbendRange (96); - expectEquals (test.getLegacyModePitchbendRange(), 96); - - test.setLegacyModeChannelRange (Range (10, 12)); - expect (test.getLegacyModeChannelRange() == Range (10, 12)); - } - { - // note on should trigger notes on all 16 channels - - UnitTestInstrument test; - test.enableLegacyMode(); - - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (2, 60, MPEValue::from7BitInt (100)); - test.noteOn (15, 60, MPEValue::from7BitInt (100)); - test.noteOn (16, 60, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 4); - - // polyphonic modulation should work across all 16 channels - - test.pitchbend (1, MPEValue::from14BitInt (9999)); - test.pressure (2, MPEValue::from7BitInt (88)); - test.timbre (15, MPEValue::from7BitInt (77)); - - expectNote (test.getNote (1, 60), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (2, 60), 100, 88, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (15, 60), 100, 0, 8192, 77, MPENote::keyDown); - expectNote (test.getNote (16, 60), 100, 0, 8192, 64, MPENote::keyDown); - - // note off should work in legacy mode - - test.noteOff (15, 60, MPEValue::from7BitInt (0)); - test.noteOff (1, 60, MPEValue::from7BitInt (0)); - test.noteOff (2, 60, MPEValue::from7BitInt (0)); - test.noteOff (16, 60, MPEValue::from7BitInt (0)); - expectEquals (test.getNumPlayingNotes(), 0); - } - { - // legacy mode w/ custom channel range: note on should trigger notes only within range - - UnitTestInstrument test; - test.enableLegacyMode (2, Range (3, 8)); // channels 3-7 - - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (2, 60, MPEValue::from7BitInt (100)); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); // should trigger - test.noteOn (4, 60, MPEValue::from7BitInt (100)); // should trigger - test.noteOn (6, 60, MPEValue::from7BitInt (100)); // should trigger - test.noteOn (7, 60, MPEValue::from7BitInt (100)); // should trigger - test.noteOn (8, 60, MPEValue::from7BitInt (100)); - test.noteOn (16, 60, MPEValue::from7BitInt (100)); - - expectEquals (test.getNumPlayingNotes(), 4); - expectNote (test.getNote (3, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (4, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (6, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (7, 60), 100, 0, 8192, 64, MPENote::keyDown); - } - { - // tracking mode in legacy mode - { - UnitTestInstrument test; - test.enableLegacyMode(); - - test.setPitchbendTrackingMode (MPEInstrument::lastNotePlayedOnChannel); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (1, 62, MPEValue::from7BitInt (100)); - test.noteOn (1, 61, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (1, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (1, 61), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (1, 62), 100, 0, 8192, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.enableLegacyMode(); - - test.setPitchbendTrackingMode (MPEInstrument::lowestNoteOnChannel); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (1, 62, MPEValue::from7BitInt (100)); - test.noteOn (1, 61, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (1, 60), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (1, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (1, 62), 100, 0, 8192, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.enableLegacyMode(); - - test.setPitchbendTrackingMode (MPEInstrument::highestNoteOnChannel); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (1, 62, MPEValue::from7BitInt (100)); - test.noteOn (1, 61, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (1, 60), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (1, 61), 100, 0, 8192, 64, MPENote::keyDown); - expectNote (test.getNote (1, 62), 100, 0, 9999, 64, MPENote::keyDown); - } - { - UnitTestInstrument test; - test.enableLegacyMode(); - - test.setPitchbendTrackingMode (MPEInstrument::allNotesOnChannel); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (1, 62, MPEValue::from7BitInt (100)); - test.noteOn (1, 61, MPEValue::from7BitInt (100)); - test.pitchbend (1, MPEValue::from14BitInt (9999)); - expectNote (test.getNote (1, 60), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (1, 61), 100, 0, 9999, 64, MPENote::keyDown); - expectNote (test.getNote (1, 62), 100, 0, 9999, 64, MPENote::keyDown); - } - } - - { - // custom pitchbend range in legacy mode. - UnitTestInstrument test; - test.enableLegacyMode (11); - - test.pitchbend (1, MPEValue::from14BitInt (4096)); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - expectDoubleWithinRelativeError (test.getMostRecentNote (1).totalPitchbendInSemitones, -5.5, 0.01); - } - { - // sustain pedal should be per channel in legacy mode. - UnitTestInstrument test; - test.enableLegacyMode(); - - test.sustainPedal (1, true); - test.noteOn (2, 61, MPEValue::from7BitInt (100)); - test.noteOff (2, 61, MPEValue::from7BitInt (100)); - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.noteOff (1, 60, MPEValue::from7BitInt (100)); - - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (1, 60), 100, 0, 8192, 64, MPENote::sustained); - - test.sustainPedal (1, false); - expectEquals (test.getNumPlayingNotes(), 0); - - test.noteOn (2, 61, MPEValue::from7BitInt (100)); - test.sustainPedal (1, true); - test.noteOff (2, 61, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 0); - } - { - // sostenuto pedal should be per channel in legacy mode. - UnitTestInstrument test; - test.enableLegacyMode(); - - test.noteOn (1, 60, MPEValue::from7BitInt (100)); - test.sostenutoPedal (1, true); - test.noteOff (1, 60, MPEValue::from7BitInt (100)); - test.noteOn (2, 61, MPEValue::from7BitInt (100)); - test.noteOff (2, 61, MPEValue::from7BitInt (100)); - - expectEquals (test.getNumPlayingNotes(), 1); - expectNote (test.getNote (1, 60), 100, 0, 8192, 64, MPENote::sustained); - - test.sostenutoPedal (1, false); - expectEquals (test.getNumPlayingNotes(), 0); - - test.noteOn (2, 61, MPEValue::from7BitInt (100)); - test.sostenutoPedal (1, true); - test.noteOff (2, 61, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 0); - } - { - // all notes released when switching layout - UnitTestInstrument test; - test.setZoneLayout (testLayout); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 1); - - test.enableLegacyMode(); - expectEquals (test.getNumPlayingNotes(), 0); - test.noteOn (3, 60, MPEValue::from7BitInt (100)); - expectEquals (test.getNumPlayingNotes(), 1); - - test.setZoneLayout (testLayout); - expectEquals (test.getNumPlayingNotes(), 0); - } - } - } - - YUP_END_IGNORE_WARNINGS_MSVC - -private: - //============================================================================== - /* This mock class is used for unit testing whether the methods of - MPEInstrument are called correctly. - */ - class UnitTestInstrument final : public MPEInstrument - , private MPEInstrument::Listener - { - using Base = MPEInstrument; - - public: - UnitTestInstrument() - : noteOnCallCounter (0) - , noteOffCallCounter (0) - , pitchbendCallCounter (0) - , pressureCallCounter (0) - , timbreCallCounter (0) - , sustainPedalCallCounter (0) - , sostenutoPedalCallCounter (0) - , noteAddedCallCounter (0) - , notePressureChangedCallCounter (0) - , notePitchbendChangedCallCounter (0) - , noteTimbreChangedCallCounter (0) - , noteKeyStateChangedCallCounter (0) - , noteReleasedCallCounter (0) - , lastMidiChannelReceived (-1) - , lastMidiNoteNumberReceived (-1) - , lastSustainPedalValueReceived (false) - , lastSostenutoPedalValueReceived (false) - { - addListener (this); - } - - void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity) override - { - Base::noteOn (midiChannel, midiNoteNumber, midiNoteOnVelocity); - - noteOnCallCounter++; - lastMidiChannelReceived = midiChannel; - lastMidiNoteNumberReceived = midiNoteNumber; - lastMPEValueReceived = midiNoteOnVelocity; - } - - void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity) override - { - Base::noteOff (midiChannel, midiNoteNumber, midiNoteOffVelocity); - - noteOffCallCounter++; - lastMidiChannelReceived = midiChannel; - lastMidiNoteNumberReceived = midiNoteNumber; - lastMPEValueReceived = midiNoteOffVelocity; - } - - void pitchbend (int midiChannel, MPEValue value) override - { - Base::pitchbend (midiChannel, value); - - pitchbendCallCounter++; - lastMidiChannelReceived = midiChannel; - lastMPEValueReceived = value; - } - - void pressure (int midiChannel, MPEValue value) override - { - Base::pressure (midiChannel, value); - - pressureCallCounter++; - lastMidiChannelReceived = midiChannel; - lastMPEValueReceived = value; - } - - void timbre (int midiChannel, MPEValue value) override - { - Base::timbre (midiChannel, value); - - timbreCallCounter++; - lastMidiChannelReceived = midiChannel; - lastMPEValueReceived = value; - } - - void sustainPedal (int midiChannel, bool value) override - { - Base::sustainPedal (midiChannel, value); - - sustainPedalCallCounter++; - lastMidiChannelReceived = midiChannel; - lastSustainPedalValueReceived = value; - } - - void sostenutoPedal (int midiChannel, bool value) override - { - Base::sostenutoPedal (midiChannel, value); - - sostenutoPedalCallCounter++; - lastMidiChannelReceived = midiChannel; - lastSostenutoPedalValueReceived = value; - } - - void aftertouch (int midiChannel, int midiNoteNumber, MPEValue value) - { - const auto message = yup::MidiMessage::aftertouchChange (midiChannel, midiNoteNumber, value.as7BitInt()); - processNextMidiEvent (message); - } - - int noteOnCallCounter, noteOffCallCounter, pitchbendCallCounter, - pressureCallCounter, timbreCallCounter, sustainPedalCallCounter, - sostenutoPedalCallCounter, noteAddedCallCounter, - notePressureChangedCallCounter, notePitchbendChangedCallCounter, - noteTimbreChangedCallCounter, noteKeyStateChangedCallCounter, - noteReleasedCallCounter, lastMidiChannelReceived, lastMidiNoteNumberReceived; - - bool lastSustainPedalValueReceived, lastSostenutoPedalValueReceived; - MPEValue lastMPEValueReceived; - std::unique_ptr lastNoteFinished; - - private: - //============================================================================== - void noteAdded (MPENote) override { noteAddedCallCounter++; } - - void notePressureChanged (MPENote) override { notePressureChangedCallCounter++; } - - void notePitchbendChanged (MPENote) override { notePitchbendChangedCallCounter++; } - - void noteTimbreChanged (MPENote) override { noteTimbreChangedCallCounter++; } - - void noteKeyStateChanged (MPENote) override { noteKeyStateChangedCallCounter++; } - - void noteReleased (MPENote finishedNote) override - { - noteReleasedCallCounter++; - lastNoteFinished.reset (new MPENote (finishedNote)); - } - }; - - //============================================================================== - void expectNote (MPENote noteToTest, - int noteOnVelocity7Bit, - int pressure7Bit, - int pitchbend14Bit, - int timbre7Bit, - MPENote::KeyState keyState) - { - expect (noteToTest.isValid()); - expectEquals (noteToTest.noteOnVelocity.as7BitInt(), noteOnVelocity7Bit); - expectEquals (noteToTest.pressure.as7BitInt(), pressure7Bit); - expectEquals (noteToTest.pitchbend.as14BitInt(), pitchbend14Bit); - expectEquals (noteToTest.timbre.as7BitInt(), timbre7Bit); - expect (noteToTest.keyState == keyState); - } - - void expectHasFinishedNote (const UnitTestInstrument& test, - int channel, - int noteNumber, - int noteOffVelocity7Bit) - { - expect (test.lastNoteFinished != nullptr); - expectEquals (int (test.lastNoteFinished->midiChannel), channel); - expectEquals (int (test.lastNoteFinished->initialNote), noteNumber); - expectEquals (test.lastNoteFinished->noteOffVelocity.as7BitInt(), noteOffVelocity7Bit); - expect (test.lastNoteFinished->keyState == MPENote::off); - } - - void expectDoubleWithinRelativeError (double actual, double expected, double maxRelativeError) - { - const double maxAbsoluteError = jmax (1.0, std::abs (expected)) * maxRelativeError; - expect (std::abs (expected - actual) < maxAbsoluteError); - } - - //============================================================================== - MPEZoneLayout testLayout; -}; - -static MPEInstrumentTests MPEInstrumentUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPEMessages.cpp b/modules/yup_audio_basics/mpe/yup_MPEMessages.cpp index ec8313b09..e53026d97 100644 --- a/modules/yup_audio_basics/mpe/yup_MPEMessages.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPEMessages.cpp @@ -127,227 +127,4 @@ MidiBuffer MPEMessages::setZoneLayout (MPEZoneLayout layout) return buffer; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MPEMessagesTests final : public UnitTest -{ -public: - MPEMessagesTests() - : UnitTest ("MPEMessages class", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("add zone"); - { - { - MidiBuffer buffer = MPEMessages::setLowerZone (7); - - const uint8 expectedBytes[] = { - 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone - 0xb1, - 0x64, - 0x00, - 0xb1, - 0x65, - 0x00, - 0xb1, - 0x06, - 0x30, // per-note pbrange (default = 48) - 0xb0, - 0x64, - 0x00, - 0xb0, - 0x65, - 0x00, - 0xb0, - 0x06, - 0x02 // master pbrange (default = 2) - }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - { - MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0); - - const uint8 expectedBytes[] = { - 0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone - 0xbe, - 0x64, - 0x00, - 0xbe, - 0x65, - 0x00, - 0xbe, - 0x06, - 0x60, // per-note pbrange (custom) - 0xbf, - 0x64, - 0x00, - 0xbf, - 0x65, - 0x00, - 0xbf, - 0x06, - 0x00 // master pbrange (custom) - }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - } - - beginTest ("set per-note pitchbend range"); - { - MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96); - - const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - - beginTest ("set master pitchbend range"); - { - MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60); - - const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - - beginTest ("clear all zones"); - { - MidiBuffer buffer = MPEMessages::clearAllZones(); - - const uint8 expectedBytes[] = { - 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone - 0xbf, - 0x64, - 0x06, - 0xbf, - 0x65, - 0x00, - 0xbf, - 0x06, - 0x00 // clear upper zone - }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - - beginTest ("set complete state"); - { - MPEZoneLayout layout; - - layout.setLowerZone (7, 96, 0); - layout.setUpperZone (7); - - MidiBuffer buffer = MPEMessages::setZoneLayout (layout); - - const uint8 expectedBytes[] = { - 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone - 0xbf, - 0x64, - 0x06, - 0xbf, - 0x65, - 0x00, - 0xbf, - 0x06, - 0x00, // clear upper zone - 0xb0, - 0x64, - 0x06, - 0xb0, - 0x65, - 0x00, - 0xb0, - 0x06, - 0x07, // set lower zone - 0xb1, - 0x64, - 0x00, - 0xb1, - 0x65, - 0x00, - 0xb1, - 0x06, - 0x60, // per-note pbrange (custom) - 0xb0, - 0x64, - 0x00, - 0xb0, - 0x65, - 0x00, - 0xb0, - 0x06, - 0x00, // master pbrange (custom) - 0xbf, - 0x64, - 0x06, - 0xbf, - 0x65, - 0x00, - 0xbf, - 0x06, - 0x07, // set upper zone - 0xbe, - 0x64, - 0x00, - 0xbe, - 0x65, - 0x00, - 0xbe, - 0x06, - 0x30, // per-note pbrange (default = 48) - 0xbf, - 0x64, - 0x00, - 0xbf, - 0x65, - 0x00, - 0xbf, - 0x06, - 0x02 // master pbrange (default = 2) - }; - - testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); - } - } - -private: - //============================================================================== - void testMidiBuffer (MidiBuffer& buffer, const uint8* expectedBytes, int expectedBytesSize) - { - uint8 actualBytes[128] = { 0 }; - extractRawBinaryData (buffer, actualBytes, sizeof (actualBytes)); - - expectEquals (std::memcmp (actualBytes, expectedBytes, (std::size_t) expectedBytesSize), 0); - } - - //============================================================================== - void extractRawBinaryData (const MidiBuffer& midiBuffer, const uint8* bufferToCopyTo, std::size_t maxBytes) - { - std::size_t pos = 0; - - for (const auto metadata : midiBuffer) - { - const uint8* data = metadata.data; - std::size_t dataSize = (std::size_t) metadata.numBytes; - - if (pos + dataSize > maxBytes) - return; - - std::memcpy ((void*) (bufferToCopyTo + pos), data, dataSize); - pos += dataSize; - } - } -}; - -static MPEMessagesTests MPEMessagesUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPENote.cpp b/modules/yup_audio_basics/mpe/yup_MPENote.cpp index ae252f8de..a58c8164f 100644 --- a/modules/yup_audio_basics/mpe/yup_MPENote.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPENote.cpp @@ -101,44 +101,4 @@ bool MPENote::operator!= (const MPENote& other) const noexcept return noteID != other.noteID; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MPENoteTests final : public UnitTest -{ -public: - MPENoteTests() - : UnitTest ("MPENote class", UnitTestCategories::midi) - { - } - - //============================================================================== - void runTest() override - { - beginTest ("getFrequencyInHertz"); - { - MPENote note; - note.initialNote = 60; - note.totalPitchbendInSemitones = -0.5; - expectEqualsWithinOneCent (note.getFrequencyInHertz(), 254.178); - } - } - -private: - //============================================================================== - void expectEqualsWithinOneCent (double frequencyInHertzActual, - double frequencyInHertzExpected) - { - double ratio = frequencyInHertzActual / frequencyInHertzExpected; - double oneCent = 1.0005946; - expect (ratio < oneCent); - expect (ratio > 1.0 / oneCent); - } -}; - -static MPENoteTests MPENoteUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPESynthesiserBase.cpp b/modules/yup_audio_basics/mpe/yup_MPESynthesiserBase.cpp index 37f7ccea4..c13fc561e 100644 --- a/modules/yup_audio_basics/mpe/yup_MPESynthesiserBase.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPESynthesiserBase.cpp @@ -177,235 +177,4 @@ void MPESynthesiserBase::setMinimumRenderingSubdivisionSize (int numSamples, boo subBlockSubdivisionIsStrict = shouldBeStrict; } -#if YUP_UNIT_TESTS - -namespace -{ -class MpeSynthesiserBaseTests final : public UnitTest -{ - enum class CallbackKind - { - process, - midi - }; - - struct StartAndLength - { - StartAndLength (int s, int l) - : start (s) - , length (l) - { - } - - int start = 0; - int length = 0; - - std::tuple tie() const noexcept { return std::tie (start, length); } - - bool operator== (const StartAndLength& other) const noexcept { return tie() == other.tie(); } - - bool operator!= (const StartAndLength& other) const noexcept { return tie() != other.tie(); } - - bool operator< (const StartAndLength& other) const noexcept { return tie() < other.tie(); } - }; - - struct Events - { - std::vector blocks; - std::vector messages; - std::vector order; - }; - - class MockSynthesiser final : public MPESynthesiserBase - { - public: - Events events; - - void handleMidiEvent (const MidiMessage& m) override - { - events.messages.emplace_back (m); - events.order.emplace_back (CallbackKind::midi); - } - - private: - using MPESynthesiserBase::renderNextSubBlock; - - void renderNextSubBlock (AudioBuffer&, - int startSample, - int numSamples) override - { - events.blocks.push_back ({ startSample, numSamples }); - events.order.emplace_back (CallbackKind::process); - } - }; - - static MidiBuffer makeTestBuffer (const int bufferLength) - { - MidiBuffer result; - - for (int i = 0; i != bufferLength; ++i) - result.addEvent ({}, i); - - return result; - } - -public: - MpeSynthesiserBaseTests() - : UnitTest ("MPE Synthesiser Base", UnitTestCategories::midi) - { - } - - void runTest() override - { - const auto sumBlockLengths = [] (const std::vector& b) - { - const auto addBlock = [] (int acc, const StartAndLength& info) - { - return acc + info.length; - }; - return std::accumulate (b.begin(), b.end(), 0, addBlock); - }; - - beginTest ("Rendering sparse subblocks works"); - { - const int blockSize = 512; - const auto midi = [&] - { - MidiBuffer b; - b.addEvent ({}, blockSize / 2); - return b; - }(); - AudioBuffer audio (1, blockSize); - - const auto processEvents = [&] (int start, int length) - { - MockSynthesiser synth; - synth.setMinimumRenderingSubdivisionSize (1, false); - synth.setCurrentPlaybackSampleRate (44100); - synth.renderNextBlock (audio, midi, start, length); - return synth.events; - }; - - { - const auto e = processEvents (0, blockSize); - expect (e.blocks.size() == 2); - expect (e.messages.size() == 1); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == blockSize); - expect (e.order == std::vector { CallbackKind::process, CallbackKind::midi, CallbackKind::process }); - } - } - - beginTest ("Rendering subblocks processes only contained midi events"); - { - const int blockSize = 512; - const auto midi = makeTestBuffer (blockSize); - AudioBuffer audio (1, blockSize); - - const auto processEvents = [&] (int start, int length) - { - MockSynthesiser synth; - synth.setMinimumRenderingSubdivisionSize (1, false); - synth.setCurrentPlaybackSampleRate (44100); - synth.renderNextBlock (audio, midi, start, length); - return synth.events; - }; - - { - const int subBlockLength = 0; - const auto e = processEvents (0, subBlockLength); - expect (e.blocks.size() == 0); - expect (e.messages.size() == 0); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == subBlockLength); - } - - { - const int subBlockLength = 0; - const auto e = processEvents (1, subBlockLength); - expect (e.blocks.size() == 0); - expect (e.messages.size() == 0); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == subBlockLength); - } - - { - const int subBlockLength = 1; - const auto e = processEvents (1, subBlockLength); - expect (e.blocks.size() == 1); - expect (e.messages.size() == 1); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == subBlockLength); - expect (e.order == std::vector { CallbackKind::midi, CallbackKind::process }); - } - - { - const auto e = processEvents (0, blockSize); - expect (e.blocks.size() == blockSize); - expect (e.messages.size() == blockSize); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == blockSize); - expect (e.order.front() == CallbackKind::midi); - } - } - - beginTest ("Subblocks respect their minimum size"); - { - const int blockSize = 512; - const auto midi = makeTestBuffer (blockSize); - AudioBuffer audio (1, blockSize); - - const auto blockLengthsAreValid = [] (const std::vector& info, int minLength, bool strict) - { - if (info.size() <= 1) - return true; - - const auto lengthIsValid = [&] (const StartAndLength& s) - { - return minLength <= s.length; - }; - const auto begin = strict ? info.begin() : std::next (info.begin()); - // The final block is allowed to be shorter than the minLength - return std::all_of (begin, std::prev (info.end()), lengthIsValid); - }; - - for (auto strict : { false, true }) - { - for (auto subblockSize : { 1, 16, 32, 64, 1024 }) - { - MockSynthesiser synth; - synth.setMinimumRenderingSubdivisionSize (subblockSize, strict); - synth.setCurrentPlaybackSampleRate (44100); - synth.renderNextBlock (audio, midi, 0, blockSize); - - const auto& e = synth.events; - expectWithinAbsoluteError (float (e.blocks.size()), - std::ceil ((float) blockSize / (float) subblockSize), - 1.0f); - expect (e.messages.size() == blockSize); - expect (std::is_sorted (e.blocks.begin(), e.blocks.end())); - expect (sumBlockLengths (e.blocks) == blockSize); - expect (blockLengthsAreValid (e.blocks, subblockSize, strict)); - } - } - - { - MockSynthesiser synth; - synth.setMinimumRenderingSubdivisionSize (32, true); - synth.setCurrentPlaybackSampleRate (44100); - synth.renderNextBlock (audio, MidiBuffer {}, 0, 16); - - expect (synth.events.blocks == std::vector { { 0, 16 } }); - expect (synth.events.order == std::vector { CallbackKind::process }); - expect (synth.events.messages.empty()); - } - } - } -}; - -MpeSynthesiserBaseTests mpeSynthesiserBaseTests; -} // namespace - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPEUtils.cpp b/modules/yup_audio_basics/mpe/yup_MPEUtils.cpp index 95e4ac180..6abc57e1b 100644 --- a/modules/yup_audio_basics/mpe/yup_MPEUtils.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPEUtils.cpp @@ -296,277 +296,4 @@ void MPEChannelRemapper::zeroArrays() } } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct MPEUtilsUnitTests final : public UnitTest -{ - MPEUtilsUnitTests() - : UnitTest ("MPE Utilities", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("MPEChannelAssigner"); - { - MPEZoneLayout layout; - - // lower - { - layout.setLowerZone (15); - - // lower zone - MPEChannelAssigner channelAssigner (layout.getLowerZone()); - - // check that channels are assigned in correct order - int noteNum = 60; - for (int ch = 2; ch <= 16; ++ch) - { - expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch); - expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); - - ++noteNum; - } - - // check that note-offs are processed - channelAssigner.noteOff (60); - expectEquals (channelAssigner.findMidiChannelForNewNote (60), 2); - expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 2); - - channelAssigner.noteOff (61); - expectEquals (channelAssigner.findMidiChannelForNewNote (61), 3); - expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 3); - - // check that assigned channel was last to play note - channelAssigner.noteOff (65); - channelAssigner.noteOff (66); - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7); - - // find closest channel playing nonequal note - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2); - - // all notes off - channelAssigner.allNotesOff(); - - // last note played - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 8); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 7); - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 2); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 8); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 7); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 2); - - // normal assignment - expectEquals (channelAssigner.findMidiChannelForNewNote (101), 3); - expectEquals (channelAssigner.findMidiChannelForNewNote (20), 4); - expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 3); - expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 4); - } - - // upper - { - layout.setUpperZone (15); - - // upper zone - MPEChannelAssigner channelAssigner (layout.getUpperZone()); - - // check that channels are assigned in correct order - int noteNum = 60; - for (int ch = 15; ch >= 1; --ch) - { - expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch); - expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); - - ++noteNum; - } - - // check that note-offs are processed - channelAssigner.noteOff (60); - expectEquals (channelAssigner.findMidiChannelForNewNote (60), 15); - expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 15); - - channelAssigner.noteOff (61); - expectEquals (channelAssigner.findMidiChannelForNewNote (61), 14); - expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 14); - - // check that assigned channel was last to play note - channelAssigner.noteOff (65); - channelAssigner.noteOff (66); - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10); - - // find closest channel playing nonequal note - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15); - - // all notes off - channelAssigner.allNotesOff(); - - // last note played - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 9); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 10); - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 1); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 15); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 9); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 10); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 1); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 15); - - // normal assignment - expectEquals (channelAssigner.findMidiChannelForNewNote (101), 14); - expectEquals (channelAssigner.findMidiChannelForNewNote (20), 13); - expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 14); - expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 13); - } - - // legacy - { - MPEChannelAssigner channelAssigner; - - // check that channels are assigned in correct order - int noteNum = 60; - for (int ch = 1; ch <= 16; ++ch) - { - expectEquals (channelAssigner.findMidiChannelForNewNote (noteNum), ch); - expectEquals (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); - - ++noteNum; - } - - // check that note-offs are processed - channelAssigner.noteOff (60); - expectEquals (channelAssigner.findMidiChannelForNewNote (60), 1); - expectEquals (channelAssigner.findMidiChannelForExistingNote (60), 1); - - channelAssigner.noteOff (61); - expectEquals (channelAssigner.findMidiChannelForNewNote (61), 2); - expectEquals (channelAssigner.findMidiChannelForExistingNote (61), 2); - - // check that assigned channel was last to play note - channelAssigner.noteOff (65); - channelAssigner.noteOff (66); - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6); - - // find closest channel playing nonequal note - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1); - - // all notes off - channelAssigner.allNotesOff(); - - // last note played - expectEquals (channelAssigner.findMidiChannelForNewNote (66), 7); - expectEquals (channelAssigner.findMidiChannelForNewNote (65), 6); - expectEquals (channelAssigner.findMidiChannelForNewNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForNewNote (55), 1); - expectEquals (channelAssigner.findMidiChannelForExistingNote (66), 7); - expectEquals (channelAssigner.findMidiChannelForExistingNote (65), 6); - expectEquals (channelAssigner.findMidiChannelForExistingNote (80), 16); - expectEquals (channelAssigner.findMidiChannelForExistingNote (55), 1); - - // normal assignment - expectEquals (channelAssigner.findMidiChannelForNewNote (101), 2); - expectEquals (channelAssigner.findMidiChannelForNewNote (20), 3); - expectEquals (channelAssigner.findMidiChannelForExistingNote (101), 2); - expectEquals (channelAssigner.findMidiChannelForExistingNote (20), 3); - } - } - - beginTest ("MPEChannelRemapper"); - { - // 3 different MPE 'sources', constant IDs - const int sourceID1 = 0; - const int sourceID2 = 1; - const int sourceID3 = 2; - - MPEZoneLayout layout; - - { - layout.setLowerZone (15); - - // lower zone - MPEChannelRemapper channelRemapper (layout.getLowerZone()); - - // first source, shouldn't remap - for (int ch = 2; ch <= 16; ++ch) - { - auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f); - - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1); - expectEquals (noteOn.getChannel(), ch); - } - - auto noteOn = MidiMessage::noteOn (2, 60, 1.0f); - - // remap onto oldest last-used channel - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2); - expectEquals (noteOn.getChannel(), 2); - - // remap onto oldest last-used channel - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3); - expectEquals (noteOn.getChannel(), 3); - - // remap to correct channel for source ID - auto noteOff = MidiMessage::noteOff (2, 60, 1.0f); - channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3); - expectEquals (noteOff.getChannel(), 3); - } - - { - layout.setUpperZone (15); - - // upper zone - MPEChannelRemapper channelRemapper (layout.getUpperZone()); - - // first source, shouldn't remap - for (int ch = 15; ch >= 1; --ch) - { - auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f); - - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1); - expectEquals (noteOn.getChannel(), ch); - } - - auto noteOn = MidiMessage::noteOn (15, 60, 1.0f); - - // remap onto oldest last-used channel - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2); - expectEquals (noteOn.getChannel(), 15); - - // remap onto oldest last-used channel - channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3); - expectEquals (noteOn.getChannel(), 14); - - // remap to correct channel for source ID - auto noteOff = MidiMessage::noteOff (15, 60, 1.0f); - channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3); - expectEquals (noteOff.getChannel(), 14); - } - } - } -}; - -static MPEUtilsUnitTests MPEUtilsUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPEValue.cpp b/modules/yup_audio_basics/mpe/yup_MPEValue.cpp index a3ae50c42..da9ec05a6 100644 --- a/modules/yup_audio_basics/mpe/yup_MPEValue.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPEValue.cpp @@ -117,100 +117,4 @@ bool MPEValue::operator!= (const MPEValue& other) const noexcept return ! operator== (other); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MPEValueTests final : public UnitTest -{ -public: - MPEValueTests() - : UnitTest ("MPEValue class", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("comparison operator"); - { - MPEValue value1 = MPEValue::from7BitInt (7); - MPEValue value2 = MPEValue::from7BitInt (7); - MPEValue value3 = MPEValue::from7BitInt (8); - - expect (value1 == value1); - expect (value1 == value2); - expect (value1 != value3); - } - - beginTest ("special values"); - { - expectEquals (MPEValue::minValue().as7BitInt(), 0); - expectEquals (MPEValue::minValue().as14BitInt(), 0); - - expectEquals (MPEValue::centreValue().as7BitInt(), 64); - expectEquals (MPEValue::centreValue().as14BitInt(), 8192); - - expectEquals (MPEValue::maxValue().as7BitInt(), 127); - expectEquals (MPEValue::maxValue().as14BitInt(), 16383); - } - - beginTest ("zero/minimum value"); - { - expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f); - expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f); - expectValuesConsistent (MPEValue::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f); - expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f); - } - - beginTest ("maximum value"); - { - expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f); - expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f); - expectValuesConsistent (MPEValue::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f); - expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f); - } - - beginTest ("centre value"); - { - expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f); - expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f); - expectValuesConsistent (MPEValue::fromUnsignedFloat (0.5f), 64, 8192, 0.0f, 0.5f); - expectValuesConsistent (MPEValue::fromSignedFloat (0.0f), 64, 8192, 0.0f, 0.5f); - } - - beginTest ("value halfway between min and centre"); - { - expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f); - expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f); - expectValuesConsistent (MPEValue::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f); - expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f); - } - } - -private: - //============================================================================== - void expectValuesConsistent (MPEValue value, - int expectedValueAs7BitInt, - int expectedValueAs14BitInt, - float expectedValueAsSignedFloat, - float expectedValueAsUnsignedFloat) - { - expectEquals (value.as7BitInt(), expectedValueAs7BitInt); - expectEquals (value.as14BitInt(), expectedValueAs14BitInt); - expectFloatWithinRelativeError (value.asSignedFloat(), expectedValueAsSignedFloat, 0.0001f); - expectFloatWithinRelativeError (value.asUnsignedFloat(), expectedValueAsUnsignedFloat, 0.0001f); - } - - //============================================================================== - void expectFloatWithinRelativeError (float actualValue, float expectedValue, float maxRelativeError) - { - const float maxAbsoluteError = jmax (1.0f, std::abs (expectedValue)) * maxRelativeError; - expect (std::abs (expectedValue - actualValue) < maxAbsoluteError); - } -}; - -static MPEValueTests MPEValueUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/mpe/yup_MPEZoneLayout.cpp b/modules/yup_audio_basics/mpe/yup_MPEZoneLayout.cpp index fb98fc6bf..41039b03f 100644 --- a/modules/yup_audio_basics/mpe/yup_MPEZoneLayout.cpp +++ b/modules/yup_audio_basics/mpe/yup_MPEZoneLayout.cpp @@ -227,195 +227,4 @@ void MPEZoneLayout::checkAndLimitZoneParameters (int minValue, int maxValue, int } } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MPEZoneLayoutTests final : public UnitTest -{ -public: - MPEZoneLayoutTests() - : UnitTest ("MPEZoneLayout class", UnitTestCategories::midi) - { - } - - void runTest() override - { - beginTest ("initialisation"); - { - MPEZoneLayout layout; - expect (! layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - } - - beginTest ("adding zones"); - { - MPEZoneLayout layout; - - layout.setLowerZone (7); - - expect (layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 7); - - layout.setUpperZone (7); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 7); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 7); - - layout.setLowerZone (3); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 3); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 7); - - layout.setUpperZone (3); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 3); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 3); - - layout.setLowerZone (15); - - expect (layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 15); - } - - beginTest ("clear all zones"); - { - MPEZoneLayout layout; - - expect (! layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - - layout.setLowerZone (7); - layout.setUpperZone (2); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - - layout.clearAllZones(); - - expect (! layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - } - - beginTest ("process MIDI buffers"); - { - MPEZoneLayout layout; - MidiBuffer buffer; - - buffer = MPEMessages::setLowerZone (7); - layout.processNextMidiBuffer (buffer); - - expect (layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 7); - - buffer = MPEMessages::setUpperZone (7); - layout.processNextMidiBuffer (buffer); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 7); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 7); - - { - buffer = MPEMessages::setLowerZone (10); - layout.processNextMidiBuffer (buffer); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 10); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 4); - - buffer = MPEMessages::setLowerZone (10, 33, 44); - layout.processNextMidiBuffer (buffer); - - expectEquals (layout.getLowerZone().numMemberChannels, 10); - expectEquals (layout.getLowerZone().perNotePitchbendRange, 33); - expectEquals (layout.getLowerZone().masterPitchbendRange, 44); - } - - { - buffer = MPEMessages::setUpperZone (10); - layout.processNextMidiBuffer (buffer); - - expect (layout.getLowerZone().isActive()); - expect (layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 4); - expectEquals (layout.getUpperZone().getMasterChannel(), 16); - expectEquals (layout.getUpperZone().numMemberChannels, 10); - - buffer = MPEMessages::setUpperZone (10, 33, 44); - - layout.processNextMidiBuffer (buffer); - - expectEquals (layout.getUpperZone().numMemberChannels, 10); - expectEquals (layout.getUpperZone().perNotePitchbendRange, 33); - expectEquals (layout.getUpperZone().masterPitchbendRange, 44); - } - - buffer = MPEMessages::clearAllZones(); - layout.processNextMidiBuffer (buffer); - - expect (! layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - } - - beginTest ("process individual MIDI messages"); - { - MPEZoneLayout layout; - - layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg - layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1 - layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2 - layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg - layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3 - layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg - - expect (layout.getLowerZone().isActive()); - expect (! layout.getUpperZone().isActive()); - expectEquals (layout.getLowerZone().getMasterChannel(), 1); - expectEquals (layout.getLowerZone().numMemberChannels, 3); - expectEquals (layout.getLowerZone().perNotePitchbendRange, 48); - expectEquals (layout.getLowerZone().masterPitchbendRange, 2); - - const auto masterPitchBend = 0x0c; - layout.processNextMidiEvent ({ 0xb0, 0x64, 0x00 }); - layout.processNextMidiEvent ({ 0xb0, 0x06, masterPitchBend }); - - expectEquals (layout.getLowerZone().masterPitchbendRange, masterPitchBend); - - const auto newPitchBend = 0x0d; - layout.processNextMidiEvent ({ 0xb0, 0x06, newPitchBend }); - - expectEquals (layout.getLowerZone().masterPitchbendRange, newPitchBend); - } - } -}; - -static MPEZoneLayoutTests MPEZoneLayoutUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/sources/yup_MemoryAudioSource.cpp b/modules/yup_audio_basics/sources/yup_MemoryAudioSource.cpp index bb2d2a06a..fe57940cd 100644 --- a/modules/yup_audio_basics/sources/yup_MemoryAudioSource.cpp +++ b/modules/yup_audio_basics/sources/yup_MemoryAudioSource.cpp @@ -122,141 +122,6 @@ void MemoryAudioSource::setLooping (bool shouldLoop) } //============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct MemoryAudioSourceTests final : public UnitTest -{ - MemoryAudioSourceTests() - : UnitTest ("MemoryAudioSource", UnitTestCategories::audio) - { - } - - void runTest() override - { - constexpr int blockSize = 512; - AudioBuffer bufferToFill { 2, blockSize }; - AudioSourceChannelInfo channelInfo { bufferToFill }; - - beginTest ("A zero-length buffer produces silence, whether or not looping is enabled"); - { - for (const bool enableLooping : { false, true }) - { - AudioBuffer buffer; - MemoryAudioSource source { buffer, true, false }; - source.setLooping (enableLooping); - source.prepareToPlay (blockSize, 44100.0); - - for (int i = 0; i < 2; ++i) - { - play (source, channelInfo); - expect (isSilent (bufferToFill)); - } - } - } - - beginTest ("A short buffer without looping is played once and followed by silence"); - { - auto buffer = getShortBuffer(); - MemoryAudioSource source { buffer, true, false }; - source.setLooping (false); - source.prepareToPlay (blockSize, 44100.0); - - play (source, channelInfo); - - auto copy = buffer; - copy.setSize (buffer.getNumChannels(), blockSize, true, true, false); - - expect (bufferToFill == copy); - - play (source, channelInfo); - - expect (isSilent (bufferToFill)); - } - - beginTest ("A short buffer with looping is played multiple times"); - { - auto buffer = getShortBuffer(); - MemoryAudioSource source { buffer, true, false }; - source.setLooping (true); - source.prepareToPlay (blockSize, 44100.0); - - play (source, channelInfo); - - for (int sample = 0; sample < buffer.getNumSamples(); ++sample) - expectEquals (bufferToFill.getSample (0, sample + buffer.getNumSamples()), buffer.getSample (0, sample)); - - expect (! isSilent (bufferToFill)); - } - - beginTest ("A long buffer without looping is played once"); - { - auto buffer = getLongBuffer(); - MemoryAudioSource source { buffer, true, false }; - source.setLooping (false); - source.prepareToPlay (blockSize, 44100.0); - - play (source, channelInfo); - - auto copy = buffer; - copy.setSize (buffer.getNumChannels(), blockSize, true, true, false); - - expect (bufferToFill == copy); - - for (int i = 0; i < 10; ++i) - play (source, channelInfo); - - expect (isSilent (bufferToFill)); - } - - beginTest ("A long buffer with looping is played multiple times"); - { - auto buffer = getLongBuffer(); - MemoryAudioSource source { buffer, true, false }; - source.setLooping (true); - source.prepareToPlay (blockSize, 44100.0); - - for (int i = 0; i < 100; ++i) - { - play (source, channelInfo); - expectEquals (bufferToFill.getSample (0, 0), buffer.getSample (0, (i * blockSize) % buffer.getNumSamples())); - } - } - } - - static AudioBuffer getTestBuffer (int length) - { - AudioBuffer buffer { 2, length }; - - for (int channel = 0; channel < buffer.getNumChannels(); ++channel) - for (int sample = 0; sample < buffer.getNumSamples(); ++sample) - buffer.setSample (channel, sample, jmap ((float) sample, 0.0f, (float) length, -1.0f, 1.0f)); - - return buffer; - } - - static AudioBuffer getShortBuffer() { return getTestBuffer (5); } - - static AudioBuffer getLongBuffer() { return getTestBuffer (1000); } - - static void play (MemoryAudioSource& source, AudioSourceChannelInfo& info) - { - info.clearActiveBufferRegion(); - source.getNextAudioBlock (info); - } - - static bool isSilent (const AudioBuffer& b) - { - for (int channel = 0; channel < b.getNumChannels(); ++channel) - if (b.findMinMax (channel, 0, b.getNumSamples()) != Range {}) - return false; - - return true; - } -}; - -static MemoryAudioSourceTests memoryAudioSourceTests; -#endif } // namespace yup diff --git a/modules/yup_audio_basics/utilities/yup_ADSR_test.cpp b/modules/yup_audio_basics/utilities/yup_ADSR_test.cpp deleted file mode 100644 index f55071729..000000000 --- a/modules/yup_audio_basics/utilities/yup_ADSR_test.cpp +++ /dev/null @@ -1,277 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -struct ADSRTests final : public UnitTest -{ - ADSRTests() - : UnitTest ("ADSR", UnitTestCategories::audio) - { - } - - void runTest() override - { - constexpr double sampleRate = 44100.0; - const ADSR::Parameters parameters { 0.1f, 0.1f, 0.5f, 0.1f }; - - ADSR adsr; - adsr.setSampleRate (sampleRate); - adsr.setParameters (parameters); - - beginTest ("Idle"); - { - adsr.reset(); - - expect (! adsr.isActive()); - expectEquals (adsr.getNextSample(), 0.0f); - } - - beginTest ("Attack"); - { - adsr.reset(); - - adsr.noteOn(); - expect (adsr.isActive()); - - auto buffer = getTestBuffer (sampleRate, parameters.attack); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isIncreasing (buffer)); - } - - beginTest ("Decay"); - { - adsr.reset(); - - adsr.noteOn(); - advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); - - auto buffer = getTestBuffer (sampleRate, parameters.decay); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isDecreasing (buffer)); - } - - beginTest ("Sustain"); - { - adsr.reset(); - - adsr.noteOn(); - advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay + 0.01) * sampleRate)); - - auto random = getRandom(); - - for (int numTests = 0; numTests < 100; ++numTests) - { - const auto sustainLevel = random.nextFloat(); - const auto sustainLength = jmax (0.1f, random.nextFloat()); - - adsr.setParameters ({ parameters.attack, parameters.decay, sustainLevel, parameters.release }); - - auto buffer = getTestBuffer (sampleRate, sustainLength); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isSustained (buffer, sustainLevel)); - } - } - - beginTest ("Release"); - { - adsr.reset(); - - adsr.noteOn(); - advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); - adsr.noteOff(); - - auto buffer = getTestBuffer (sampleRate, parameters.release); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isDecreasing (buffer)); - } - - beginTest ("Zero-length attack jumps to decay"); - { - adsr.reset(); - adsr.setParameters ({ 0.0f, parameters.decay, parameters.sustain, parameters.release }); - - adsr.noteOn(); - - auto buffer = getTestBuffer (sampleRate, parameters.decay); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isDecreasing (buffer)); - } - - beginTest ("Zero-length decay jumps to sustain"); - { - adsr.reset(); - adsr.setParameters ({ parameters.attack, 0.0f, parameters.sustain, parameters.release }); - - adsr.noteOn(); - advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); - adsr.getNextSample(); - - expectEquals (adsr.getNextSample(), parameters.sustain); - - auto buffer = getTestBuffer (sampleRate, 1); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isSustained (buffer, parameters.sustain)); - } - - beginTest ("Zero-length attack and decay jumps to sustain"); - { - adsr.reset(); - adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release }); - - adsr.noteOn(); - - expectEquals (adsr.getNextSample(), parameters.sustain); - - auto buffer = getTestBuffer (sampleRate, 1); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isSustained (buffer, parameters.sustain)); - } - - beginTest ("Zero-length attack and decay releases correctly"); - { - adsr.reset(); - adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release }); - - adsr.noteOn(); - adsr.noteOff(); - - auto buffer = getTestBuffer (sampleRate, parameters.release); - adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); - - expect (isDecreasing (buffer)); - } - - beginTest ("Zero-length release resets to idle"); - { - adsr.reset(); - adsr.setParameters ({ parameters.attack, parameters.decay, parameters.sustain, 0.0f }); - - adsr.noteOn(); - advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); - adsr.noteOff(); - - expect (! adsr.isActive()); - } - } - - static void advanceADSR (ADSR& adsr, int numSamplesToAdvance) - { - while (--numSamplesToAdvance >= 0) - adsr.getNextSample(); - } - - static AudioBuffer getTestBuffer (double sampleRate, float lengthInSeconds) - { - AudioBuffer buffer { 2, roundToInt (lengthInSeconds * sampleRate) }; - - for (int channel = 0; channel < buffer.getNumChannels(); ++channel) - for (int sample = 0; sample < buffer.getNumSamples(); ++sample) - buffer.setSample (channel, sample, 1.0f); - - return buffer; - } - - static bool isIncreasing (const AudioBuffer& b) - { - jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); - - for (int channel = 0; channel < b.getNumChannels(); ++channel) - { - float previousSample = -1.0f; - - for (int sample = 0; sample < b.getNumSamples(); ++sample) - { - const auto currentSample = b.getSample (channel, sample); - - if (currentSample <= previousSample) - return false; - - previousSample = currentSample; - } - } - - return true; - } - - static bool isDecreasing (const AudioBuffer& b) - { - jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); - - for (int channel = 0; channel < b.getNumChannels(); ++channel) - { - float previousSample = std::numeric_limits::max(); - - for (int sample = 0; sample < b.getNumSamples(); ++sample) - { - const auto currentSample = b.getSample (channel, sample); - - if (currentSample >= previousSample) - return false; - - previousSample = currentSample; - } - } - - return true; - } - - static bool isSustained (const AudioBuffer& b, float sustainLevel) - { - jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); - - for (int channel = 0; channel < b.getNumChannels(); ++channel) - if (b.findMinMax (channel, 0, b.getNumSamples()) != Range { sustainLevel, sustainLevel }) - return false; - - return true; - } -}; - -static ADSRTests adsrTests; - -} // namespace yup diff --git a/modules/yup_audio_basics/utilities/yup_SmoothedValue.cpp b/modules/yup_audio_basics/utilities/yup_SmoothedValue.cpp deleted file mode 100644 index bc1f941de..000000000 --- a/modules/yup_audio_basics/utilities/yup_SmoothedValue.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -#if YUP_UNIT_TESTS - -static CommonSmoothedValueTests> commonLinearSmoothedValueTests; -static CommonSmoothedValueTests> commonMultiplicativeSmoothedValueTests; - -class SmoothedValueTests final : public UnitTest -{ -public: - SmoothedValueTests() - : UnitTest ("SmoothedValueTests", UnitTestCategories::smoothedValues) - { - } - - void runTest() override - { - beginTest ("Linear moving target"); - { - SmoothedValue sv; - - sv.reset (12); - float initialValue = 0.0f; - sv.setCurrentAndTargetValue (initialValue); - sv.setTargetValue (1.0f); - - auto delta = sv.getNextValue() - initialValue; - - sv.skip (6); - - auto newInitialValue = sv.getCurrentValue(); - sv.setTargetValue (newInitialValue + 2.0f); - auto doubleDelta = sv.getNextValue() - newInitialValue; - - expectWithinAbsoluteError (doubleDelta, delta * 2.0f, 1.0e-7f); - } - - beginTest ("Multiplicative curve"); - { - SmoothedValue sv; - - auto numSamples = 12; - AudioBuffer values (2, numSamples + 1); - - sv.reset (numSamples); - sv.setCurrentAndTargetValue (1.0); - sv.setTargetValue (2.0f); - - values.setSample (0, 0, sv.getCurrentValue()); - - for (int i = 1; i < values.getNumSamples(); ++i) - values.setSample (0, i, sv.getNextValue()); - - sv.setTargetValue (1.0f); - values.setSample (1, values.getNumSamples() - 1, sv.getCurrentValue()); - - for (int i = values.getNumSamples() - 2; i >= 0; --i) - values.setSample (1, i, sv.getNextValue()); - - for (int i = 0; i < values.getNumSamples(); ++i) - expectWithinAbsoluteError (values.getSample (0, i), values.getSample (1, i), 1.0e-9); - } - } -}; - -static SmoothedValueTests smoothedValueTests; - -#endif - -} // namespace yup diff --git a/modules/yup_audio_basics/utilities/yup_SmoothedValue.h b/modules/yup_audio_basics/utilities/yup_SmoothedValue.h index 7712653e5..d97bc6cb0 100644 --- a/modules/yup_audio_basics/utilities/yup_SmoothedValue.h +++ b/modules/yup_audio_basics/utilities/yup_SmoothedValue.h @@ -400,225 +400,4 @@ class SmoothedValue : public SmoothedValueBase using LinearSmoothedValue = SmoothedValue; -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -template -class CommonSmoothedValueTests : public UnitTest -{ -public: - CommonSmoothedValueTests() - : UnitTest ("CommonSmoothedValueTests", UnitTestCategories::smoothedValues) - { - } - - void runTest() override - { - beginTest ("Initial state"); - { - SmoothedValueType sv; - - auto value = sv.getCurrentValue(); - expectEquals (sv.getTargetValue(), value); - - sv.getNextValue(); - expectEquals (sv.getCurrentValue(), value); - expect (! sv.isSmoothing()); - } - - beginTest ("Resetting"); - { - auto initialValue = 15.0f; - - SmoothedValueType sv (initialValue); - sv.reset (3); - expectEquals (sv.getCurrentValue(), initialValue); - - auto targetValue = initialValue + 1.0f; - sv.setTargetValue (targetValue); - expectEquals (sv.getTargetValue(), targetValue); - expectEquals (sv.getCurrentValue(), initialValue); - expect (sv.isSmoothing()); - - auto currentValue = sv.getNextValue(); - expect (currentValue > initialValue); - expectEquals (sv.getCurrentValue(), currentValue); - expectEquals (sv.getTargetValue(), targetValue); - expect (sv.isSmoothing()); - - sv.reset (5); - - expectEquals (sv.getCurrentValue(), targetValue); - expectEquals (sv.getTargetValue(), targetValue); - expect (! sv.isSmoothing()); - - sv.getNextValue(); - expectEquals (sv.getCurrentValue(), targetValue); - - sv.setTargetValue (1.5f); - sv.getNextValue(); - - float newStart = 0.2f; - sv.setCurrentAndTargetValue (newStart); - expectEquals (sv.getNextValue(), newStart); - expectEquals (sv.getTargetValue(), newStart); - expectEquals (sv.getCurrentValue(), newStart); - expect (! sv.isSmoothing()); - } - - beginTest ("Sample rate"); - { - SmoothedValueType svSamples { 3.0f }; - auto svTime = svSamples; - - auto numSamples = 12; - - svSamples.reset (numSamples); - svTime.reset (numSamples * 2, 1.0); - - for (int i = 0; i < numSamples; ++i) - { - svTime.skip (1); - expectWithinAbsoluteError (svSamples.getNextValue(), - svTime.getNextValue(), - 1.0e-7f); - } - } - - beginTest ("Block processing"); - { - SmoothedValueType sv (1.0f); - - sv.reset (12); - sv.setTargetValue (2.0f); - - const auto numSamples = 15; - - AudioBuffer referenceData (1, numSamples); - - for (int i = 0; i < numSamples; ++i) - referenceData.setSample (0, i, sv.getNextValue()); - - expect (referenceData.getSample (0, 0) > 0); - expect (referenceData.getSample (0, 10) < sv.getTargetValue()); - expectWithinAbsoluteError (referenceData.getSample (0, 11), - sv.getTargetValue(), - 2.0e-7f); - - auto getUnitData = [] (int numSamplesToGenerate) - { - AudioBuffer result (1, numSamplesToGenerate); - - for (int i = 0; i < numSamplesToGenerate; ++i) - result.setSample (0, i, 1.0f); - - return result; - }; - - auto compareData = [this] (const AudioBuffer& test, - const AudioBuffer& reference) - { - for (int i = 0; i < test.getNumSamples(); ++i) - expectWithinAbsoluteError (test.getSample (0, i), - reference.getSample (0, i), - 2.0e-7f); - }; - - auto testData = getUnitData (numSamples); - sv.setCurrentAndTargetValue (1.0f); - sv.setTargetValue (2.0f); - sv.applyGain (testData.getWritePointer (0), numSamples); - compareData (testData, referenceData); - - testData = getUnitData (numSamples); - AudioBuffer destData (1, numSamples); - sv.setCurrentAndTargetValue (1.0f); - sv.setTargetValue (2.0f); - sv.applyGain (destData.getWritePointer (0), - testData.getReadPointer (0), - numSamples); - compareData (destData, referenceData); - compareData (testData, getUnitData (numSamples)); - - testData = getUnitData (numSamples); - sv.setCurrentAndTargetValue (1.0f); - sv.setTargetValue (2.0f); - sv.applyGain (testData, numSamples); - compareData (testData, referenceData); - } - - beginTest ("Skip"); - { - SmoothedValueType sv; - - sv.reset (12); - sv.setCurrentAndTargetValue (1.0f); - sv.setTargetValue (2.0f); - - Array reference; - - for (int i = 0; i < 15; ++i) - reference.add (sv.getNextValue()); - - sv.setCurrentAndTargetValue (1.0f); - sv.setTargetValue (2.0f); - - expectWithinAbsoluteError (sv.skip (1), reference[0], 1.0e-6f); - expectWithinAbsoluteError (sv.skip (1), reference[1], 1.0e-6f); - expectWithinAbsoluteError (sv.skip (2), reference[3], 1.0e-6f); - sv.skip (3); - expectWithinAbsoluteError (sv.getCurrentValue(), reference[6], 1.0e-6f); - expectEquals (sv.skip (300), sv.getTargetValue()); - expectEquals (sv.getCurrentValue(), sv.getTargetValue()); - } - - beginTest ("Negative"); - { - SmoothedValueType sv; - - auto numValues = 12; - sv.reset (numValues); - - std::vector> ranges = { { -1.0f, -2.0f }, - { -100.0f, -3.0f } }; - - for (auto range : ranges) - { - auto start = range.first, end = range.second; - - sv.setCurrentAndTargetValue (start); - sv.setTargetValue (end); - - auto val = sv.skip (numValues / 2); - - if (end > start) - expect (val > start && val < end); - else - expect (val < start && val > end); - - auto nextVal = sv.getNextValue(); - expect (end > start ? (nextVal > val) : (nextVal < val)); - - auto endVal = sv.skip (500); - expectEquals (endVal, end); - expectEquals (sv.getNextValue(), end); - expectEquals (sv.getCurrentValue(), end); - - sv.setCurrentAndTargetValue (start); - sv.setTargetValue (end); - - SmoothedValueType positiveSv { -start }; - positiveSv.reset (numValues); - positiveSv.setTargetValue (-end); - - for (int i = 0; i < numValues + 2; ++i) - expectEquals (sv.getNextValue(), -positiveSv.getNextValue()); - } - } - } -}; - -#endif - } // namespace yup diff --git a/modules/yup_audio_basics/yup_audio_basics.cpp b/modules/yup_audio_basics/yup_audio_basics.cpp index e61382f3d..07c436a43 100644 --- a/modules/yup_audio_basics/yup_audio_basics.cpp +++ b/modules/yup_audio_basics/yup_audio_basics.cpp @@ -78,8 +78,6 @@ #include "utilities/yup_IIRFilter.cpp" #include "utilities/yup_LagrangeInterpolator.cpp" #include "utilities/yup_WindowedSincInterpolator.cpp" -#include "utilities/yup_Interpolators.cpp" -#include "utilities/yup_SmoothedValue.cpp" #include "midi/yup_MidiBuffer.cpp" #include "midi/yup_MidiFile.cpp" #include "midi/yup_MidiKeyboardState.cpp" @@ -114,8 +112,3 @@ #include "midi/ump/yup_UMPMidi1ToMidi2DefaultTranslator.cpp" #include "midi/ump/yup_UMPIterator.cpp" #include "utilities/yup_AudioWorkgroup.cpp" - -#if YUP_UNIT_TESTS -#include "utilities/yup_ADSR_test.cpp" -#include "midi/ump/yup_UMP_test.cpp" -#endif diff --git a/modules/yup_core/files/yup_FileInputStream.cpp b/modules/yup_core/files/yup_FileInputStream.cpp index 6e7661dc2..456a0ac5e 100644 --- a/modules/yup_core/files/yup_FileInputStream.cpp +++ b/modules/yup_core/files/yup_FileInputStream.cpp @@ -93,102 +93,4 @@ bool FileInputStream::setPosition (int64 pos) return currentPosition == pos; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct FileInputStreamTests final : public UnitTest -{ - FileInputStreamTests() - : UnitTest ("FileInputStream", UnitTestCategories::streams) - { - } - - void runTest() override - { - beginTest ("Open stream non-existent file"); - { - auto tempFile = File::createTempFile (".txt"); - expect (! tempFile.exists()); - - FileInputStream stream (tempFile); - expect (stream.failedToOpen()); - } - - beginTest ("Open stream existing file"); - { - auto tempFile = File::createTempFile (".txt"); - tempFile.create(); - expect (tempFile.exists()); - - FileInputStream stream (tempFile); - expect (stream.openedOk()); - } - - const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); - File f (File::createTempFile (".txt")); - f.appendData (data.getData(), data.getSize()); - FileInputStream stream (f); - - beginTest ("Read"); - { - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - size_t numBytesRead = 0; - MemoryBlock readBuffer (data.getSize()); - - while (numBytesRead < data.getSize()) - { - numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - - expect (readBuffer == data); - } - - beginTest ("Skip"); - { - stream.setPosition (0); - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - size_t numBytesRead = 0; - const int numBytesToSkip = 5; - - while (numBytesRead < data.getSize()) - { - stream.skipNextBytes (numBytesToSkip); - numBytesRead += numBytesToSkip; - numBytesRead = std::min (numBytesRead, data.getSize()); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - - f.deleteFile(); - } - } -}; - -static FileInputStreamTests fileInputStreamTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/files/yup_FileSearchPath.cpp b/modules/yup_core/files/yup_FileSearchPath.cpp index 84b507d68..2a61a05bb 100644 --- a/modules/yup_core/files/yup_FileSearchPath.cpp +++ b/modules/yup_core/files/yup_FileSearchPath.cpp @@ -205,57 +205,4 @@ bool FileSearchPath::isFileInPath (const File& fileToCheck, return false; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class FileSearchPathTests final : public UnitTest -{ -public: - FileSearchPathTests() - : UnitTest ("FileSearchPath", UnitTestCategories::files) - { - } - - void runTest() override - { - beginTest ("removeRedundantPaths"); - { -#if YUP_WINDOWS - const String prefix = "C:"; -#else - const String prefix = ""; -#endif - - { - FileSearchPath fsp { prefix + "/a/b/c/d;" + prefix + "/a/b/c/e;" + prefix + "/a/b/c" }; - fsp.removeRedundantPaths(); - expectEquals (fsp.toString(), prefix + "/a/b/c"); - } - - { - FileSearchPath fsp { prefix + "/a/b/c;" + prefix + "/a/b/c/d;" + prefix + "/a/b/c/e" }; - fsp.removeRedundantPaths(); - expectEquals (fsp.toString(), prefix + "/a/b/c"); - } - - { - FileSearchPath fsp { prefix + "/a/b/c/d;" + prefix + "/a/b/c;" + prefix + "/a/b/c/e" }; - fsp.removeRedundantPaths(); - expectEquals (fsp.toString(), prefix + "/a/b/c"); - } - - { - FileSearchPath fsp { "%FOO%;" + prefix + "/a/b/c;%FOO%;" + prefix + "/a/b/c/d" }; - fsp.removeRedundantPaths(); - expectEquals (fsp.toString(), "%FOO%;" + prefix + "/a/b/c"); - } - } - } -}; - -static FileSearchPathTests fileSearchPathTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/files/yup_RangedDirectoryIterator.h b/modules/yup_core/files/yup_RangedDirectoryIterator.h index eb595f40a..a507d05f5 100644 --- a/modules/yup_core/files/yup_RangedDirectoryIterator.h +++ b/modules/yup_core/files/yup_RangedDirectoryIterator.h @@ -48,7 +48,7 @@ YUP_BEGIN_IGNORE_DEPRECATION_WARNINGS @tags{Core} */ -class DirectoryEntry final +class YUP_API DirectoryEntry final { public: /** The path to a file or folder. */ @@ -112,7 +112,7 @@ inline const DirectoryEntry& operator* (const DirectoryEntry& e) noexcept { retu @tags{Core} */ -class RangedDirectoryIterator final +class YUP_API RangedDirectoryIterator final { public: using difference_type = std::ptrdiff_t; diff --git a/modules/yup_core/memory/yup_AllocationHooks.cpp b/modules/yup_core/memory/yup_AllocationHooks.cpp index dfe7fd5c2..7a6812132 100644 --- a/modules/yup_core/memory/yup_AllocationHooks.cpp +++ b/modules/yup_core/memory/yup_AllocationHooks.cpp @@ -94,24 +94,4 @@ void operator delete[] (void* p, size_t) noexcept std::free (p); } -namespace yup -{ - -//============================================================================== -UnitTestAllocationChecker::UnitTestAllocationChecker (UnitTest& test) - : unitTest (test) -{ - getAllocationHooksForThread().addListener (this); -} - -UnitTestAllocationChecker::~UnitTestAllocationChecker() noexcept -{ - getAllocationHooksForThread().removeListener (this); - unitTest.expectEquals ((int) calls, 0, "new or delete was incorrectly called while allocation checker was active"); -} - -void UnitTestAllocationChecker::newOrDeleteCalled() noexcept { ++calls; } - -} // namespace yup - #endif diff --git a/modules/yup_core/memory/yup_AllocationHooks.h b/modules/yup_core/memory/yup_AllocationHooks.h index 8b986a7d3..2fa6e056d 100644 --- a/modules/yup_core/memory/yup_AllocationHooks.h +++ b/modules/yup_core/memory/yup_AllocationHooks.h @@ -60,32 +60,6 @@ class AllocationHooks ListenerList listenerList; }; -//============================================================================== -/** Scoped checker which will cause a unit test failure if any new/delete calls - are made during the lifetime of the UnitTestAllocationChecker. -*/ -class UnitTestAllocationChecker : private AllocationHooks::Listener -{ -public: - /** Create a checker which will log a failure to the passed test if - any calls to new/delete are made. - - Remember to call `UnitTest::beginTest` before constructing this checker! - */ - explicit UnitTestAllocationChecker (UnitTest& test); - - /** Will add a failure to the test if the number of new/delete calls during - this object's lifetime was greater than zero. - */ - ~UnitTestAllocationChecker() noexcept override; - -private: - void newOrDeleteCalled() noexcept override; - - UnitTest& unitTest; - size_t calls = 0; -}; - } // namespace yup #endif diff --git a/modules/yup_core/network/yup_IPAddress.cpp b/modules/yup_core/network/yup_IPAddress.cpp index 1dcadd64b..899ad3cab 100644 --- a/modules/yup_core/network/yup_IPAddress.cpp +++ b/modules/yup_core/network/yup_IPAddress.cpp @@ -401,95 +401,4 @@ Array IPAddress::getAllAddresses (bool includeIPv6) return addresses; } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct IPAddressTests final : public UnitTest -{ - IPAddressTests() - : UnitTest ("IPAddress", UnitTestCategories::networking) - { - } - - void runTest() override - { - testConstructors(); - testFindAllAddresses(); - testFindBroadcastAddress(); - } - - void testConstructors() - { - beginTest ("constructors"); - - // Default IPAdress should be null - IPAddress defaultConstructed; - expect (defaultConstructed.isNull()); - - auto local = IPAddress::local(); - expect (! local.isNull()); - - IPAddress ipv4 { 1, 2, 3, 4 }; - expect (! ipv4.isNull()); - expect (! ipv4.isIPv6); - expect (ipv4.toString() == "1.2.3.4"); - } - - void testFindAllAddresses() - { - beginTest ("find all addresses"); - - Array ipv4Addresses; - Array allAddresses; - - IPAddress::findAllAddresses (ipv4Addresses, false); - IPAddress::findAllAddresses (allAddresses, true); - - expect (allAddresses.size() >= ipv4Addresses.size()); - - for (auto& a : ipv4Addresses) - { - expect (! a.isNull()); - expect (! a.isIPv6); - } - - for (auto& a : allAddresses) - { - expect (! a.isNull()); - } - } - - void testFindBroadcastAddress() - { - beginTest ("broadcast addresses"); - - Array addresses; - - // Only IPv4 interfaces have broadcast - IPAddress::findAllAddresses (addresses, false); - - for (auto& a : addresses) - { - expect (! a.isNull()); - - auto broadcastAddress = IPAddress::getInterfaceBroadcastAddress (a); - - // If we retrieve an address, it should be an IPv4 address - if (! broadcastAddress.isNull()) - { - expect (! a.isIPv6); - } - } - - // Expect to fail to find a broadcast for this address - IPAddress address { 1, 2, 3, 4 }; - expect (IPAddress::getInterfaceBroadcastAddress (address).isNull()); - } -}; - -static IPAddressTests iPAddressTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/network/yup_NamedPipe.cpp b/modules/yup_core/network/yup_NamedPipe.cpp index baa6d6132..072c7c59e 100644 --- a/modules/yup_core/network/yup_NamedPipe.cpp +++ b/modules/yup_core/network/yup_NamedPipe.cpp @@ -83,211 +83,6 @@ String NamedPipe::getName() const // other methods for this class are implemented in the platform-specific files -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class NamedPipeTests final : public UnitTest -{ -public: - //============================================================================== - NamedPipeTests() - : UnitTest ("NamedPipe", UnitTestCategories::networking) - { - } - - void runTest() override - { - const auto pipeName = "TestPipe" + String ((intptr_t) Thread::getCurrentThreadId()); - - beginTest ("Pre test cleanup"); - { - NamedPipe pipe; - expect (pipe.createNewPipe (pipeName, false)); - } - - beginTest ("Create pipe"); - { - NamedPipe pipe; - expect (! pipe.isOpen()); - - expect (pipe.createNewPipe (pipeName, true)); - expect (pipe.isOpen()); - - expect (pipe.createNewPipe (pipeName, false)); - expect (pipe.isOpen()); - - NamedPipe otherPipe; - expect (! otherPipe.createNewPipe (pipeName, true)); - expect (! otherPipe.isOpen()); - } - - beginTest ("Existing pipe"); - { - NamedPipe pipe; - - expect (! pipe.openExisting (pipeName)); - expect (! pipe.isOpen()); - - expect (pipe.createNewPipe (pipeName, true)); - - NamedPipe otherPipe; - expect (otherPipe.openExisting (pipeName)); - expect (otherPipe.isOpen()); - } - - int sendData = 4684682; - - beginTest ("Receive message created pipe"); - { - NamedPipe pipe; - expect (pipe.createNewPipe (pipeName, true)); - - WaitableEvent senderFinished; - SenderThread sender (pipeName, false, senderFinished, sendData); - - sender.startThread(); - - int recvData = -1; - auto bytesRead = pipe.read (&recvData, sizeof (recvData), 2000); - - expect (senderFinished.wait (4000)); - - expectEquals (bytesRead, (int) sizeof (recvData)); - expectEquals (sender.result, (int) sizeof (sendData)); - expectEquals (recvData, sendData); - } - - beginTest ("Receive message existing pipe"); - { - WaitableEvent senderFinished; - SenderThread sender (pipeName, true, senderFinished, sendData); - - NamedPipe pipe; - expect (pipe.openExisting (pipeName)); - - sender.startThread(); - - int recvData = -1; - auto bytesRead = pipe.read (&recvData, sizeof (recvData), 2000); - - expect (senderFinished.wait (4000)); - - expectEquals (bytesRead, (int) sizeof (recvData)); - expectEquals (sender.result, (int) sizeof (sendData)); - expectEquals (recvData, sendData); - } - - beginTest ("Send message created pipe"); - { - NamedPipe pipe; - expect (pipe.createNewPipe (pipeName, true)); - - WaitableEvent receiverFinished; - ReceiverThread receiver (pipeName, false, receiverFinished); - - receiver.startThread(); - - auto bytesWritten = pipe.write (&sendData, sizeof (sendData), 2000); - - expect (receiverFinished.wait (4000)); - - expectEquals (bytesWritten, (int) sizeof (sendData)); - expectEquals (receiver.result, (int) sizeof (receiver.recvData)); - expectEquals (receiver.recvData, sendData); - } - - beginTest ("Send message existing pipe"); - { - WaitableEvent receiverFinished; - ReceiverThread receiver (pipeName, true, receiverFinished); - - NamedPipe pipe; - expect (pipe.openExisting (pipeName)); - - receiver.startThread(); - - auto bytesWritten = pipe.write (&sendData, sizeof (sendData), 2000); - - expect (receiverFinished.wait (4000)); - - expectEquals (bytesWritten, (int) sizeof (sendData)); - expectEquals (receiver.result, (int) sizeof (receiver.recvData)); - expectEquals (receiver.recvData, sendData); - } - } - -private: - //============================================================================== - struct NamedPipeThread : public Thread - { - NamedPipeThread (const String& tName, const String& pName, bool shouldCreatePipe, WaitableEvent& completed) - : Thread (tName) - , pipeName (pName) - , workCompleted (completed) - { - if (shouldCreatePipe) - pipe.createNewPipe (pipeName); - else - pipe.openExisting (pipeName); - } - - NamedPipe pipe; - const String& pipeName; - WaitableEvent& workCompleted; - - int result = -2; - }; - - //============================================================================== - struct SenderThread final : public NamedPipeThread - { - SenderThread (const String& pName, bool shouldCreatePipe, WaitableEvent& completed, int sData) - : NamedPipeThread ("NamePipeSender", pName, shouldCreatePipe, completed) - , sendData (sData) - { - } - - ~SenderThread() override - { - stopThread (100); - } - - void run() override - { - result = pipe.write (&sendData, sizeof (sendData), 2000); - workCompleted.signal(); - } - - const int sendData; - }; - - //============================================================================== - struct ReceiverThread final : public NamedPipeThread - { - ReceiverThread (const String& pName, bool shouldCreatePipe, WaitableEvent& completed) - : NamedPipeThread ("NamePipeSender", pName, shouldCreatePipe, completed) - { - } - - ~ReceiverThread() override - { - stopThread (100); - } - - void run() override - { - result = pipe.read (&recvData, sizeof (recvData), 2000); - workCompleted.signal(); - } - - int recvData = -2; - }; -}; - -static NamedPipeTests namedPipeTests; - -#endif #endif } // namespace yup diff --git a/modules/yup_core/network/yup_Socket.cpp b/modules/yup_core/network/yup_Socket.cpp index b78157c61..5f6c132f3 100644 --- a/modules/yup_core/network/yup_Socket.cpp +++ b/modules/yup_core/network/yup_Socket.cpp @@ -843,73 +843,6 @@ bool DatagramSocket::setEnablePortReuse ([[maybe_unused]] bool enabled) YUP_END_IGNORE_WARNINGS_MSVC -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct SocketTests final : public UnitTest -{ - SocketTests() - : UnitTest ("Sockets", UnitTestCategories::networking) - { - } - - void runTest() override - { - auto localHost = IPAddress::local(); - int portNum = 12345; - - beginTest ("StreamingSocket"); - { - StreamingSocket socketServer; - - expect (socketServer.isConnected() == false); - expect (socketServer.getHostName().isEmpty()); - expect (socketServer.getBoundPort() == -1); - expect (static_cast (socketServer.getRawSocketHandle()) == invalidSocket); - - expect (socketServer.createListener (portNum, localHost.toString())); - - StreamingSocket socket; - - expect (socket.connect (localHost.toString(), portNum)); - - expect (socket.isConnected() == true); - expect (socket.getHostName() == localHost.toString()); - expect (socket.getBoundPort() != -1); - expect (static_cast (socket.getRawSocketHandle()) != invalidSocket); - - socket.close(); - - expect (socket.isConnected() == false); - expect (socket.getHostName().isEmpty()); - expect (socket.getBoundPort() == -1); - expect (static_cast (socket.getRawSocketHandle()) == invalidSocket); - } - - beginTest ("DatagramSocket"); - { - DatagramSocket socket; - - expect (socket.getBoundPort() == -1); - expect (static_cast (socket.getRawSocketHandle()) != invalidSocket); - - expect (socket.bindToPort (portNum, localHost.toString())); - - expect (socket.getBoundPort() == portNum); - expect (static_cast (socket.getRawSocketHandle()) != invalidSocket); - - socket.shutdown(); - - expect (socket.getBoundPort() == -1); - expect (static_cast (socket.getRawSocketHandle()) == invalidSocket); - } - } -}; - -static SocketTests socketTests; - -#endif #endif } // namespace yup diff --git a/modules/yup_core/streams/yup_BufferedInputStream.cpp b/modules/yup_core/streams/yup_BufferedInputStream.cpp index 3c67cb774..c459c5041 100644 --- a/modules/yup_core/streams/yup_BufferedInputStream.cpp +++ b/modules/yup_core/streams/yup_BufferedInputStream.cpp @@ -200,143 +200,4 @@ String BufferedInputStream::readString() return InputStream::readString(); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct BufferedInputStreamTests final : public UnitTest -{ - template - static void applyImpl (Fn&& fn, std::index_sequence, Values&& values) - { - fn (std::get (values)...); - } - - template - static void apply (Fn&& fn, std::tuple values) - { - applyImpl (fn, std::make_index_sequence(), values); - } - - template - static void allCombinationsImpl (Fn&& fn, Values&& values) - { - apply (fn, values); - } - - template - static void allCombinationsImpl (Fn&& fn, Values&& values, Range&& range, Ranges&&... ranges) - { - for (auto& item : range) - allCombinationsImpl (fn, std::tuple_cat (values, std::tie (item)), ranges...); - } - - template - static void allCombinations (Fn&& fn, Ranges&&... ranges) - { - allCombinationsImpl (fn, std::tie(), ranges...); - } - - BufferedInputStreamTests() - : UnitTest ("BufferedInputStream", UnitTestCategories::streams) - { - } - - void runTest() override - { - const MemoryBlock testBufferA ("abcdefghijklmnopqrstuvwxyz", 26); - - const auto testBufferB = [&] - { - MemoryBlock mb { 8192 }; - auto r = getRandom(); - - std::for_each (mb.begin(), mb.end(), [&] (char& item) - { - item = (char) r.nextInt (std::numeric_limits::max()); - }); - - return mb; - }(); - - const MemoryBlock buffers[] { testBufferA, testBufferB }; - const int readSizes[] { 3, 10, 50 }; - const bool shouldPeek[] { false, true }; - - const auto runTest = [this] (const MemoryBlock& data, const int readSize, const bool peek) - { - MemoryInputStream mi (data, true); - - BufferedInputStream stream (mi, jmin (200, (int) data.getSize())); - - beginTest ("Read"); - - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - size_t numBytesRead = 0; - MemoryBlock readBuffer (data.getSize()); - - while (numBytesRead < data.getSize()) - { - if (peek) - expectEquals (stream.peekByte(), *(char*) (data.begin() + numBytesRead)); - - const auto startingPos = numBytesRead; - numBytesRead += (size_t) stream.read (readBuffer.begin() + numBytesRead, readSize); - - expect (std::equal (readBuffer.begin() + startingPos, - readBuffer.begin() + numBytesRead, - data.begin() + startingPos, - data.begin() + numBytesRead)); - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - - expect (readBuffer == data); - - beginTest ("Skip"); - - stream.setPosition (0); - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - numBytesRead = 0; - const int numBytesToSkip = 5; - - while (numBytesRead < data.getSize()) - { - expectEquals (stream.peekByte(), *(char*) (data.begin() + numBytesRead)); - - stream.skipNextBytes (numBytesToSkip); - numBytesRead += numBytesToSkip; - numBytesRead = std::min (numBytesRead, data.getSize()); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - }; - - allCombinations (runTest, buffers, readSizes, shouldPeek); - } -}; - -static BufferedInputStreamTests bufferedInputStreamTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/streams/yup_MemoryInputStream.cpp b/modules/yup_core/streams/yup_MemoryInputStream.cpp index faeb691c3..aec7a1488 100644 --- a/modules/yup_core/streams/yup_MemoryInputStream.cpp +++ b/modules/yup_core/streams/yup_MemoryInputStream.cpp @@ -123,126 +123,4 @@ void MemoryInputStream::skipNextBytes (int64 numBytesToSkip) setPosition (getPosition() + numBytesToSkip); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -class MemoryStreamTests final : public UnitTest -{ -public: - MemoryStreamTests() - : UnitTest ("MemoryInputStream & MemoryOutputStream", UnitTestCategories::streams) - { - } - - void runTest() override - { - beginTest ("Basics"); - Random r = getRandom(); - - int randomInt = r.nextInt(); - int64 randomInt64 = r.nextInt64(); - double randomDouble = r.nextDouble(); - String randomString (createRandomWideCharString (r)); - - MemoryOutputStream mo; - mo.writeInt (randomInt); - mo.writeIntBigEndian (randomInt); - mo.writeCompressedInt (randomInt); - mo.writeString (randomString); - mo.writeInt64 (randomInt64); - mo.writeInt64BigEndian (randomInt64); - mo.writeDouble (randomDouble); - mo.writeDoubleBigEndian (randomDouble); - - MemoryInputStream mi (mo.getData(), mo.getDataSize(), false); - expect (mi.readInt() == randomInt); - expect (mi.readIntBigEndian() == randomInt); - expect (mi.readCompressedInt() == randomInt); - expectEquals (mi.readString(), randomString); - expect (mi.readInt64() == randomInt64); - expect (mi.readInt64BigEndian() == randomInt64); - expectEquals (mi.readDouble(), randomDouble); - expectEquals (mi.readDoubleBigEndian(), randomDouble); - - const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); - MemoryInputStream stream (data, true); - - beginTest ("Read"); - - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - size_t numBytesRead = 0; - MemoryBlock readBuffer (data.getSize()); - - while (numBytesRead < data.getSize()) - { - numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - - expect (readBuffer == data); - - beginTest ("Skip"); - - stream.setPosition (0); - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - numBytesRead = 0; - const int numBytesToSkip = 5; - - while (numBytesRead < data.getSize()) - { - stream.skipNextBytes (numBytesToSkip); - numBytesRead += numBytesToSkip; - numBytesRead = std::min (numBytesRead, data.getSize()); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == data.getSize())); - } - - expectEquals (stream.getPosition(), (int64) data.getSize()); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - } - - static String createRandomWideCharString (Random& r) - { - yup_wchar buffer[50] = { 0 }; - - for (int i = 0; i < numElementsInArray (buffer) - 1; ++i) - { - if (r.nextBool()) - { - do - { - buffer[i] = (yup_wchar) (1 + r.nextInt (0x10ffff - 1)); - } while (! CharPointer_UTF16::canRepresent (buffer[i])); - } - else - buffer[i] = (yup_wchar) (1 + r.nextInt (0xff)); - } - - return CharPointer_UTF32 (buffer); - } -}; - -static MemoryStreamTests memoryInputStreamUnitTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/streams/yup_SubregionStream.cpp b/modules/yup_core/streams/yup_SubregionStream.cpp index 4ec45ab1d..80f9987eb 100644 --- a/modules/yup_core/streams/yup_SubregionStream.cpp +++ b/modules/yup_core/streams/yup_SubregionStream.cpp @@ -96,83 +96,4 @@ bool SubregionStream::isExhausted() return source->isExhausted(); } -//============================================================================== -//============================================================================== -#if YUP_UNIT_TESTS - -struct SubregionInputStreamTests final : public UnitTest -{ - SubregionInputStreamTests() - : UnitTest ("SubregionInputStream", UnitTestCategories::streams) - { - } - - void runTest() override - { - const MemoryBlock data ("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", 52); - MemoryInputStream mi (data, true); - - const int offset = getRandom().nextInt ((int) data.getSize()); - const size_t subregionSize = data.getSize() - (size_t) offset; - - SubregionStream stream (&mi, offset, (int) subregionSize, false); - - beginTest ("Read"); - - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) subregionSize); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - size_t numBytesRead = 0; - MemoryBlock readBuffer (subregionSize); - - while (numBytesRead < subregionSize) - { - numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (subregionSize - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == subregionSize)); - } - - expectEquals (stream.getPosition(), (int64) subregionSize); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - - const MemoryBlock memoryBlockToCheck (data.begin() + (size_t) offset, data.getSize() - (size_t) offset); - expect (readBuffer == memoryBlockToCheck); - - beginTest ("Skip"); - - stream.setPosition (0); - expectEquals (stream.getPosition(), (int64) 0); - expectEquals (stream.getTotalLength(), (int64) subregionSize); - expectEquals (stream.getNumBytesRemaining(), stream.getTotalLength()); - expect (! stream.isExhausted()); - - numBytesRead = 0; - const int64 numBytesToSkip = 5; - - while (numBytesRead < subregionSize) - { - stream.skipNextBytes (numBytesToSkip); - numBytesRead += numBytesToSkip; - numBytesRead = std::min (numBytesRead, subregionSize); - - expectEquals (stream.getPosition(), (int64) numBytesRead); - expectEquals (stream.getNumBytesRemaining(), (int64) (subregionSize - numBytesRead)); - expect (stream.isExhausted() == (numBytesRead == subregionSize)); - } - - expectEquals (stream.getPosition(), (int64) subregionSize); - expectEquals (stream.getNumBytesRemaining(), (int64) 0); - expect (stream.isExhausted()); - } -}; - -static SubregionInputStreamTests subregionInputStreamTests; - -#endif - } // namespace yup diff --git a/modules/yup_core/time/yup_Time.cpp b/modules/yup_core/time/yup_Time.cpp index e9e750144..994398abb 100644 --- a/modules/yup_core/time/yup_Time.cpp +++ b/modules/yup_core/time/yup_Time.cpp @@ -64,36 +64,6 @@ static std::tm millisToLocal (int64 millis) noexcept #endif } -static std::tm millisToUTC (int64 millis) noexcept -{ -#if YUP_WINDOWS - std::tm result; - millis /= 1000; - - if (_gmtime64_s (&result, &millis) != 0) - zerostruct (result); - - return result; - -#else - std::tm result; - auto now = (time_t) (millis / 1000); - - if (gmtime_r (&now, &result) == nullptr) - zerostruct (result); - - return result; -#endif -} - -static int getUTCOffsetSeconds (const int64 millis) noexcept -{ - auto utc = millisToUTC (millis); - utc.tm_isdst = -1; // Treat this UTC time as local to find the offset - - return (int) ((millis / 1000) - (int64) mktime (&utc)); -} - static int extendedModulo (const int64 value, const int modulo) noexcept { return (int) (value >= 0 ? (value % modulo) @@ -175,6 +145,21 @@ static int64 daysFrom1970 (int year, int month) noexcept return daysFrom1970 (year) + daysFromJan1 (year, month); } +static int64 mktime_local (const std::tm& t) noexcept +{ + auto t1 = t; + const auto result = mktime (&t1); + + jassert (t.tm_year == t1.tm_year + && t.tm_mon == t1.tm_mon + && t.tm_mday == t1.tm_mday + && t.tm_hour == t1.tm_hour + && t.tm_min == t1.tm_min + && t.tm_sec == t1.tm_sec); + + return (int64) result; +} + // There's no posix function that does a UTC version of mktime, // so annoyingly we need to implement this manually.. static int64 mktime_utc (const std::tm& t) noexcept @@ -218,10 +203,11 @@ Time::Time (int year, int month, int day, int hours, int minutes, int seconds, i t.tm_hour = hours; t.tm_min = minutes; t.tm_sec = seconds; - t.tm_isdst = -1; + t.tm_isdst = useLocalTime ? -1 : 0; - millisSinceEpoch = 1000 * (useLocalTime ? (int64) mktime (&t) : TimeHelpers::mktime_utc (t)) - + milliseconds; + millisSinceEpoch = milliseconds + 1000 * (useLocalTime + ? TimeHelpers::mktime_local (t) + : TimeHelpers::mktime_utc (t)); } //============================================================================== @@ -434,7 +420,8 @@ String Time::getTimeZone() const int Time::getUTCOffsetSeconds() const noexcept { - return TimeHelpers::getUTCOffsetSeconds (millisSinceEpoch); + const auto local = TimeHelpers::millisToLocal (millisSinceEpoch); + return (int) (TimeHelpers::mktime_utc (local) - (millisSinceEpoch / 1000)); } String Time::getUTCOffsetString (bool includeSemiColon) const diff --git a/modules/yup_core/unit_tests/yup_UnitTest.cpp b/modules/yup_core/unit_tests/yup_UnitTest.cpp deleted file mode 100644 index 35c8d709d..000000000 --- a/modules/yup_core/unit_tests/yup_UnitTest.cpp +++ /dev/null @@ -1,309 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -UnitTest::UnitTest (const String& nm, const String& ctg) - : name (nm) - , category (ctg) -{ - getAllTests().add (this); -} - -UnitTest::~UnitTest() -{ - getAllTests().removeFirstMatchingValue (this); -} - -Array& UnitTest::getAllTests() -{ - static Array tests; - return tests; -} - -Array UnitTest::getTestsInCategory (const String& category) -{ - if (category.isEmpty()) - return getAllTests(); - - Array unitTests; - - for (auto* test : getAllTests()) - if (test->getCategory() == category) - unitTests.add (test); - - return unitTests; -} - -StringArray UnitTest::getAllCategories() -{ - StringArray categories; - - for (auto* test : getAllTests()) - if (test->getCategory().isNotEmpty()) - categories.addIfNotAlreadyThere (test->getCategory()); - - return categories; -} - -void UnitTest::initialise() {} - -void UnitTest::shutdown() {} - -void UnitTest::performTest (UnitTestRunner* const newRunner) -{ - jassert (newRunner != nullptr); - runner = newRunner; - - initialise(); - runTest(); - shutdown(); -} - -void UnitTest::logMessage (const String& message) -{ - // This method's only valid while the test is being run! - jassert (runner != nullptr); - - runner->logMessage (message); -} - -void UnitTest::beginTest (const String& testName) -{ - // This method's only valid while the test is being run! - jassert (runner != nullptr); - - runner->beginNewTest (this, testName); -} - -void UnitTest::expect (const bool result, const String& failureMessage) -{ - // This method's only valid while the test is being run! - jassert (runner != nullptr); - - if (result) - runner->addPass(); - else - runner->addFail (failureMessage); -} - -Random UnitTest::getRandom() const -{ - // This method's only valid while the test is being run! - jassert (runner != nullptr); - - return runner->randomForTest; -} - -//============================================================================== -UnitTestRunner::UnitTestRunner() {} - -UnitTestRunner::~UnitTestRunner() {} - -void UnitTestRunner::setAssertOnFailure (bool shouldAssert) noexcept -{ - assertOnFailure = shouldAssert; -} - -void UnitTestRunner::setPassesAreLogged (bool shouldDisplayPasses) noexcept -{ - logPasses = shouldDisplayPasses; -} - -int UnitTestRunner::getNumResults() const noexcept -{ - return results.size(); -} - -const UnitTestRunner::TestResult* UnitTestRunner::getResult (int index) const noexcept -{ - return results[index]; -} - -void UnitTestRunner::resultsUpdated() -{ -} - -void UnitTestRunner::runTests (const Array& tests, int64 randomSeed) -{ - results.clear(); - resultsUpdated(); - - if (randomSeed == 0) - randomSeed = Random().nextInt (0x7ffffff); - - randomForTest = Random (randomSeed); - logMessage ("Random seed: 0x" + String::toHexString (randomSeed)); - - for (auto* t : tests) - { - if (shouldAbortTests()) - break; - -#if YUP_EXCEPTIONS_DISABLED - t->performTest (this); -#else - try - { - t->performTest (this); - } - catch (...) - { - addFail ("An unhandled exception was thrown!"); - } -#endif - } - - endTest(); -} - -void UnitTestRunner::runAllTests (int64 randomSeed) -{ - runTests (UnitTest::getAllTests(), randomSeed); -} - -void UnitTestRunner::runTestsInCategory (const String& category, int64 randomSeed) -{ - runTests (UnitTest::getTestsInCategory (category), randomSeed); -} - -void UnitTestRunner::logMessage (const String& message) -{ - Logger::writeToLog (message); -} - -bool UnitTestRunner::shouldAbortTests() -{ - return false; -} - -static String getTestNameString (const String& testName, const String& subCategory) -{ - return testName + " / " + subCategory; -} - -void UnitTestRunner::beginNewTest (UnitTest* const test, const String& subCategory) -{ - endTest(); - currentTest = test; - - auto testName = test->getName(); - results.add (new TestResult (testName, subCategory)); - - logMessage ("-----------------------------------------------------------------"); - logMessage ("Starting tests in: " + getTestNameString (testName, subCategory) + "..."); - - resultsUpdated(); -} - -void UnitTestRunner::endTest() -{ - if (auto* r = results.getLast()) - { - r->endTime = Time::getCurrentTime(); - - if (r->failures > 0) - { - String m ("FAILED!! "); - m << r->failures << (r->failures == 1 ? " test" : " tests") - << " failed, out of a total of " << (r->passes + r->failures); - - logMessage (String()); - logMessage (m); - logMessage (String()); - } - else - { - logMessage ("Completed tests in " + getTestNameString (r->unitTestName, r->subcategoryName)); - } - } -} - -void UnitTestRunner::addPass() -{ - { - const ScopedLock sl (results.getLock()); - - auto* r = results.getLast(); - jassert (r != nullptr); // You need to call UnitTest::beginTest() before performing any tests! - - r->passes++; - - if (logPasses) - { - String message ("Test "); - message << (r->failures + r->passes) << " passed"; - logMessage (message); - } - } - - resultsUpdated(); -} - -void UnitTestRunner::addFail (const String& failureMessage) -{ - { - const ScopedLock sl (results.getLock()); - - auto* r = results.getLast(); - jassert (r != nullptr); // You need to call UnitTest::beginTest() before performing any tests! - - r->failures++; - - String message ("!!! Test "); - message << (r->failures + r->passes) << " failed"; - - if (failureMessage.isNotEmpty()) - message << ": " << failureMessage; - - r->messages.add (message); - - logMessage (message); - } - - resultsUpdated(); - - if (assertOnFailure) - { - jassertfalse; - } -} - -} // namespace yup diff --git a/modules/yup_core/unit_tests/yup_UnitTest.h b/modules/yup_core/unit_tests/yup_UnitTest.h deleted file mode 100644 index 223f79dc4..000000000 --- a/modules/yup_core/unit_tests/yup_UnitTest.h +++ /dev/null @@ -1,466 +0,0 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -class UnitTestRunner; - -//============================================================================== -/** - This is a base class for classes that perform a unit test. - - To write a test using this class, your code should look something like this: - - @code - class MyTest : public UnitTest - { - public: - MyTest() : UnitTest ("Foobar testing") {} - - void runTest() override - { - beginTest ("Part 1"); - - expect (myFoobar.doesSomething()); - expect (myFoobar.doesSomethingElse()); - - beginTest ("Part 2"); - - expect (myOtherFoobar.doesSomething()); - expect (myOtherFoobar.doesSomethingElse()); - - ...etc.. - } - }; - - // Creating a static instance will automatically add the instance to the array - // returned by UnitTest::getAllTests(), so the test will be included when you call - // UnitTestRunner::runAllTests() - static MyTest test; - @endcode - - To run a test, use the UnitTestRunner class. - - @see UnitTestRunner - - @tags{Core} -*/ -class YUP_API UnitTest -{ -public: - //============================================================================== - /** Creates a test with the given name and optionally places it in a category. */ - explicit UnitTest (const String& name, const String& category = String()); - - /** Destructor. */ - virtual ~UnitTest(); - - /** Returns the name of the test. */ - const String& getName() const noexcept { return name; } - - /** Returns the category of the test. */ - const String& getCategory() const noexcept { return category; } - - /** Runs the test, using the specified UnitTestRunner. - You shouldn't need to call this method directly - use - UnitTestRunner::runTests() instead. - */ - void performTest (UnitTestRunner* runner); - - /** Returns the set of all UnitTest objects that currently exist. */ - static Array& getAllTests(); - - /** Returns the set of UnitTests in a specified category. */ - static Array getTestsInCategory (const String& category); - - /** Returns a StringArray containing all of the categories of UnitTests that have been registered. */ - static StringArray getAllCategories(); - - //============================================================================== - /** You can optionally implement this method to set up your test. - This method will be called before runTest(). - */ - virtual void initialise(); - - /** You can optionally implement this method to clear up after your test has been run. - This method will be called after runTest() has returned. - */ - virtual void shutdown(); - - /** Implement this method in your subclass to actually run your tests. - - The content of your implementation should call beginTest() and expect() - to perform the tests. - */ - virtual void runTest() = 0; - - //============================================================================== - /** Tells the system that a new subsection of tests is beginning. - This should be called from your runTest() method, and may be called - as many times as you like, to demarcate different sets of tests. - */ - void beginTest (const String& testName); - - //============================================================================== - /** Checks that the result of a test is true, and logs this result. - - In your runTest() method, you should call this method for each condition that - you want to check, e.g. - - @code - void runTest() - { - beginTest ("basic tests"); - expect (x + y == 2); - expect (getThing() == someThing); - ...etc... - } - @endcode - - If testResult is true, a pass is logged; if it's false, a failure is logged. - If the failure message is specified, it will be written to the log if the test fails. - */ - void expect (bool testResult, const String& failureMessage = String()); - - //============================================================================== - /** Compares a value to an expected value. - If they are not equal, prints out a message containing the expected and actual values. - */ - template - void expectEquals (ValueType actual, ValueType expected, String failureMessage = String()) - { - bool result = exactlyEqual (actual, expected); - expectResultAndPrint (actual, expected, result, "", failureMessage); - } - - /** Checks whether a value is not equal to a comparison value. - If this check fails, prints out a message containing the actual and comparison values. - */ - template - void expectNotEquals (ValueType value, ValueType valueToCompareTo, String failureMessage = String()) - { - bool result = ! exactlyEqual (value, valueToCompareTo); - expectResultAndPrint (value, valueToCompareTo, result, "unequal to", failureMessage); - } - - /** Checks whether a value is greater than a comparison value. - If this check fails, prints out a message containing the actual and comparison values. - */ - template - void expectGreaterThan (ValueType value, ValueType valueToCompareTo, String failureMessage = String()) - { - bool result = value > valueToCompareTo; - expectResultAndPrint (value, valueToCompareTo, result, "greater than", failureMessage); - } - - /** Checks whether a value is less than a comparison value. - If this check fails, prints out a message containing the actual and comparison values. - */ - template - void expectLessThan (ValueType value, ValueType valueToCompareTo, String failureMessage = String()) - { - bool result = value < valueToCompareTo; - expectResultAndPrint (value, valueToCompareTo, result, "less than", failureMessage); - } - - /** Checks whether a value is greater or equal to a comparison value. - If this check fails, prints out a message containing the actual and comparison values. - */ - template - void expectGreaterOrEqual (ValueType value, ValueType valueToCompareTo, String failureMessage = String()) - { - bool result = value >= valueToCompareTo; - expectResultAndPrint (value, valueToCompareTo, result, "greater or equal to", failureMessage); - } - - /** Checks whether a value is less or equal to a comparison value. - If this check fails, prints out a message containing the actual and comparison values. - */ - template - void expectLessOrEqual (ValueType value, ValueType valueToCompareTo, String failureMessage = String()) - { - bool result = value <= valueToCompareTo; - expectResultAndPrint (value, valueToCompareTo, result, "less or equal to", failureMessage); - } - - /** Computes the difference between a value and a comparison value, and if it is larger than a - specified maximum value, prints out a message containing the actual and comparison values - and the maximum allowed error. - */ - template - void expectWithinAbsoluteError (ValueType actual, ValueType expected, ValueType maxAbsoluteError, String failureMessage = String()) - { - const ValueType diff = std::abs (actual - expected); - const bool result = diff <= maxAbsoluteError; - - expectResultAndPrint (actual, expected, result, " within " + String (maxAbsoluteError) + " of", failureMessage); - } - -//============================================================================== -/** Checks that the result of an expression does not throw an exception. */ -#define expectDoesNotThrow(expr) \ - try \ - { \ - (expr); \ - expect (true); \ - } \ - catch (...) \ - { \ - expect (false, "Expected: does not throw an exception, Actual: throws."); \ - } - -/** Checks that the result of an expression throws an exception. */ -#define expectThrows(expr) \ - try \ - { \ - (expr); \ - expect (false, "Expected: throws an exception, Actual: does not throw."); \ - } \ - catch (...) \ - { \ - expect (true); \ - } - -/** Checks that the result of an expression throws an exception of a certain type. */ -#define expectThrowsType(expr, type) \ - try \ - { \ - (expr); \ - expect (false, "Expected: throws an exception of type " #type ", Actual: does not throw."); \ - } \ - catch (type&) \ - { \ - expect (true); \ - } \ - catch (...) \ - { \ - expect (false, "Expected: throws an exception of type " #type ", Actual: throws another type."); \ - } - - //============================================================================== - /** Writes a message to the test log. - This can only be called from within your runTest() method. - */ - void logMessage (const String& message); - - /** Returns a shared RNG that all unit tests should use. - If a test needs random numbers, it's important that when an error is found, the - exact circumstances can be re-created in order to re-test the problem, by - repeating the test with the same random seed value. - To make this possible, the UnitTestRunner class creates a master seed value - for the run, writes this number to the log, and then this method returns a - Random object based on that seed. All tests should only use this method to - create any Random objects that they need. - - Note that this method will return an identical object each time it's called - for a given run, so if you need several different Random objects, the best - way to do that is to call Random::combineSeed() on the result to permute it - with a constant value. - */ - Random getRandom() const; - -private: - //============================================================================== - template - void expectResultAndPrint (ValueType value, ValueType valueToCompareTo, bool result, String compDescription, String failureMessage) - { - if (! result) - { - if (failureMessage.isNotEmpty()) - failureMessage << " -- "; - - failureMessage << "Expected value" << (compDescription.isEmpty() ? "" : " ") - << compDescription << ": " << valueToCompareTo - << ", Actual value: " << value; - } - - expect (result, failureMessage); - } - - //============================================================================== - const String name, category; - UnitTestRunner* runner = nullptr; - - YUP_DECLARE_NON_COPYABLE (UnitTest) -}; - -//============================================================================== -/** - Runs a set of unit tests. - - You can instantiate one of these objects and use it to invoke tests on a set of - UnitTest objects. - - By using a subclass of UnitTestRunner, you can intercept logging messages and - perform custom behaviour when each test completes. - - @see UnitTest - - @tags{Core} -*/ -class YUP_API UnitTestRunner -{ -public: - //============================================================================== - /** */ - UnitTestRunner(); - - /** Destructor. */ - virtual ~UnitTestRunner(); - - /** Runs a set of tests. - - The tests are performed in order, and the results are logged. To run all the - registered UnitTest objects that exist, use runAllTests(). - - If you want to run the tests with a predetermined seed, you can pass that into - the randomSeed argument, or pass 0 to have a randomly-generated seed chosen. - */ - void runTests (const Array& tests, int64 randomSeed = 0); - - /** Runs all the UnitTest objects that currently exist. - This calls runTests() for all the objects listed in UnitTest::getAllTests(). - - If you want to run the tests with a predetermined seed, you can pass that into - the randomSeed argument, or pass 0 to have a randomly-generated seed chosen. - */ - void runAllTests (int64 randomSeed = 0); - - /** Runs all the UnitTest objects within a specified category. - This calls runTests() for all the objects listed in UnitTest::getTestsInCategory(). - - If you want to run the tests with a predetermined seed, you can pass that into - the randomSeed argument, or pass 0 to have a randomly-generated seed chosen. - */ - void runTestsInCategory (const String& category, int64 randomSeed = 0); - - /** Sets a flag to indicate whether an assertion should be triggered if a test fails. - This is true by default. - */ - void setAssertOnFailure (bool shouldAssert) noexcept; - - /** Sets a flag to indicate whether successful tests should be logged. - By default, this is set to false, so that only failures will be displayed in the log. - */ - void setPassesAreLogged (bool shouldDisplayPasses) noexcept; - - //============================================================================== - /** Contains the results of a test. - - One of these objects is instantiated each time UnitTest::beginTest() is called, and - it contains details of the number of subsequent UnitTest::expect() calls that are - made. - */ - struct TestResult - { - TestResult() = default; - - explicit TestResult (const String& name, const String& subCategory) - : unitTestName (name) - , subcategoryName (subCategory) - { - } - - /** The main name of this test (i.e. the name of the UnitTest object being run). */ - String unitTestName; - /** The name of the current subcategory (i.e. the name that was set when UnitTest::beginTest() was called). */ - String subcategoryName; - - /** The number of UnitTest::expect() calls that succeeded. */ - int passes = 0; - /** The number of UnitTest::expect() calls that failed. */ - int failures = 0; - - /** A list of messages describing the failed tests. */ - StringArray messages; - - /** The time at which this test was started. */ - Time startTime = Time::getCurrentTime(); - /** The time at which this test ended. */ - Time endTime; - }; - - /** Returns the number of TestResult objects that have been performed. - @see getResult - */ - int getNumResults() const noexcept; - - /** Returns one of the TestResult objects that describes a test that has been run. - @see getNumResults - */ - const TestResult* getResult (int index) const noexcept; - -protected: - /** Called when the list of results changes. - You can override this to perform some sort of behaviour when results are added. - */ - virtual void resultsUpdated(); - - /** Logs a message about the current test progress. - By default this just writes the message to the Logger class, but you could override - this to do something else with the data. - */ - virtual void logMessage (const String& message); - - /** This can be overridden to let the runner know that it should abort the tests - as soon as possible, e.g. because the thread needs to stop. - */ - virtual bool shouldAbortTests(); - -private: - //============================================================================== - friend class UnitTest; - - UnitTest* currentTest = nullptr; - String currentSubCategory; - OwnedArray results; - bool assertOnFailure = true, logPasses = false; - Random randomForTest; - - void beginNewTest (UnitTest* test, const String& subCategory); - void endTest(); - - void addPass(); - void addFail (const String& failureMessage); - - YUP_DECLARE_NON_COPYABLE (UnitTestRunner) -}; - -} // namespace yup diff --git a/modules/yup_core/yup_core.cpp b/modules/yup_core/yup_core.cpp index 6fc0bc860..7c905dc85 100644 --- a/modules/yup_core/yup_core.cpp +++ b/modules/yup_core/yup_core.cpp @@ -215,7 +215,6 @@ extern char** environ; #include "time/yup_PerformanceCounter.cpp" #include "time/yup_RelativeTime.cpp" #include "time/yup_Time.cpp" -#include "unit_tests/yup_UnitTest.cpp" #include "containers/yup_Variant.cpp" #include "javascript/yup_JSON.cpp" #include "javascript/yup_JSONUtils.cpp" @@ -325,11 +324,6 @@ extern char** environ; #include "streams/yup_URLInputSource.cpp" #endif -//============================================================================== -#if YUP_UNIT_TESTS -#include "maths/yup_MathsFunctions_test.cpp" -#endif - //============================================================================== #include diff --git a/modules/yup_core/yup_core.h b/modules/yup_core/yup_core.h index 4fc7aabd6..5209289e5 100644 --- a/modules/yup_core/yup_core.h +++ b/modules/yup_core/yup_core.h @@ -375,7 +375,6 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "network/yup_WebInputStream.h" #include "streams/yup_URLInputSource.h" #include "time/yup_PerformanceCounter.h" -#include "unit_tests/yup_UnitTest.h" #include "xml/yup_XmlDocument.h" #include "xml/yup_XmlElement.h" #include "zip/yup_GZIPCompressorOutputStream.h" @@ -405,10 +404,6 @@ YUP_END_IGNORE_WARNINGS_MSVC #include "native/yup_JNIHelpers_android.h" #endif -#if YUP_UNIT_TESTS -#include "unit_tests/yup_UnitTestCategories.h" -#endif - #if YUP_ENABLE_PROFILING YUP_BEGIN_IGNORE_WARNINGS_MSVC (4267) #include diff --git a/modules/yup_core/zip/yup_ZipFile.cpp b/modules/yup_core/zip/yup_ZipFile.cpp index 4677d43b8..8b05c2c86 100644 --- a/modules/yup_core/zip/yup_ZipFile.cpp +++ b/modules/yup_core/zip/yup_ZipFile.cpp @@ -130,11 +130,11 @@ struct ZipFile::ZipEntryHolder static Time parseFileTime (uint32 time, uint32 date) noexcept { auto year = (int) (1980 + (date >> 9)); - auto month = (int) (((date >> 5) & 15) - 1); + auto month = jlimit (0, 11, (int) (((date >> 5) & 15) - 1)); auto day = (int) (date & 31); - auto hours = (int) time >> 11; - auto minutes = (int) ((time >> 5) & 63); - auto seconds = (int) ((time & 31) << 1); + auto hours = jlimit (0, 23, (int) time >> 11); + auto minutes = jlimit (0, 59, (int) ((time >> 5) & 63)); + auto seconds = jlimit (0, 59, (int) ((time & 31) << 1)); return { year, month, day, hours, minutes, seconds }; } diff --git a/modules/yup_events/messages/yup_ApplicationBase.cpp b/modules/yup_events/messages/yup_ApplicationBase.cpp index 4b0070292..7a78fba3a 100644 --- a/modules/yup_events/messages/yup_ApplicationBase.cpp +++ b/modules/yup_events/messages/yup_ApplicationBase.cpp @@ -67,18 +67,23 @@ void YUPApplicationBase::setApplicationReturnValue (const int newReturnValue) no // This is called on the Mac and iOS where the OS doesn't allow the stack to unwind on shutdown.. void YUPApplicationBase::appWillTerminateByForce() { + int appReturnValue = 0; + YUP_AUTORELEASEPOOL { { const std::unique_ptr app (appInstance); if (app != nullptr) - app->shutdownApp(); + appReturnValue = app->shutdownApp(); } DeletedAtShutdown::deleteAll(); MessageManager::deleteInstance(); } + + if (appReturnValue != 0) + exit (appReturnValue); } void YUPApplicationBase::quit() diff --git a/modules/yup_graphics/primitives/yup_Point.h b/modules/yup_graphics/primitives/yup_Point.h index 254b6bf95..e09f1ba90 100644 --- a/modules/yup_graphics/primitives/yup_Point.h +++ b/modules/yup_graphics/primitives/yup_Point.h @@ -260,7 +260,7 @@ class YUP_API Point */ [[nodiscard]] constexpr ValueType horizontalDistanceTo (const Point& other) const noexcept { - return yup_abs (other.x - x); + return other.x - x; } /** Calculates the vertical distance between this point and another point. @@ -274,7 +274,7 @@ class YUP_API Point */ [[nodiscard]] constexpr ValueType verticalDistanceTo (const Point& other) const noexcept { - return yup_abs (other.y - y); + return other.y - y; } /** Calculates the Manhattan distance between this point and another point. diff --git a/tests/main.cpp b/tests/main.cpp index d364be8b5..3d20bd198 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -59,19 +59,137 @@ struct TestApplication : yup::YUPApplication int argc = argv.size() - 1; testing::InitGoogleMock (&argc, argv.data()); - yup::MessageManager::callAsync ([this, commandLineParameters] + // Add our custom minimalist listener + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + delete listeners.Release (listeners.default_result_printer()); + listeners.Append (new CompactPrinter (*this)); + + programStart = std::chrono::steady_clock::now(); + + // Start running suites one by one + runNextSuite (0); + } + + void shutdown() override {} + +private: + struct FailedTest + { + std::string name; + std::string failureDetails; + }; + + std::chrono::steady_clock::time_point programStart; + std::vector failedTests; + int totalTests = 0; + int passedTests = 0; + + void runNextSuite (int suiteIndex) + { + auto* unitTest = ::testing::UnitTest::GetInstance(); + + if (suiteIndex >= unitTest->total_test_suite_count()) { - auto result = RUN_ALL_TESTS(); + reportSummary(); + return; + } + + auto* testSuite = unitTest->GetTestSuite (suiteIndex); + std::string suiteName = testSuite->name(); - setApplicationReturnValue (result); + ::testing::GTEST_FLAG (filter) = suiteName + ".*"; + + yup::MessageManager::callAsync ([this, suiteIndex, suiteName] + { + (void) RUN_ALL_TESTS(); - quit(); + runNextSuite (suiteIndex + 1); }); } - void shutdown() override + void reportSummary() { + auto totalElapsed = std::chrono::steady_clock::now() - programStart; + std::cout << "\n========================================\n"; + + if (! failedTests.empty()) + { + std::cout << "*** FAILURES (" << failedTests.size() << "):\n"; + for (const auto& fail : failedTests) + { + std::cout << "\n--- " << fail.name << "\n" + << fail.failureDetails << "\n"; + } + } + + std::cout << "\n========================================\n"; + std::cout << "RESULT: " + << (failedTests.empty() ? "ALL PASSED" : "SOME FAILED") + << " (" << passedTests << "/" << totalTests << " tests) in " + << std::chrono::duration_cast (totalElapsed).count() + << " ms\n"; + + setApplicationReturnValue (failedTests.empty() ? 0 : 1); + + quit(); } + + // --- Custom Compact Printer --- + struct CompactPrinter : testing::EmptyTestEventListener + { + explicit CompactPrinter (TestApplication& app) + : owner (app) + { + } + + void OnTestStart (const testing::TestInfo& info) override + { + testStart = std::chrono::steady_clock::now(); + + failureStream.str (""); + failureStream.clear(); + } + + void OnTestPartResult (const testing::TestPartResult& result) override + { + if (result.failed()) + { + failureStream << result.file_name() << ":" << result.line_number() << ": " + << result.summary() << "\n"; + } + } + + void OnTestEnd (const testing::TestInfo& info) override + { + auto elapsed = std::chrono::steady_clock::now() - testStart; + owner.totalTests++; + + std::ostringstream line; + line << (std::string (info.test_suite_name()) + "." + info.name()); + + if (info.result()->Failed()) + { + std::cout << "*** FAIL - " << line.str() + << " (" << std::chrono::duration_cast (elapsed).count() << " ms)\n"; + + owner.failedTests.push_back ( + { std::string (info.test_suite_name()) + "." + info.name(), + failureStream.str() }); + } + else + { + std::cout << "--- PASS - " << line.str() + << " (" << std::chrono::duration_cast (elapsed).count() << " ms)\n"; + + owner.passedTests++; + } + } + + private: + TestApplication& owner; + std::chrono::steady_clock::time_point testStart; + std::stringstream failureStream; + }; }; START_YUP_APPLICATION (TestApplication) diff --git a/tests/yup_audio_basics/yup_ADSR.cpp b/tests/yup_audio_basics/yup_ADSR.cpp new file mode 100644 index 000000000..79a432d4c --- /dev/null +++ b/tests/yup_audio_basics/yup_ADSR.cpp @@ -0,0 +1,273 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ==============================================================================*/ + +#include +#include + +using namespace yup; + +namespace +{ +void advanceADSR (ADSR& adsr, int numSamplesToAdvance) +{ + while (--numSamplesToAdvance >= 0) + adsr.getNextSample(); +} + +AudioBuffer getTestBuffer (double sampleRate, float lengthInSeconds) +{ + AudioBuffer buffer { 2, roundToInt (lengthInSeconds * sampleRate) }; + + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + buffer.setSample (channel, sample, 1.0f); + + return buffer; +} + +bool isIncreasing (const AudioBuffer& b) +{ + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + { + float previousSample = -1.0f; + + for (int sample = 0; sample < b.getNumSamples(); ++sample) + { + const auto currentSample = b.getSample (channel, sample); + + if (currentSample <= previousSample) + return false; + + previousSample = currentSample; + } + } + + return true; +} + +bool isDecreasing (const AudioBuffer& b) +{ + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + { + float previousSample = std::numeric_limits::max(); + + for (int sample = 0; sample < b.getNumSamples(); ++sample) + { + const auto currentSample = b.getSample (channel, sample); + + if (currentSample >= previousSample) + return false; + + previousSample = currentSample; + } + } + + return true; +} + +bool isSustained (const AudioBuffer& b, float sustainLevel) +{ + jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0); + + for (int channel = 0; channel < b.getNumChannels(); ++channel) + if (b.findMinMax (channel, 0, b.getNumSamples()) != Range { sustainLevel, sustainLevel }) + return false; + + return true; +} +} // namespace + +class ADSRTest : public ::testing::Test +{ +protected: + void SetUp() override + { + adsr.setSampleRate (sampleRate); + adsr.setParameters (parameters); + } + + constexpr static double sampleRate = 44100.0; + const ADSR::Parameters parameters { 0.1f, 0.1f, 0.5f, 0.1f }; + ADSR adsr; +}; + +TEST_F (ADSRTest, Idle) +{ + adsr.reset(); + + EXPECT_FALSE (adsr.isActive()); + EXPECT_EQ (adsr.getNextSample(), 0.0f); +} + +TEST_F (ADSRTest, Attack) +{ + adsr.reset(); + + adsr.noteOn(); + EXPECT_TRUE (adsr.isActive()); + + auto buffer = getTestBuffer (sampleRate, parameters.attack); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isIncreasing (buffer)); +} + +TEST_F (ADSRTest, Decay) +{ + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); + + auto buffer = getTestBuffer (sampleRate, parameters.decay); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isDecreasing (buffer)); +} + +TEST_F (ADSRTest, Sustain) +{ + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay + 0.01) * sampleRate)); + + Random random (12345); + + for (int numTests = 0; numTests < 100; ++numTests) + { + const auto sustainLevel = random.nextFloat(); + const auto sustainLength = jmax (0.1f, random.nextFloat()); + + adsr.setParameters ({ parameters.attack, parameters.decay, sustainLevel, parameters.release }); + + auto buffer = getTestBuffer (sampleRate, sustainLength); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isSustained (buffer, sustainLevel)); + } +} + +TEST_F (ADSRTest, Release) +{ + adsr.reset(); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); + adsr.noteOff(); + + auto buffer = getTestBuffer (sampleRate, parameters.release); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isDecreasing (buffer)); +} + +TEST_F (ADSRTest, ZeroLengthAttackJumpsToDecay) +{ + adsr.reset(); + adsr.setParameters ({ 0.0f, parameters.decay, parameters.sustain, parameters.release }); + + adsr.noteOn(); + + auto buffer = getTestBuffer (sampleRate, parameters.decay); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isDecreasing (buffer)); +} + +TEST_F (ADSRTest, ZeroLengthDecayJumpsToSustain) +{ + adsr.reset(); + adsr.setParameters ({ parameters.attack, 0.0f, parameters.sustain, parameters.release }); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt (parameters.attack * sampleRate)); + adsr.getNextSample(); + + EXPECT_EQ (adsr.getNextSample(), parameters.sustain); + + auto buffer = getTestBuffer (sampleRate, 1); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isSustained (buffer, parameters.sustain)); +} + +TEST_F (ADSRTest, ZeroLengthAttackAndDecayJumpsToSustain) +{ + adsr.reset(); + adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release }); + + adsr.noteOn(); + + EXPECT_EQ (adsr.getNextSample(), parameters.sustain); + + auto buffer = getTestBuffer (sampleRate, 1); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isSustained (buffer, parameters.sustain)); +} + +TEST_F (ADSRTest, ZeroLengthAttackAndDecayReleasesCorrectly) +{ + adsr.reset(); + adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release }); + + adsr.noteOn(); + adsr.noteOff(); + + auto buffer = getTestBuffer (sampleRate, parameters.release); + adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples()); + + EXPECT_TRUE (isDecreasing (buffer)); +} + +TEST_F (ADSRTest, ZeroLengthReleaseResetsToIdle) +{ + adsr.reset(); + adsr.setParameters ({ parameters.attack, parameters.decay, parameters.sustain, 0.0f }); + + adsr.noteOn(); + advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate)); + adsr.noteOff(); + + EXPECT_FALSE (adsr.isActive()); +} diff --git a/tests/yup_audio_basics/yup_AudioChannelSet.cpp b/tests/yup_audio_basics/yup_AudioChannelSet.cpp new file mode 100644 index 000000000..b103d8026 --- /dev/null +++ b/tests/yup_audio_basics/yup_AudioChannelSet.cpp @@ -0,0 +1,132 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +namespace +{ +AudioChannelSet channelSetFromMask (uint64 mask) +{ + Array channels; + for (int bit = 0; bit <= 62; ++bit) + if ((mask & (1ull << bit)) != 0) + channels.add (static_cast (bit)); + + return AudioChannelSet::channelSetWithChannels (channels); +} +} // namespace + +class AudioChannelSetTest : public ::testing::Test +{ +protected: + void SetUp() override + { + max = AudioChannelSet::maxChannelsOfNamedLayout; + } + + void checkAmbisonic (uint64 mask, int order, const char* layoutName) + { + auto expected = AudioChannelSet::ambisonic (order); + auto numChannels = expected.size(); + + EXPECT_EQ (numChannels, BigInteger ((int64) mask).countNumberOfSetBits()); + EXPECT_EQ (channelSetFromMask (mask), expected); + + EXPECT_EQ (order, expected.getAmbisonicOrder()); + EXPECT_EQ (expected.getDescription(), layoutName); + + auto layouts = AudioChannelSet::channelSetsWithNumberOfChannels (numChannels); + EXPECT_TRUE (layouts.contains (expected)); + + for (auto layout : layouts) + EXPECT_EQ (layout.getAmbisonicOrder(), (layout == expected ? order : -1)); + } + + int max; +}; + +TEST_F (AudioChannelSetTest, MaxChannelsOfNamedLayoutIsNonDiscrete) +{ + EXPECT_GE (AudioChannelSet::channelSetsWithNumberOfChannels (max).size(), 2); +} + +TEST_F (AudioChannelSetTest, ChannelSetsWithNumberOfChannelsReturnsCorrectSpeakerCount) +{ + for (auto ch = 1; ch <= max; ++ch) + { + auto channelSets = AudioChannelSet::channelSetsWithNumberOfChannels (ch); + + for (auto set : channelSets) + EXPECT_EQ (set.size(), ch); + } +} + +TEST_F (AudioChannelSetTest, Ambisonics) +{ + uint64 mask = 0; + + mask |= (1ull << AudioChannelSet::ambisonicACN0); + checkAmbisonic (mask, 0, "0th Order Ambisonics"); + + mask |= (1ull << AudioChannelSet::ambisonicACN1) | (1ull << AudioChannelSet::ambisonicACN2) | (1ull << AudioChannelSet::ambisonicACN3); + checkAmbisonic (mask, 1, "1st Order Ambisonics"); + + mask |= (1ull << AudioChannelSet::ambisonicACN4) | (1ull << AudioChannelSet::ambisonicACN5) | (1ull << AudioChannelSet::ambisonicACN6) + | (1ull << AudioChannelSet::ambisonicACN7) | (1ull << AudioChannelSet::ambisonicACN8); + checkAmbisonic (mask, 2, "2nd Order Ambisonics"); + + mask |= (1ull << AudioChannelSet::ambisonicACN9) | (1ull << AudioChannelSet::ambisonicACN10) | (1ull << AudioChannelSet::ambisonicACN11) + | (1ull << AudioChannelSet::ambisonicACN12) | (1ull << AudioChannelSet::ambisonicACN13) | (1ull << AudioChannelSet::ambisonicACN14) + | (1ull << AudioChannelSet::ambisonicACN15); + checkAmbisonic (mask, 3, "3rd Order Ambisonics"); + + mask |= (1ull << AudioChannelSet::ambisonicACN16) | (1ull << AudioChannelSet::ambisonicACN17) | (1ull << AudioChannelSet::ambisonicACN18) + | (1ull << AudioChannelSet::ambisonicACN19) | (1ull << AudioChannelSet::ambisonicACN20) | (1ull << AudioChannelSet::ambisonicACN21) + | (1ull << AudioChannelSet::ambisonicACN22) | (1ull << AudioChannelSet::ambisonicACN23) | (1ull << AudioChannelSet::ambisonicACN24); + checkAmbisonic (mask, 4, "4th Order Ambisonics"); + + mask |= (1ull << AudioChannelSet::ambisonicACN25) | (1ull << AudioChannelSet::ambisonicACN26) | (1ull << AudioChannelSet::ambisonicACN27) + | (1ull << AudioChannelSet::ambisonicACN28) | (1ull << AudioChannelSet::ambisonicACN29) | (1ull << AudioChannelSet::ambisonicACN30) + | (1ull << AudioChannelSet::ambisonicACN31) | (1ull << AudioChannelSet::ambisonicACN32) | (1ull << AudioChannelSet::ambisonicACN33) + | (1ull << AudioChannelSet::ambisonicACN34) | (1ull << AudioChannelSet::ambisonicACN35); + checkAmbisonic (mask, 5, "5th Order Ambisonics"); +} diff --git a/tests/yup_audio_basics/yup_AudioDataConverters.cpp b/tests/yup_audio_basics/yup_AudioDataConverters.cpp new file mode 100644 index 000000000..a4cbe71a3 --- /dev/null +++ b/tests/yup_audio_basics/yup_AudioDataConverters.cpp @@ -0,0 +1,216 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +namespace +{ +template +void testRoundTripConversion (Random& r) +{ + auto testWithInPlace = [&] (bool inPlace) + { + const int numSamples = 2048; + int32 original[numSamples], converted[numSamples], reversed[numSamples]; + + { + AudioData::Pointer d (original); + bool clippingFailed = false; + + for (int i = 0; i < numSamples / 2; ++i) + { + d.setAsFloat (r.nextFloat() * 2.2f - 1.1f); + + if (! d.isFloatingPoint()) + clippingFailed = d.getAsFloat() > 1.0f || d.getAsFloat() < -1.0f || clippingFailed; + + ++d; + d.setAsInt32 (r.nextInt()); + ++d; + } + + EXPECT_FALSE (clippingFailed); + } + + // convert data from the source to dest format.. + std::unique_ptr conv (new AudioData::ConverterInstance, + AudioData::Pointer>()); + conv->convertSamples (inPlace ? reversed : converted, original, numSamples); + + // ..and back again.. + conv.reset (new AudioData::ConverterInstance, + AudioData::Pointer>()); + if (! inPlace) + zeromem (reversed, sizeof (reversed)); + + conv->convertSamples (reversed, inPlace ? reversed : converted, numSamples); + + { + int biggestDiff = 0; + AudioData::Pointer d1 (original); + AudioData::Pointer d2 (reversed); + + const int errorMargin = 2 * AudioData::Pointer::get32BitResolution() + + AudioData::Pointer::get32BitResolution(); + + for (int i = 0; i < numSamples; ++i) + { + biggestDiff = jmax (biggestDiff, std::abs (d1.getAsInt32() - d2.getAsInt32())); + ++d1; + ++d2; + } + + EXPECT_LE (biggestDiff, errorMargin); + } + }; + + testWithInPlace (false); + testWithInPlace (true); +} + +template +void testAllEndianness (Random& r) +{ + testRoundTripConversion (r); + testRoundTripConversion (r); +} + +template +void testAllFormats (Random& r) +{ + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); + testAllEndianness (r); +} + +template +void testFormatWithAllEndianness (Random& r) +{ + testAllFormats (r); + testAllFormats (r); +} +} // namespace + +class AudioDataConvertersTest : public ::testing::Test +{ +protected: + void SetUp() override + { + r.setSeed (12345); + } + + Random r; +}; + +TEST_F (AudioDataConvertersTest, RoundTripConversionInt8) +{ + testFormatWithAllEndianness (r); +} + +TEST_F (AudioDataConvertersTest, RoundTripConversionInt16) +{ + testFormatWithAllEndianness (r); +} + +TEST_F (AudioDataConvertersTest, RoundTripConversionInt24) +{ + testFormatWithAllEndianness (r); +} + +TEST_F (AudioDataConvertersTest, RoundTripConversionInt32) +{ + testFormatWithAllEndianness (r); +} + +TEST_F (AudioDataConvertersTest, RoundTripConversionFloat32) +{ + testFormatWithAllEndianness (r); +} + +TEST_F (AudioDataConvertersTest, Interleaving) +{ + using Format = AudioData::Format; + + constexpr auto numChannels = 4; + constexpr auto numSamples = 512; + + AudioBuffer sourceBuffer { numChannels, numSamples }, + destBuffer { 1, numChannels * numSamples }; + + for (int ch = 0; ch < numChannels; ++ch) + for (int i = 0; i < numSamples; ++i) + sourceBuffer.setSample (ch, i, r.nextFloat()); + + AudioData::interleaveSamples (AudioData::NonInterleavedSource { sourceBuffer.getArrayOfReadPointers(), numChannels }, + AudioData::InterleavedDest { destBuffer.getWritePointer (0), numChannels }, + numSamples); + + for (int ch = 0; ch < numChannels; ++ch) + for (int i = 0; i < numSamples; ++i) + EXPECT_EQ (destBuffer.getSample (0, ch + (i * numChannels)), sourceBuffer.getSample (ch, i)); +} + +TEST_F (AudioDataConvertersTest, Deinterleaving) +{ + using Format = AudioData::Format; + + constexpr auto numChannels = 4; + constexpr auto numSamples = 512; + + AudioBuffer sourceBuffer { 1, numChannels * numSamples }, + destBuffer { numChannels, numSamples }; + + for (int ch = 0; ch < numChannels; ++ch) + for (int i = 0; i < numSamples; ++i) + sourceBuffer.setSample (0, ch + (i * numChannels), r.nextFloat()); + + AudioData::deinterleaveSamples (AudioData::InterleavedSource { sourceBuffer.getReadPointer (0), numChannels }, + AudioData::NonInterleavedDest { destBuffer.getArrayOfWritePointers(), numChannels }, + numSamples); + + for (int ch = 0; ch < numChannels; ++ch) + for (int i = 0; i < numSamples; ++i) + EXPECT_EQ (sourceBuffer.getSample (0, ch + (i * numChannels)), destBuffer.getSample (ch, i)); +} diff --git a/modules/yup_audio_basics/utilities/yup_Interpolators.cpp b/tests/yup_audio_basics/yup_Interpolators.cpp similarity index 67% rename from modules/yup_audio_basics/utilities/yup_Interpolators.cpp rename to tests/yup_audio_basics/yup_Interpolators.cpp index 395d2d739..ab9e47156 100644 --- a/modules/yup_audio_basics/utilities/yup_Interpolators.cpp +++ b/tests/yup_audio_basics/yup_Interpolators.cpp @@ -1,203 +1,207 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup -{ - -#if YUP_UNIT_TESTS - -class InterpolatorTests final : public UnitTest -{ -public: - InterpolatorTests() - : UnitTest ("InterpolatorTests", UnitTestCategories::audio) - { - } - -private: - template - void runInterplatorTests (const String& interpolatorName) - { - auto createGaussian = [] (std::vector& destination, float scale, float centreInSamples, float width) - { - for (size_t i = 0; i < destination.size(); ++i) - { - auto x = (((float) i) - centreInSamples) * width; - destination[i] = std::exp (-(x * x)); - } - - FloatVectorOperations::multiply (destination.data(), scale, (int) destination.size()); - }; - - auto findGaussianPeak = [] (const std::vector& input) -> float - { - auto max = std::max_element (std::begin (input), std::end (input)); - auto maxPrev = max - 1; - jassert (maxPrev >= std::begin (input)); - auto maxNext = max + 1; - jassert (maxNext < std::end (input)); - auto quadraticMaxLoc = (*maxPrev - *maxNext) / (2.0f * ((*maxNext + *maxPrev) - (2.0f * *max))); - return quadraticMaxLoc + (float) std::distance (std::begin (input), max); - }; - - auto expectAllElementsWithin = [this] (const std::vector& v1, const std::vector& v2, float tolerance) - { - expectEquals ((int) v1.size(), (int) v2.size()); - - for (size_t i = 0; i < v1.size(); ++i) - expectWithinAbsoluteError (v1[i], v2[i], tolerance); - }; - - InterpolatorType interpolator; - - constexpr size_t inputSize = 1001; - static_assert (inputSize > 800 + InterpolatorType::getBaseLatency(), - "The test InterpolatorTests input buffer is too small"); - - std::vector input (inputSize); - constexpr auto inputGaussianMidpoint = (float) (inputSize - 1) / 2.0f; - constexpr auto inputGaussianValueAtEnds = 0.000001f; - const auto inputGaussianWidth = std::sqrt (-std::log (inputGaussianValueAtEnds)) / inputGaussianMidpoint; - - createGaussian (input, 1.0f, inputGaussianMidpoint, inputGaussianWidth); - - for (auto speedRatio : { 0.4, 0.8263, 1.0, 1.05, 1.2384, 1.6 }) - { - const auto expectedGaussianMidpoint = (inputGaussianMidpoint + InterpolatorType::getBaseLatency()) / (float) speedRatio; - const auto expectedGaussianWidth = inputGaussianWidth * (float) speedRatio; - - const auto outputBufferSize = (size_t) std::floor ((float) input.size() / speedRatio); - - for (int numBlocks : { 1, 5 }) - { - const auto inputBlockSize = (float) input.size() / (float) numBlocks; - const auto outputBlockSize = (int) std::floor (inputBlockSize / speedRatio); - - std::vector output (outputBufferSize, std::numeric_limits::min()); - - beginTest (interpolatorName + " process " + String (numBlocks) + " blocks ratio " + String (speedRatio)); - - interpolator.reset(); - - { - auto* inputPtr = input.data(); - auto* outputPtr = output.data(); - - for (int i = 0; i < numBlocks; ++i) - { - auto numInputSamplesRead = interpolator.process (speedRatio, inputPtr, outputPtr, outputBlockSize); - inputPtr += numInputSamplesRead; - outputPtr += outputBlockSize; - } - } - - expectWithinAbsoluteError (findGaussianPeak (output), expectedGaussianMidpoint, 0.1f); - - std::vector expectedOutput (output.size()); - createGaussian (expectedOutput, 1.0f, expectedGaussianMidpoint, expectedGaussianWidth); - - expectAllElementsWithin (output, expectedOutput, 0.02f); - - beginTest (interpolatorName + " process adding " + String (numBlocks) + " blocks ratio " + String (speedRatio)); - - interpolator.reset(); - - constexpr float addingGain = 0.7384f; - - { - auto* inputPtr = input.data(); - auto* outputPtr = output.data(); - - for (int i = 0; i < numBlocks; ++i) - { - auto numInputSamplesRead = interpolator.processAdding (speedRatio, inputPtr, outputPtr, outputBlockSize, addingGain); - inputPtr += numInputSamplesRead; - outputPtr += outputBlockSize; - } - } - - expectWithinAbsoluteError (findGaussianPeak (output), expectedGaussianMidpoint, 0.1f); - - std::vector additionalOutput (output.size()); - createGaussian (additionalOutput, addingGain, expectedGaussianMidpoint, expectedGaussianWidth); - FloatVectorOperations::add (expectedOutput.data(), additionalOutput.data(), (int) additionalOutput.size()); - - expectAllElementsWithin (output, expectedOutput, 0.02f); - } - - beginTest (interpolatorName + " process wrap 0 ratio " + String (speedRatio)); - - std::vector doubleLengthOutput (2 * outputBufferSize, std::numeric_limits::min()); - - interpolator.reset(); - interpolator.process (speedRatio, input.data(), doubleLengthOutput.data(), (int) doubleLengthOutput.size(), (int) input.size(), 0); - - std::vector expectedDoubleLengthOutput (doubleLengthOutput.size()); - createGaussian (expectedDoubleLengthOutput, 1.0f, expectedGaussianMidpoint, expectedGaussianWidth); - - expectAllElementsWithin (doubleLengthOutput, expectedDoubleLengthOutput, 0.02f); - - beginTest (interpolatorName + " process wrap double ratio " + String (speedRatio)); - - interpolator.reset(); - interpolator.process (speedRatio, input.data(), doubleLengthOutput.data(), (int) doubleLengthOutput.size(), (int) input.size(), (int) input.size()); - - std::vector secondGaussian (doubleLengthOutput.size()); - createGaussian (secondGaussian, 1.0f, expectedGaussianMidpoint + (float) outputBufferSize, expectedGaussianWidth); - FloatVectorOperations::add (expectedDoubleLengthOutput.data(), secondGaussian.data(), (int) expectedDoubleLengthOutput.size()); - - expectAllElementsWithin (doubleLengthOutput, expectedDoubleLengthOutput, 0.02f); - } - } - -public: - void runTest() override - { - runInterplatorTests ("WindowedSincInterpolator"); - runInterplatorTests ("LagrangeInterpolator"); - runInterplatorTests ("CatmullRomInterpolator"); - runInterplatorTests ("LinearInterpolator"); - } -}; - -static InterpolatorTests interpolatorTests; - -#endif - -} // namespace yup +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +void createGaussian (std::vector& destination, float scale, float centreInSamples, float width) +{ + for (size_t i = 0; i < destination.size(); ++i) + { + auto x = (((float) i) - centreInSamples) * width; + destination[i] = std::exp (-(x * x)); + } + + FloatVectorOperations::multiply (destination.data(), scale, (int) destination.size()); +} + +float findGaussianPeak (const std::vector& input) +{ + auto max = std::max_element (std::begin (input), std::end (input)); + auto maxPrev = max - 1; + jassert (maxPrev >= std::begin (input)); + auto maxNext = max + 1; + jassert (maxNext < std::end (input)); + auto quadraticMaxLoc = (*maxPrev - *maxNext) / (2.0f * ((*maxNext + *maxPrev) - (2.0f * *max))); + return quadraticMaxLoc + (float) std::distance (std::begin (input), max); +} + +void expectAllElementsWithin (const std::vector& v1, const std::vector& v2, float tolerance) +{ + EXPECT_EQ ((int) v1.size(), (int) v2.size()); + + for (size_t i = 0; i < v1.size(); ++i) + EXPECT_NEAR (v1[i], v2[i], tolerance); +} +} // namespace + +template +class InterpolatorTests : public ::testing::Test +{ +protected: + void runInterpolatorTests (const String& interpolatorName) + { + InterpolatorType interpolator; + + constexpr size_t inputSize = 1001; + static_assert (inputSize > 800 + InterpolatorType::getBaseLatency(), + "The test InterpolatorTests input buffer is too small"); + + std::vector input (inputSize); + constexpr auto inputGaussianMidpoint = (float) (inputSize - 1) / 2.0f; + constexpr auto inputGaussianValueAtEnds = 0.000001f; + const auto inputGaussianWidth = std::sqrt (-std::log (inputGaussianValueAtEnds)) / inputGaussianMidpoint; + + createGaussian (input, 1.0f, inputGaussianMidpoint, inputGaussianWidth); + + for (auto speedRatio : { 0.4, 0.8263, 1.0, 1.05, 1.2384, 1.6 }) + { + const auto expectedGaussianMidpoint = (inputGaussianMidpoint + InterpolatorType::getBaseLatency()) / (float) speedRatio; + const auto expectedGaussianWidth = inputGaussianWidth * (float) speedRatio; + + const auto outputBufferSize = (size_t) std::floor ((float) input.size() / speedRatio); + + for (int numBlocks : { 1, 5 }) + { + const auto inputBlockSize = (float) input.size() / (float) numBlocks; + const auto outputBlockSize = (int) std::floor (inputBlockSize / speedRatio); + + std::vector output (outputBufferSize, std::numeric_limits::min()); + + interpolator.reset(); + + { + auto* inputPtr = input.data(); + auto* outputPtr = output.data(); + + for (int i = 0; i < numBlocks; ++i) + { + auto numInputSamplesRead = interpolator.process (speedRatio, inputPtr, outputPtr, outputBlockSize); + inputPtr += numInputSamplesRead; + outputPtr += outputBlockSize; + } + } + + EXPECT_NEAR (findGaussianPeak (output), expectedGaussianMidpoint, 0.1f); + + std::vector expectedOutput (output.size()); + createGaussian (expectedOutput, 1.0f, expectedGaussianMidpoint, expectedGaussianWidth); + + expectAllElementsWithin (output, expectedOutput, 0.02f); + + // Test process adding + interpolator.reset(); + + constexpr float addingGain = 0.7384f; + + { + auto* inputPtr = input.data(); + auto* outputPtr = output.data(); + + for (int i = 0; i < numBlocks; ++i) + { + auto numInputSamplesRead = interpolator.processAdding (speedRatio, inputPtr, outputPtr, outputBlockSize, addingGain); + inputPtr += numInputSamplesRead; + outputPtr += outputBlockSize; + } + } + + EXPECT_NEAR (findGaussianPeak (output), expectedGaussianMidpoint, 0.1f); + + std::vector additionalOutput (output.size()); + createGaussian (additionalOutput, addingGain, expectedGaussianMidpoint, expectedGaussianWidth); + FloatVectorOperations::add (expectedOutput.data(), additionalOutput.data(), (int) additionalOutput.size()); + + expectAllElementsWithin (output, expectedOutput, 0.02f); + } + + // Test process wrap 0 + std::vector doubleLengthOutput (2 * outputBufferSize, std::numeric_limits::min()); + + interpolator.reset(); + interpolator.process (speedRatio, input.data(), doubleLengthOutput.data(), (int) doubleLengthOutput.size(), (int) input.size(), 0); + + std::vector expectedDoubleLengthOutput (doubleLengthOutput.size()); + createGaussian (expectedDoubleLengthOutput, 1.0f, expectedGaussianMidpoint, expectedGaussianWidth); + + expectAllElementsWithin (doubleLengthOutput, expectedDoubleLengthOutput, 0.02f); + + // Test process wrap double + interpolator.reset(); + interpolator.process (speedRatio, input.data(), doubleLengthOutput.data(), (int) doubleLengthOutput.size(), (int) input.size(), (int) input.size()); + + std::vector secondGaussian (doubleLengthOutput.size()); + createGaussian (secondGaussian, 1.0f, expectedGaussianMidpoint + (float) outputBufferSize, expectedGaussianWidth); + FloatVectorOperations::add (expectedDoubleLengthOutput.data(), secondGaussian.data(), (int) expectedDoubleLengthOutput.size()); + + expectAllElementsWithin (doubleLengthOutput, expectedDoubleLengthOutput, 0.02f); + } + } +}; + +// Test fixture for WindowedSincInterpolator +using WindowedSincInterpolatorTests = InterpolatorTests; +using LagrangeInterpolatorTests = InterpolatorTests; +using CatmullRomInterpolatorTests = InterpolatorTests; +using LinearInterpolatorTests = InterpolatorTests; + +TEST_F (WindowedSincInterpolatorTests, ProcessingTests) +{ + runInterpolatorTests ("WindowedSincInterpolator"); +} + +TEST_F (LagrangeInterpolatorTests, ProcessingTests) +{ + runInterpolatorTests ("LagrangeInterpolator"); +} + +TEST_F (CatmullRomInterpolatorTests, ProcessingTests) +{ + runInterpolatorTests ("CatmullRomInterpolator"); +} + +TEST_F (LinearInterpolatorTests, ProcessingTests) +{ + runInterpolatorTests ("LinearInterpolator"); +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_MPEInstrument.cpp b/tests/yup_audio_basics/yup_MPEInstrument.cpp new file mode 100644 index 000000000..04d4c280d --- /dev/null +++ b/tests/yup_audio_basics/yup_MPEInstrument.cpp @@ -0,0 +1,256 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +class InstrumentListener : public MPEInstrument::Listener +{ +public: + void noteAdded (MPENote newNote) override + { + lastNoteAdded = newNote; + noteAddedCallCounter++; + } + + void notePressureChanged (MPENote changedNote) override + { + lastNotePressureChanged = changedNote; + } + + void notePitchbendChanged (MPENote changedNote) override + { + lastNotePitchbendChanged = changedNote; + } + + void noteTimbreChanged (MPENote changedNote) override + { + lastNoteTimbreChanged = changedNote; + } + + void noteKeyStateChanged (MPENote changedNote) override + { + lastNoteKeyStateChanged = changedNote; + } + + void noteReleased (MPENote finishedNote) override + { + lastNoteReleased = finishedNote; + noteReleasedCallCounter++; + } + + MPENote lastNoteAdded, lastNotePressureChanged, lastNotePitchbendChanged, + lastNoteTimbreChanged, lastNoteKeyStateChanged, lastNoteReleased; + int noteAddedCallCounter = 0, noteReleasedCallCounter = 0; +}; + +void expectNote (MPENote note, int initialNote, int totalPitchbendInSemitones, int pitchbendInMPEUnits, int timbre, MPENote::KeyState keyState) +{ + EXPECT_EQ (note.initialNote, initialNote); + EXPECT_EQ (note.totalPitchbendInSemitones, totalPitchbendInSemitones); + EXPECT_EQ (note.pitchbend.as14BitInt(), pitchbendInMPEUnits); + EXPECT_EQ (note.timbre.as7BitInt(), timbre); + EXPECT_EQ (note.keyState, keyState); +} + +} // namespace + +class MPEInstrumentTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // using lower and upper MPE zones with the following layout for testing + // + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + // * ...................| |........................ * + testLayout.setLowerZone (5); + testLayout.setUpperZone (6); + } + + MPEZoneLayout testLayout; +}; + +TEST_F (MPEInstrumentTest, InitialZoneLayout) +{ + MPEInstrument test; + EXPECT_FALSE (test.getZoneLayout().getLowerZone().isActive()); + EXPECT_FALSE (test.getZoneLayout().getUpperZone().isActive()); +} + +TEST_F (MPEInstrumentTest, GetSetZoneLayout) +{ + MPEInstrument test; + test.setZoneLayout (testLayout); + + auto newLayout = test.getZoneLayout(); + + EXPECT_TRUE (test.getZoneLayout().getLowerZone().isActive()); + EXPECT_TRUE (test.getZoneLayout().getUpperZone().isActive()); + EXPECT_EQ (newLayout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (newLayout.getLowerZone().numMemberChannels, 5); + EXPECT_EQ (newLayout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (newLayout.getUpperZone().numMemberChannels, 6); +} + +TEST_F (MPEInstrumentTest, NoteOnNoteOff) +{ + { + MPEInstrument test; + test.setZoneLayout (testLayout); + EXPECT_EQ (test.getNumPlayingNotes(), 0); + } + + { + MPEInstrument test; + test.setZoneLayout (testLayout); + + InstrumentListener listener; + test.addListener (&listener); + + // note-on on unused channel - ignore + test.noteOn (7, 60, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNumPlayingNotes(), 0); + EXPECT_EQ (listener.noteAddedCallCounter, 0); + + // note-on on member channel - create new note + test.noteOn (3, 60, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNumPlayingNotes(), 1); + EXPECT_EQ (listener.noteAddedCallCounter, 1); + + auto note1 = test.getNote (3, 60); + EXPECT_EQ (note1.initialNote, 60); + EXPECT_EQ (note1.totalPitchbendInSemitones, 0); + EXPECT_EQ (note1.pitchbend.as14BitInt(), 8192); + EXPECT_EQ (note1.timbre.as7BitInt(), 64); + EXPECT_EQ (note1.keyState, MPENote::keyDown); + + // note-off + test.noteOff (3, 60, MPEValue::from7BitInt (33)); + EXPECT_EQ (test.getNumPlayingNotes(), 0); + EXPECT_EQ (listener.noteReleasedCallCounter, 1); + + // note-on on master channel - create new note + test.noteOn (1, 62, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNumPlayingNotes(), 1); + EXPECT_EQ (listener.noteAddedCallCounter, 2); + + /* + auto note2 = test.getNote(1, 62); + EXPECT_EQ(note1.initialNote, 62); + EXPECT_EQ(note1.totalPitchbendInSemitones, 0); + EXPECT_EQ(note1.pitchbend.as14BitInt(), 8192); + EXPECT_EQ(note1.timbre.as7BitInt(), 64); + EXPECT_EQ(note1.keyState, MPENote::keyDown); + */ + + // note-off + test.noteOff (1, 62, MPEValue::from7BitInt (33)); + EXPECT_EQ (test.getNumPlayingNotes(), 0); + EXPECT_EQ (listener.noteReleasedCallCounter, 2); + } +} + +TEST_F (MPEInstrumentTest, NoteOffIgnoresNonMatchingNotes) +{ + MPEInstrument test; + test.setZoneLayout (testLayout); + test.noteOn (3, 60, MPEValue::from7BitInt (100)); + + InstrumentListener listener; + test.addListener (&listener); + + // note off with non-matching note number shouldn't do anything + test.noteOff (3, 61, MPEValue::from7BitInt (33)); + EXPECT_EQ (test.getNumPlayingNotes(), 1); + //expectNote(test.getNote(3, 60), 100, 0, 8192, 64, MPENote::keyDown); + EXPECT_EQ (listener.noteReleasedCallCounter, 0); + + // note off with non-matching midi channel shouldn't do anything + test.noteOff (2, 60, MPEValue::from7BitInt (33)); + EXPECT_EQ (test.getNumPlayingNotes(), 1); + //expectNote(test.getNote(3, 60), 100, 0, 8192, 64, MPENote::keyDown); + EXPECT_EQ (listener.noteReleasedCallCounter, 0); +} + +TEST_F (MPEInstrumentTest, PitchbendChangeModifiesCorrectNote) +{ + MPEInstrument test; + test.setZoneLayout (testLayout); + + test.noteOn (3, 60, MPEValue::from7BitInt (100)); + test.noteOn (4, 61, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNumPlayingNotes(), 2); + + test.pitchbend (4, MPEValue::from14BitInt (9000)); + EXPECT_EQ (test.getNote (3, 60).pitchbend.as14BitInt(), 8192); + EXPECT_EQ (test.getNote (4, 61).pitchbend.as14BitInt(), 9000); +} + +TEST_F (MPEInstrumentTest, PressureChangeModifiesCorrectNote) +{ + MPEInstrument test; + test.setZoneLayout (testLayout); + + test.noteOn (3, 60, MPEValue::from7BitInt (100)); + test.noteOn (4, 61, MPEValue::from7BitInt (100)); + + test.pressure (4, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNote (3, 60).pressure.as7BitInt(), 0); + EXPECT_EQ (test.getNote (4, 61).pressure.as7BitInt(), 100); +} + +TEST_F (MPEInstrumentTest, TimbreChangeModifiesCorrectNote) +{ + MPEInstrument test; + test.setZoneLayout (testLayout); + + test.noteOn (3, 60, MPEValue::from7BitInt (100)); + test.noteOn (4, 61, MPEValue::from7BitInt (100)); + + test.timbre (4, MPEValue::from7BitInt (100)); + EXPECT_EQ (test.getNote (3, 60).timbre.as7BitInt(), 64); + EXPECT_EQ (test.getNote (4, 61).timbre.as7BitInt(), 100); +} diff --git a/tests/yup_audio_basics/yup_MPEMessages.cpp b/tests/yup_audio_basics/yup_MPEMessages.cpp new file mode 100644 index 000000000..7e8602fa7 --- /dev/null +++ b/tests/yup_audio_basics/yup_MPEMessages.cpp @@ -0,0 +1,255 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +namespace +{ +void extractRawBinaryData (const MidiBuffer& midiBuffer, const uint8* bufferToCopyTo, std::size_t maxBytes) +{ + std::size_t pos = 0; + + for (const auto metadata : midiBuffer) + { + const uint8* data = metadata.data; + std::size_t dataSize = (std::size_t) metadata.numBytes; + + if (pos + dataSize > maxBytes) + return; + + std::memcpy ((void*) (bufferToCopyTo + pos), data, dataSize); + pos += dataSize; + } +} + +void testMidiBuffer (MidiBuffer& buffer, const uint8* expectedBytes, int expectedBytesSize) +{ + uint8 actualBytes[128] = { 0 }; + extractRawBinaryData (buffer, actualBytes, sizeof (actualBytes)); + + EXPECT_EQ (std::memcmp (actualBytes, expectedBytes, (std::size_t) expectedBytesSize), 0); +} +} // namespace + +class MPEMessagesTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Setup common test data if needed + } +}; + +TEST_F (MPEMessagesTest, AddZoneLower) +{ + MidiBuffer buffer = MPEMessages::setLowerZone (7); + + const uint8 expectedBytes[] = { + 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone + 0xb1, + 0x64, + 0x00, + 0xb1, + 0x65, + 0x00, + 0xb1, + 0x06, + 0x30, // per-note pbrange (default = 48) + 0xb0, + 0x64, + 0x00, + 0xb0, + 0x65, + 0x00, + 0xb0, + 0x06, + 0x02 // master pbrange (default = 2) + }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} + +TEST_F (MPEMessagesTest, AddZoneUpper) +{ + MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0); + + const uint8 expectedBytes[] = { + 0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone + 0xbe, + 0x64, + 0x00, + 0xbe, + 0x65, + 0x00, + 0xbe, + 0x06, + 0x60, // per-note pbrange (custom) + 0xbf, + 0x64, + 0x00, + 0xbf, + 0x65, + 0x00, + 0xbf, + 0x06, + 0x00 // master pbrange (custom) + }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} + +TEST_F (MPEMessagesTest, SetPerNotePitchbendRange) +{ + MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96); + + const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} + +TEST_F (MPEMessagesTest, SetMasterPitchbendRange) +{ + MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60); + + const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} + +TEST_F (MPEMessagesTest, ClearAllZones) +{ + MidiBuffer buffer = MPEMessages::clearAllZones(); + + const uint8 expectedBytes[] = { + 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone + 0xbf, + 0x64, + 0x06, + 0xbf, + 0x65, + 0x00, + 0xbf, + 0x06, + 0x00 // clear upper zone + }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} + +TEST_F (MPEMessagesTest, SetCompleteState) +{ + MPEZoneLayout layout; + + layout.setLowerZone (7, 96, 0); + layout.setUpperZone (7); + + MidiBuffer buffer = MPEMessages::setZoneLayout (layout); + + const uint8 expectedBytes[] = { + 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone + 0xbf, + 0x64, + 0x06, + 0xbf, + 0x65, + 0x00, + 0xbf, + 0x06, + 0x00, // clear upper zone + 0xb0, + 0x64, + 0x06, + 0xb0, + 0x65, + 0x00, + 0xb0, + 0x06, + 0x07, // set lower zone + 0xb1, + 0x64, + 0x00, + 0xb1, + 0x65, + 0x00, + 0xb1, + 0x06, + 0x60, // per-note pbrange (custom) + 0xb0, + 0x64, + 0x00, + 0xb0, + 0x65, + 0x00, + 0xb0, + 0x06, + 0x00, // master pbrange (custom) + 0xbf, + 0x64, + 0x06, + 0xbf, + 0x65, + 0x00, + 0xbf, + 0x06, + 0x07, // set upper zone + 0xbe, + 0x64, + 0x00, + 0xbe, + 0x65, + 0x00, + 0xbe, + 0x06, + 0x30, // per-note pbrange (default = 48) + 0xbf, + 0x64, + 0x00, + 0xbf, + 0x65, + 0x00, + 0xbf, + 0x06, + 0x02 // master pbrange (default = 2) + }; + + testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes)); +} diff --git a/modules/yup_core/unit_tests/yup_UnitTestCategories.h b/tests/yup_audio_basics/yup_MPENote.cpp similarity index 56% rename from modules/yup_core/unit_tests/yup_UnitTestCategories.h rename to tests/yup_audio_basics/yup_MPENote.cpp index cf30b41d1..d0f815161 100644 --- a/modules/yup_core/unit_tests/yup_UnitTestCategories.h +++ b/tests/yup_audio_basics/yup_MPENote.cpp @@ -1,70 +1,64 @@ -/* - ============================================================================== - - This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com - - YUP is an open source library subject to open-source licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - to use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== - - This file is part of the JUCE library. - Copyright (c) 2022 - Raw Material Software Limited - - JUCE is an open source library subject to commercial or open-source - licensing. - - The code included in this file is provided under the terms of the ISC license - http://www.isc.org/downloads/software-support-policy/isc-license. Permission - To use, copy, modify, and/or distribute this software for any purpose with or - without fee is hereby granted provided that the above copyright notice and - this permission notice appear in all copies. - - JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER - EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE - DISCLAIMED. - - ============================================================================== -*/ - -namespace yup::UnitTestCategories -{ - -static const String analytics { "Analytics" }; -static const String audio { "Audio" }; -static const String audioProcessorParameters { "AudioProcessorParameters" }; -static const String audioProcessors { "AudioProcessors" }; -static const String blocks { "Blocks" }; -static const String compression { "Compression" }; -static const String containers { "Containers" }; -static const String cryptography { "Cryptography" }; -static const String dsp { "DSP" }; -static const String files { "Files" }; -static const String graphics { "Graphics" }; -static const String gui { "GUI" }; -static const String json { "JSON" }; -static const String maths { "Maths" }; -static const String memory { "Memory" }; -static const String midi { "MIDI" }; -static const String native { "Native" }; -static const String networking { "Networking" }; -static const String osc { "OSC" }; -static const String smoothedValues { "SmoothedValues" }; -static const String streams { "Streams" }; -static const String text { "Text" }; -static const String threads { "Threads" }; -static const String time { "Time" }; -static const String values { "Values" }; -static const String xml { "XML" }; - -} // namespace yup::UnitTestCategories +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +void expectEqualsWithinOneCent (double frequencyInHertzActual, + double frequencyInHertzExpected) +{ + double ratio = frequencyInHertzActual / frequencyInHertzExpected; + double oneCent = 1.0005946; + EXPECT_LT (ratio, oneCent); + EXPECT_GT (ratio, 1.0 / oneCent); +} +} // namespace + +TEST (MPENoteTests, GetFrequencyInHertz) +{ + MPENote note; + note.initialNote = 60; + note.totalPitchbendInSemitones = -0.5; + expectEqualsWithinOneCent (note.getFrequencyInHertz(), 254.178); +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_MPESynthesiserBase.cpp b/tests/yup_audio_basics/yup_MPESynthesiserBase.cpp new file mode 100644 index 000000000..f8ec142e9 --- /dev/null +++ b/tests/yup_audio_basics/yup_MPESynthesiserBase.cpp @@ -0,0 +1,265 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +namespace +{ +enum class CallbackKind +{ + process, + midi +}; + +struct StartAndLength +{ + StartAndLength (int s, int l) + : start (s) + , length (l) + { + } + + int start = 0; + int length = 0; + + std::tuple tie() const noexcept { return std::tie (start, length); } + + bool operator== (const StartAndLength& other) const noexcept { return tie() == other.tie(); } + + bool operator!= (const StartAndLength& other) const noexcept { return tie() != other.tie(); } + + bool operator< (const StartAndLength& other) const noexcept { return tie() < other.tie(); } +}; + +struct Events +{ + std::vector blocks; + std::vector messages; + std::vector order; +}; + +class MockSynthesiser final : public MPESynthesiserBase +{ +public: + Events events; + + void handleMidiEvent (const MidiMessage& m) override + { + events.messages.emplace_back (m); + events.order.emplace_back (CallbackKind::midi); + } + +private: + using MPESynthesiserBase::renderNextSubBlock; + + void renderNextSubBlock (AudioBuffer&, + int startSample, + int numSamples) override + { + events.blocks.push_back ({ startSample, numSamples }); + events.order.emplace_back (CallbackKind::process); + } +}; + +MidiBuffer makeTestBuffer (const int bufferLength) +{ + MidiBuffer result; + + for (int i = 0; i != bufferLength; ++i) + result.addEvent ({}, i); + + return result; +} + +int sumBlockLengths (const std::vector& b) +{ + const auto addBlock = [] (int acc, const StartAndLength& info) + { + return acc + info.length; + }; + return std::accumulate (b.begin(), b.end(), 0, addBlock); +} + +bool blockLengthsAreValid (const std::vector& info, int minLength, bool strict) +{ + if (info.size() <= 1) + return true; + + const auto lengthIsValid = [&] (const StartAndLength& s) + { + return minLength <= s.length; + }; + const auto begin = strict ? info.begin() : std::next (info.begin()); + // The final block is allowed to be shorter than the minLength + return std::all_of (begin, std::prev (info.end()), lengthIsValid); +} +} // namespace + +class MPESynthesiserBaseTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Setup common test data if needed + } +}; + +TEST_F (MPESynthesiserBaseTest, RenderingSparseSubblocksWorks) +{ + const int blockSize = 512; + const auto midi = [&] + { + MidiBuffer b; + b.addEvent ({}, blockSize / 2); + return b; + }(); + AudioBuffer audio (1, blockSize); + + const auto processEvents = [&] (int start, int length) + { + MockSynthesiser synth; + synth.setMinimumRenderingSubdivisionSize (1, false); + synth.setCurrentPlaybackSampleRate (44100); + synth.renderNextBlock (audio, midi, start, length); + return synth.events; + }; + + { + const auto e = processEvents (0, blockSize); + EXPECT_EQ (e.blocks.size(), 2u); + EXPECT_EQ (e.messages.size(), 1u); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), blockSize); + EXPECT_EQ (e.order, (std::vector { CallbackKind::process, CallbackKind::midi, CallbackKind::process })); + } +} + +TEST_F (MPESynthesiserBaseTest, RenderingSubblocksProcessesOnlyContainedMidiEvents) +{ + const int blockSize = 512; + const auto midi = makeTestBuffer (blockSize); + AudioBuffer audio (1, blockSize); + + const auto processEvents = [&] (int start, int length) + { + MockSynthesiser synth; + synth.setMinimumRenderingSubdivisionSize (1, false); + synth.setCurrentPlaybackSampleRate (44100); + synth.renderNextBlock (audio, midi, start, length); + return synth.events; + }; + + { + const int subBlockLength = 0; + const auto e = processEvents (0, subBlockLength); + EXPECT_EQ (e.blocks.size(), 0u); + EXPECT_EQ (e.messages.size(), 0u); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), subBlockLength); + } + + { + const int subBlockLength = 0; + const auto e = processEvents (1, subBlockLength); + EXPECT_EQ (e.blocks.size(), 0u); + EXPECT_EQ (e.messages.size(), 0u); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), subBlockLength); + } + + { + const int subBlockLength = 1; + const auto e = processEvents (1, subBlockLength); + EXPECT_EQ (e.blocks.size(), 1u); + EXPECT_EQ (e.messages.size(), 1u); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), subBlockLength); + EXPECT_EQ (e.order, (std::vector { CallbackKind::midi, CallbackKind::process })); + } + + { + const auto e = processEvents (0, blockSize); + EXPECT_EQ (e.blocks.size(), blockSize); + EXPECT_EQ (e.messages.size(), blockSize); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), blockSize); + EXPECT_EQ (e.order.front(), CallbackKind::midi); + } +} + +TEST_F (MPESynthesiserBaseTest, SubblocksRespectTheirMinimumSize) +{ + const int blockSize = 512; + const auto midi = makeTestBuffer (blockSize); + AudioBuffer audio (1, blockSize); + + for (auto strict : { false, true }) + { + for (auto subblockSize : { 1, 16, 32, 64, 1024 }) + { + MockSynthesiser synth; + synth.setMinimumRenderingSubdivisionSize (subblockSize, strict); + synth.setCurrentPlaybackSampleRate (44100); + synth.renderNextBlock (audio, midi, 0, blockSize); + + const auto& e = synth.events; + EXPECT_NEAR (float (e.blocks.size()), + std::ceil ((float) blockSize / (float) subblockSize), + 1.0f); + EXPECT_EQ (e.messages.size(), blockSize); + EXPECT_TRUE (std::is_sorted (e.blocks.begin(), e.blocks.end())); + EXPECT_EQ (sumBlockLengths (e.blocks), blockSize); + EXPECT_TRUE (blockLengthsAreValid (e.blocks, subblockSize, strict)); + } + } + + { + MockSynthesiser synth; + synth.setMinimumRenderingSubdivisionSize (32, true); + synth.setCurrentPlaybackSampleRate (44100); + synth.renderNextBlock (audio, MidiBuffer {}, 0, 16); + + EXPECT_EQ (synth.events.blocks, (std::vector { { 0, 16 } })); + EXPECT_EQ (synth.events.order, (std::vector { CallbackKind::process })); + EXPECT_TRUE (synth.events.messages.empty()); + } +} diff --git a/tests/yup_audio_basics/yup_MPEUtils.cpp b/tests/yup_audio_basics/yup_MPEUtils.cpp new file mode 100644 index 000000000..972356c7a --- /dev/null +++ b/tests/yup_audio_basics/yup_MPEUtils.cpp @@ -0,0 +1,299 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +TEST (MPEUtilsTests, MPEChannelAssignerLowerZone) +{ + MPEZoneLayout layout; + layout.setLowerZone (15); + + // lower zone + MPEChannelAssigner channelAssigner (layout.getLowerZone()); + + // check that channels are assigned in correct order + int noteNum = 60; + for (int ch = 2; ch <= 16; ++ch) + { + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (noteNum), ch); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); + + ++noteNum; + } + + // check that note-offs are processed + channelAssigner.noteOff (60); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (60), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (60), 2); + + channelAssigner.noteOff (61); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (61), 3); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (61), 3); + + // check that assigned channel was last to play note + channelAssigner.noteOff (65); + channelAssigner.noteOff (66); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 8); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 8); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 7); + + // find closest channel playing nonequal note + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 2); + + // all notes off + channelAssigner.allNotesOff(); + + // last note played + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 8); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 8); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 2); + + // normal assignment + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (101), 3); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (20), 4); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (101), 3); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (20), 4); +} + +TEST (MPEUtilsTests, MPEChannelAssignerUpperZone) +{ + MPEZoneLayout layout; + layout.setUpperZone (15); + + // upper zone + MPEChannelAssigner channelAssigner (layout.getUpperZone()); + + // check that channels are assigned in correct order + int noteNum = 60; + for (int ch = 15; ch >= 1; --ch) + { + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (noteNum), ch); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); + + ++noteNum; + } + + // check that note-offs are processed + channelAssigner.noteOff (60); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (60), 15); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (60), 15); + + channelAssigner.noteOff (61); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (61), 14); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (61), 14); + + // check that assigned channel was last to play note + channelAssigner.noteOff (65); + channelAssigner.noteOff (66); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 9); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 10); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 9); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 10); + + // find closest channel playing nonequal note + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 15); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 15); + + // all notes off + channelAssigner.allNotesOff(); + + // last note played + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 9); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 10); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 15); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 9); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 10); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 15); + + // normal assignment + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (101), 14); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (20), 13); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (101), 14); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (20), 13); +} + +TEST (MPEUtilsTests, MPEChannelAssignerLegacy) +{ + MPEChannelAssigner channelAssigner; + + // check that channels are assigned in correct order + int noteNum = 60; + for (int ch = 1; ch <= 16; ++ch) + { + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (noteNum), ch); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (noteNum), ch); + + ++noteNum; + } + + // check that note-offs are processed + channelAssigner.noteOff (60); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (60), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (60), 1); + + channelAssigner.noteOff (61); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (61), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (61), 2); + + // check that assigned channel was last to play note + channelAssigner.noteOff (65); + channelAssigner.noteOff (66); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 6); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 6); + + // find closest channel playing nonequal note + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 1); + + // all notes off + channelAssigner.allNotesOff(); + + // last note played + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (66), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (65), 6); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (55), 1); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (66), 7); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (65), 6); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (80), 16); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (55), 1); + + // normal assignment + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (101), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForNewNote (20), 3); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (101), 2); + EXPECT_EQ (channelAssigner.findMidiChannelForExistingNote (20), 3); +} + +TEST (MPEUtilsTests, MPEChannelRemapperLowerZone) +{ + // 3 different MPE 'sources', constant IDs + const int sourceID1 = 0; + const int sourceID2 = 1; + const int sourceID3 = 2; + + MPEZoneLayout layout; + layout.setLowerZone (15); + + // lower zone + MPEChannelRemapper channelRemapper (layout.getLowerZone()); + + // first source, shouldn't remap + for (int ch = 2; ch <= 16; ++ch) + { + auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f); + + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1); + EXPECT_EQ (noteOn.getChannel(), ch); + } + + auto noteOn = MidiMessage::noteOn (2, 60, 1.0f); + + // remap onto oldest last-used channel + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2); + EXPECT_EQ (noteOn.getChannel(), 2); + + // remap onto oldest last-used channel + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3); + EXPECT_EQ (noteOn.getChannel(), 3); + + // remap to correct channel for source ID + auto noteOff = MidiMessage::noteOff (2, 60, 1.0f); + channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3); + EXPECT_EQ (noteOff.getChannel(), 3); +} + +TEST (MPEUtilsTests, MPEChannelRemapperUpperZone) +{ + // 3 different MPE 'sources', constant IDs + const int sourceID1 = 0; + const int sourceID2 = 1; + const int sourceID3 = 2; + + MPEZoneLayout layout; + layout.setUpperZone (15); + + // upper zone + MPEChannelRemapper channelRemapper (layout.getUpperZone()); + + // first source, shouldn't remap + for (int ch = 15; ch >= 1; --ch) + { + auto noteOn = MidiMessage::noteOn (ch, 60, 1.0f); + + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID1); + EXPECT_EQ (noteOn.getChannel(), ch); + } + + auto noteOn = MidiMessage::noteOn (15, 60, 1.0f); + + // remap onto oldest last-used channel + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID2); + EXPECT_EQ (noteOn.getChannel(), 15); + + // remap onto oldest last-used channel + channelRemapper.remapMidiChannelIfNeeded (noteOn, sourceID3); + EXPECT_EQ (noteOn.getChannel(), 14); + + // remap to correct channel for source ID + auto noteOff = MidiMessage::noteOff (15, 60, 1.0f); + channelRemapper.remapMidiChannelIfNeeded (noteOff, sourceID3); + EXPECT_EQ (noteOff.getChannel(), 14); +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_MPEValue.cpp b/tests/yup_audio_basics/yup_MPEValue.cpp new file mode 100644 index 000000000..10d915a0c --- /dev/null +++ b/tests/yup_audio_basics/yup_MPEValue.cpp @@ -0,0 +1,119 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +void expectValuesConsistent (MPEValue value, + int expectedValueAs7BitInt, + int expectedValueAs14BitInt, + float expectedValueAsSignedFloat, + float expectedValueAsUnsignedFloat) +{ + EXPECT_EQ (value.as7BitInt(), expectedValueAs7BitInt); + EXPECT_EQ (value.as14BitInt(), expectedValueAs14BitInt); + + const float maxRelativeError = 0.0001f; + const float maxAbsoluteErrorSigned = jmax (1.0f, std::abs (expectedValueAsSignedFloat)) * maxRelativeError; + const float maxAbsoluteErrorUnsigned = jmax (1.0f, std::abs (expectedValueAsUnsignedFloat)) * maxRelativeError; + + EXPECT_LT (std::abs (expectedValueAsSignedFloat - value.asSignedFloat()), maxAbsoluteErrorSigned); + EXPECT_LT (std::abs (expectedValueAsUnsignedFloat - value.asUnsignedFloat()), maxAbsoluteErrorUnsigned); +} +} // namespace + +TEST (MPEValueTests, ComparisonOperator) +{ + MPEValue value1 = MPEValue::from7BitInt (7); + MPEValue value2 = MPEValue::from7BitInt (7); + MPEValue value3 = MPEValue::from7BitInt (8); + + EXPECT_TRUE (value1 == value1); + EXPECT_TRUE (value1 == value2); + EXPECT_TRUE (value1 != value3); +} + +TEST (MPEValueTests, SpecialValues) +{ + EXPECT_EQ (MPEValue::minValue().as7BitInt(), 0); + EXPECT_EQ (MPEValue::minValue().as14BitInt(), 0); + + EXPECT_EQ (MPEValue::centreValue().as7BitInt(), 64); + EXPECT_EQ (MPEValue::centreValue().as14BitInt(), 8192); + + EXPECT_EQ (MPEValue::maxValue().as7BitInt(), 127); + EXPECT_EQ (MPEValue::maxValue().as14BitInt(), 16383); +} + +TEST (MPEValueTests, ZeroMinimumValue) +{ + expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f); + expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f); + expectValuesConsistent (MPEValue::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f); + expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f); +} + +TEST (MPEValueTests, MaximumValue) +{ + expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f); + expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f); + expectValuesConsistent (MPEValue::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f); + expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f); +} + +TEST (MPEValueTests, CentreValue) +{ + expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f); + expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f); + expectValuesConsistent (MPEValue::fromUnsignedFloat (0.5f), 64, 8192, 0.0f, 0.5f); + expectValuesConsistent (MPEValue::fromSignedFloat (0.0f), 64, 8192, 0.0f, 0.5f); +} + +TEST (MPEValueTests, ValueHalfwayBetweenMinAndCentre) +{ + expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f); + expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f); + expectValuesConsistent (MPEValue::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f); + expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f); +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_MPEZoneLayout.cpp b/tests/yup_audio_basics/yup_MPEZoneLayout.cpp new file mode 100644 index 000000000..fe7380098 --- /dev/null +++ b/tests/yup_audio_basics/yup_MPEZoneLayout.cpp @@ -0,0 +1,223 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +class MPEZoneLayoutTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Setup common test data if needed + } +}; + +TEST_F (MPEZoneLayoutTest, Initialisation) +{ + MPEZoneLayout layout; + EXPECT_FALSE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); +} + +TEST_F (MPEZoneLayoutTest, AddingZones) +{ + MPEZoneLayout layout; + + layout.setLowerZone (7); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 7); + + layout.setUpperZone (7); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 7); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 7); + + layout.setLowerZone (3); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 3); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 7); + + layout.setUpperZone (3); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 3); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 3); + + layout.setLowerZone (15); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 15); +} + +TEST_F (MPEZoneLayoutTest, ClearAllZones) +{ + MPEZoneLayout layout; + + EXPECT_FALSE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); + + layout.setLowerZone (7); + layout.setUpperZone (2); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + + layout.clearAllZones(); + + EXPECT_FALSE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); +} + +TEST_F (MPEZoneLayoutTest, ProcessMidiBuffers) +{ + MPEZoneLayout layout; + MidiBuffer buffer; + + buffer = MPEMessages::setLowerZone (7); + layout.processNextMidiBuffer (buffer); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 7); + + buffer = MPEMessages::setUpperZone (7); + layout.processNextMidiBuffer (buffer); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 7); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 7); + + { + buffer = MPEMessages::setLowerZone (10); + layout.processNextMidiBuffer (buffer); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 10); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 4); + + buffer = MPEMessages::setLowerZone (10, 33, 44); + layout.processNextMidiBuffer (buffer); + + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 10); + EXPECT_EQ (layout.getLowerZone().perNotePitchbendRange, 33); + EXPECT_EQ (layout.getLowerZone().masterPitchbendRange, 44); + } + + { + buffer = MPEMessages::setUpperZone (10); + layout.processNextMidiBuffer (buffer); + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_TRUE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 4); + EXPECT_EQ (layout.getUpperZone().getMasterChannel(), 16); + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 10); + + buffer = MPEMessages::setUpperZone (10, 33, 44); + + layout.processNextMidiBuffer (buffer); + + EXPECT_EQ (layout.getUpperZone().numMemberChannels, 10); + EXPECT_EQ (layout.getUpperZone().perNotePitchbendRange, 33); + EXPECT_EQ (layout.getUpperZone().masterPitchbendRange, 44); + } + + buffer = MPEMessages::clearAllZones(); + layout.processNextMidiBuffer (buffer); + + EXPECT_FALSE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); +} + +TEST_F (MPEZoneLayoutTest, ProcessIndividualMidiMessages) +{ + MPEZoneLayout layout; + + layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg + layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1 + layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2 + layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg + layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3 + layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg + + EXPECT_TRUE (layout.getLowerZone().isActive()); + EXPECT_FALSE (layout.getUpperZone().isActive()); + EXPECT_EQ (layout.getLowerZone().getMasterChannel(), 1); + EXPECT_EQ (layout.getLowerZone().numMemberChannels, 3); + EXPECT_EQ (layout.getLowerZone().perNotePitchbendRange, 48); + EXPECT_EQ (layout.getLowerZone().masterPitchbendRange, 2); + + const auto masterPitchBend = 0x0c; + layout.processNextMidiEvent ({ 0xb0, 0x64, 0x00 }); + layout.processNextMidiEvent ({ 0xb0, 0x06, masterPitchBend }); + + EXPECT_EQ (layout.getLowerZone().masterPitchbendRange, masterPitchBend); + + const auto newPitchBend = 0x0d; + layout.processNextMidiEvent ({ 0xb0, 0x06, newPitchBend }); + + EXPECT_EQ (layout.getLowerZone().masterPitchbendRange, newPitchBend); +} diff --git a/tests/yup_audio_basics/yup_MemoryAudioSource.cpp b/tests/yup_audio_basics/yup_MemoryAudioSource.cpp new file mode 100644 index 000000000..b856c4b23 --- /dev/null +++ b/tests/yup_audio_basics/yup_MemoryAudioSource.cpp @@ -0,0 +1,176 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +AudioBuffer getTestBuffer (int length) +{ + AudioBuffer buffer { 2, length }; + + for (int channel = 0; channel < buffer.getNumChannels(); ++channel) + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + buffer.setSample (channel, sample, jmap ((float) sample, 0.0f, (float) length, -1.0f, 1.0f)); + + return buffer; +} + +AudioBuffer getShortBuffer() { return getTestBuffer (5); } + +AudioBuffer getLongBuffer() { return getTestBuffer (1000); } + +void play (MemoryAudioSource& source, AudioSourceChannelInfo& info) +{ + info.clearActiveBufferRegion(); + source.getNextAudioBlock (info); +} + +bool isSilent (const AudioBuffer& b) +{ + for (int channel = 0; channel < b.getNumChannels(); ++channel) + if (b.findMinMax (channel, 0, b.getNumSamples()) != Range {}) + return false; + + return true; +} +} // namespace + +class MemoryAudioSourceTests : public ::testing::Test +{ +protected: + void SetUp() override + { + bufferToFill.setSize (2, blockSize); + channelInfo = std::make_unique (bufferToFill); + } + + constexpr static int blockSize = 512; + AudioBuffer bufferToFill; + std::unique_ptr channelInfo; +}; + +TEST_F (MemoryAudioSourceTests, ZeroLengthBufferProducesSilence) +{ + for (const bool enableLooping : { false, true }) + { + AudioBuffer buffer; + MemoryAudioSource source { buffer, true, false }; + source.setLooping (enableLooping); + source.prepareToPlay (blockSize, 44100.0); + + for (int i = 0; i < 2; ++i) + { + play (source, *channelInfo); + EXPECT_TRUE (isSilent (bufferToFill)); + } + } +} + +TEST_F (MemoryAudioSourceTests, ShortBufferWithoutLoopingPlayedOnceAndSilence) +{ + auto buffer = getShortBuffer(); + MemoryAudioSource source { buffer, true, false }; + source.setLooping (false); + source.prepareToPlay (blockSize, 44100.0); + + play (source, *channelInfo); + + auto copy = buffer; + copy.setSize (buffer.getNumChannels(), blockSize, true, true, false); + + EXPECT_TRUE (bufferToFill == copy); + + play (source, *channelInfo); + + EXPECT_TRUE (isSilent (bufferToFill)); +} + +TEST_F (MemoryAudioSourceTests, ShortBufferWithLoopingPlayedMultipleTimes) +{ + auto buffer = getShortBuffer(); + MemoryAudioSource source { buffer, true, false }; + source.setLooping (true); + source.prepareToPlay (blockSize, 44100.0); + + play (source, *channelInfo); + + for (int sample = 0; sample < buffer.getNumSamples(); ++sample) + EXPECT_EQ (bufferToFill.getSample (0, sample + buffer.getNumSamples()), buffer.getSample (0, sample)); + + EXPECT_FALSE (isSilent (bufferToFill)); +} + +TEST_F (MemoryAudioSourceTests, LongBufferWithoutLoopingPlayedOnce) +{ + auto buffer = getLongBuffer(); + MemoryAudioSource source { buffer, true, false }; + source.setLooping (false); + source.prepareToPlay (blockSize, 44100.0); + + play (source, *channelInfo); + + auto copy = buffer; + copy.setSize (buffer.getNumChannels(), blockSize, true, true, false); + + EXPECT_TRUE (bufferToFill == copy); + + for (int i = 0; i < 10; ++i) + play (source, *channelInfo); + + EXPECT_TRUE (isSilent (bufferToFill)); +} + +TEST_F (MemoryAudioSourceTests, LongBufferWithLoopingPlayedMultipleTimes) +{ + auto buffer = getLongBuffer(); + MemoryAudioSource source { buffer, true, false }; + source.setLooping (true); + source.prepareToPlay (blockSize, 44100.0); + + for (int i = 0; i < 100; ++i) + { + play (source, *channelInfo); + EXPECT_EQ (bufferToFill.getSample (0, 0), buffer.getSample (0, (i * blockSize) % buffer.getNumSamples())); + } +} diff --git a/tests/yup_audio_basics/yup_MidiFile.cpp b/tests/yup_audio_basics/yup_MidiFile.cpp new file mode 100644 index 000000000..930d8d0d8 --- /dev/null +++ b/tests/yup_audio_basics/yup_MidiFile.cpp @@ -0,0 +1,306 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#if 0 +using namespace yup; + +namespace +{ +void writeBytes(OutputStream& os, const std::vector& bytes) +{ + for (const auto& byte : bytes) + os.writeByte((char)byte); +} + +template +MidiMessageSequence parseSequence(Fn&& fn) +{ + MemoryOutputStream os; + fn(os); + + return MidiFileHelpers::readTrack(reinterpret_cast(os.getData()), + (int)os.getDataSize()); +} + +template +Optional parseHeader(Fn&& fn) +{ + MemoryOutputStream os; + fn(os); + + return MidiFileHelpers::parseMidiHeader(reinterpret_cast(os.getData()), + os.getDataSize()); +} + +template +Optional parseFile(Fn&& fn) +{ + MemoryOutputStream os; + fn(os); + + MemoryInputStream is(os.getData(), os.getDataSize(), false); + MidiFile mf; + + int fileType = 0; + + if (mf.readFrom(is, true, &fileType)) + return mf; + + return {}; +} +} // namespace + +class MidiFileTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Setup common test data if needed + } +}; + +TEST_F(MidiFileTest, ReadTrackRespectsRunningStatus) +{ + const auto sequence = parseSequence([](OutputStream& os) + { + MidiFileHelpers::writeVariableLengthInt(os, 100); + writeBytes(os, { 0x90, 0x40, 0x40 }); + MidiFileHelpers::writeVariableLengthInt(os, 200); + writeBytes(os, { 0x40, 0x40 }); + MidiFileHelpers::writeVariableLengthInt(os, 300); + writeBytes(os, { 0xff, 0x2f, 0x00 }); + }); + + EXPECT_EQ(sequence.getNumEvents(), 3); + EXPECT_TRUE(sequence.getEventPointer(0)->message.isNoteOn()); + EXPECT_TRUE(sequence.getEventPointer(1)->message.isNoteOn()); + EXPECT_TRUE(sequence.getEventPointer(2)->message.isEndOfTrackMetaEvent()); +} + +TEST_F(MidiFileTest, ReadTrackReturnsAvailableMessagesIfInputIsTruncated) +{ + { + const auto sequence = parseSequence([](OutputStream& os) + { + // Incomplete delta time + writeBytes(os, { 0xff }); + }); + + EXPECT_EQ(sequence.getNumEvents(), 0); + } + + { + const auto sequence = parseSequence([](OutputStream& os) + { + // Complete delta with no following event + MidiFileHelpers::writeVariableLengthInt(os, 0xffff); + }); + + EXPECT_EQ(sequence.getNumEvents(), 0); + } + + { + const auto sequence = parseSequence([](OutputStream& os) + { + // Complete delta with malformed following event + MidiFileHelpers::writeVariableLengthInt(os, 0xffff); + writeBytes(os, { 0x90, 0x40 }); + }); + + EXPECT_EQ(sequence.getNumEvents(), 1); + EXPECT_TRUE(sequence.getEventPointer(0)->message.isNoteOff()); + EXPECT_EQ(sequence.getEventPointer(0)->message.getNoteNumber(), 0x40); + EXPECT_EQ(sequence.getEventPointer(0)->message.getVelocity(), (uint8)0x00); + } +} + +TEST_F(MidiFileTest, HeaderParsingWorks) +{ + { + // No data + const auto header = parseHeader([](OutputStream&) {}); + EXPECT_FALSE(header.hasValue()); + } + + { + // Invalid initial byte + const auto header = parseHeader([](OutputStream& os) + { + writeBytes(os, { 0xff }); + }); + + EXPECT_FALSE(header.hasValue()); + } + + { + // Type block, but no header data + const auto header = parseHeader([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd' }); + }); + + EXPECT_FALSE(header.hasValue()); + } + + { + // Well-formed header, but track type is 0 and channels != 1 + const auto header = parseHeader([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 }); + }); + + EXPECT_FALSE(header.hasValue()); + } + + { + // Well-formed header, but track type is 5 + const auto header = parseHeader([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 }); + }); + + EXPECT_FALSE(header.hasValue()); + } + + { + // Well-formed header + const auto header = parseHeader([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 }); + }); + + EXPECT_TRUE(header.hasValue()); + + EXPECT_EQ(header->fileType, (short)1); + EXPECT_EQ(header->numberOfTracks, (short)16); + EXPECT_EQ(header->timeFormat, (short)1); + EXPECT_EQ((int)header->bytesRead, 14); + } +} + +TEST_F(MidiFileTest, ReadFromStream) +{ + { + // Empty input + const auto file = parseFile([](OutputStream&) {}); + EXPECT_FALSE(file.hasValue()); + } + + { + // Malformed header + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd' }); + }); + + EXPECT_FALSE(file.hasValue()); + } + + { + // Header, no channels + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 }); + }); + + EXPECT_TRUE(file.hasValue()); + EXPECT_EQ(file->getNumTracks(), 0); + } + + { + // Header, one malformed channel + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); + writeBytes(os, { 'M', 'T', 'r', '?' }); + }); + + EXPECT_FALSE(file.hasValue()); + } + + { + // Header, one channel with malformed message + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); + writeBytes(os, { 'M', 'T', 'r', 'k', 0, 0, 0, 1, 0xff }); + }); + + EXPECT_TRUE(file.hasValue()); + EXPECT_EQ(file->getNumTracks(), 1); + EXPECT_EQ(file->getTrack(0)->getNumEvents(), 0); + } + + { + // Header, one channel with incorrect length message + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); + writeBytes(os, { 'M', 'T', 'r', 'k', 0x0f, 0, 0, 0, 0xff }); + }); + + EXPECT_FALSE(file.hasValue()); + } + + { + // Header, one channel, all well-formed + const auto file = parseFile([](OutputStream& os) + { + writeBytes(os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 }); + writeBytes(os, { 'M', 'T', 'r', 'k', 0, 0, 0, 4 }); + + MidiFileHelpers::writeVariableLengthInt(os, 0x0f); + writeBytes(os, { 0x80, 0x00, 0x00 }); + }); + + EXPECT_TRUE(file.hasValue()); + EXPECT_EQ(file->getNumTracks(), 1); + + auto& track = *file->getTrack(0); + EXPECT_EQ(track.getNumEvents(), 1); + EXPECT_TRUE(track.getEventPointer(0)->message.isNoteOff()); + EXPECT_EQ(track.getEventPointer(0)->message.getTimeStamp(), (double)0x0f); + } +} + +#endif diff --git a/tests/yup_audio_basics/yup_MidiMessage.cpp b/tests/yup_audio_basics/yup_MidiMessage.cpp index f47e61bc7..de2841ee9 100644 --- a/tests/yup_audio_basics/yup_MidiMessage.cpp +++ b/tests/yup_audio_basics/yup_MidiMessage.cpp @@ -45,7 +45,10 @@ using namespace yup; namespace { -const std::vector metaEvents[] { + +// clang-format off +const std::vector metaEvents[] +{ // Format is 0xff, followed by a 'kind' byte, followed by a variable-length // 'data-length' value, followed by that many data bytes { 0xff, 0x00, 0x02, 0x00, 0x00 }, // Sequence number @@ -64,6 +67,8 @@ const std::vector metaEvents[] { { 0xff, 0x59, 0x02, 0x01, 0x02 }, // Key signature { 0xff, 0x7f, 0x00 }, // Sequencer-specific }; +// clang-format on + } // namespace TEST (MidiMessageTests, ReadVariableLengthValueShouldReturnCompatibleResults) diff --git a/tests/yup_audio_basics/yup_MidiMessageSequence.cpp b/tests/yup_audio_basics/yup_MidiMessageSequence.cpp new file mode 100644 index 000000000..5d78f2726 --- /dev/null +++ b/tests/yup_audio_basics/yup_MidiMessageSequence.cpp @@ -0,0 +1,245 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#if 0 + +using namespace yup; + +namespace +{ +struct ControlValue +{ + int control, value; +}; + +struct DataEntry +{ + int controllerBase, channel, parameter, value; + double time; + + std::array getControlValues() const + { + return { { { controllerBase + 1, (parameter >> 7) & 0x7f }, + { controllerBase + 0, (parameter >> 0) & 0x7f }, + { 0x06, (value >> 7) & 0x7f }, + { 0x26, (value >> 0) & 0x7f } } }; + } + + void addToSequence(MidiMessageSequence& s) const + { + for (const auto& pair : getControlValues()) + s.addEvent(MidiMessage::controllerEvent(channel, pair.control, pair.value), time); + } + + bool matches(const MidiMessage* begin, const MidiMessage* end) const + { + const auto isEqual = [this](const ControlValue& cv, const MidiMessage& msg) + { + return exactlyEqual(msg.getTimeStamp(), time) + && msg.isController() + && msg.getChannel() == channel + && msg.getControllerNumber() == cv.control + && msg.getControllerValue() == cv.value; + }; + + const auto pairs = getControlValues(); + return std::equal(pairs.begin(), pairs.end(), begin, end, isEqual); + } +}; + +bool messagesAreEqual(const MidiMessage& a, const MidiMessage& b) +{ + return a.getDescription() == b.getDescription() + && exactlyEqual(a.getTimeStamp(), b.getTimeStamp()); +} + +} // namespace + +class MidiMessageSequenceTest : public ::testing::Test +{ +protected: + void SetUp() override + { + s.addEvent(MidiMessage::noteOn(1, 60, 0.5f).withTimeStamp(0.0)); + s.addEvent(MidiMessage::noteOff(1, 60, 0.5f).withTimeStamp(4.0)); + s.addEvent(MidiMessage::noteOn(1, 30, 0.5f).withTimeStamp(2.0)); + s.addEvent(MidiMessage::noteOff(1, 30, 0.5f).withTimeStamp(8.0)); + } + + void addNrpn(MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0) + { + DataEntry{ 0x62, channel, parameter, value, time }.addToSequence(seq); + } + + void addRpn(MidiMessageSequence& seq, int channel, int parameter, int value, double time = 0.0) + { + DataEntry{ 0x64, channel, parameter, value, time }.addToSequence(seq); + } + + void checkNrpn(const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0) + { + auto result = DataEntry{ 0x62, channel, parameter, value, time }.matches(begin, end); + EXPECT_TRUE(result); + } + + void checkRpn(const MidiMessage* begin, const MidiMessage* end, int channel, int parameter, int value, double time = 0.0) + { + auto result = DataEntry{ 0x64, channel, parameter, value, time }.matches(begin, end); + EXPECT_TRUE(result); + } + + MidiMessageSequence s; +}; + +TEST_F(MidiMessageSequenceTest, StartAndEndTime) +{ + EXPECT_EQ(s.getStartTime(), 0.0); + EXPECT_EQ(s.getEndTime(), 8.0); + EXPECT_EQ(s.getEventTime(1), 2.0); +} + +TEST_F(MidiMessageSequenceTest, MatchingNoteOffAndOns) +{ + s.updateMatchedPairs(); + EXPECT_EQ(s.getTimeOfMatchingKeyUp(0), 4.0); + EXPECT_EQ(s.getTimeOfMatchingKeyUp(1), 8.0); + EXPECT_EQ(s.getIndexOfMatchingKeyUp(0), 2); + EXPECT_EQ(s.getIndexOfMatchingKeyUp(1), 3); +} + +TEST_F(MidiMessageSequenceTest, TimeAndIndices) +{ + EXPECT_EQ(s.getNextIndexAtTime(0.5), 1); + EXPECT_EQ(s.getNextIndexAtTime(2.5), 2); + EXPECT_EQ(s.getNextIndexAtTime(9.0), 4); +} + +TEST_F(MidiMessageSequenceTest, DeletingEvents) +{ + s.deleteEvent(0, true); + EXPECT_EQ(s.getNumEvents(), 2); +} + +TEST_F(MidiMessageSequenceTest, MergingSequences) +{ + MidiMessageSequence s2; + s2.addEvent(MidiMessage::noteOn(2, 25, 0.5f).withTimeStamp(0.0)); + s2.addEvent(MidiMessage::noteOn(2, 40, 0.5f).withTimeStamp(1.0)); + s2.addEvent(MidiMessage::noteOff(2, 40, 0.5f).withTimeStamp(5.0)); + s2.addEvent(MidiMessage::noteOn(2, 80, 0.5f).withTimeStamp(3.0)); + s2.addEvent(MidiMessage::noteOff(2, 80, 0.5f).withTimeStamp(7.0)); + s2.addEvent(MidiMessage::noteOff(2, 25, 0.5f).withTimeStamp(9.0)); + + s.addSequence(s2, 0.0, 0.0, 8.0); // Intentionally cut off the last note off + s.updateMatchedPairs(); + + EXPECT_EQ(s.getNumEvents(), 7); + EXPECT_EQ(s.getIndexOfMatchingKeyUp(0), -1); // Truncated note, should be no note off + EXPECT_EQ(s.getTimeOfMatchingKeyUp(1), 5.0); +} + +TEST_F(MidiMessageSequenceTest, CreateControllerUpdatesForTimeEmitsNRPNComponentsInCorrectOrder) +{ + const auto channel = 1; + const auto number = 200; + const auto value = 300; + + MidiMessageSequence sequence; + addNrpn(sequence, channel, number, value); + + Array m; + sequence.createControllerUpdatesForTime(channel, 1.0, m); + + checkNrpn(m.begin(), m.end(), channel, number, value); +} + +TEST_F(MidiMessageSequenceTest, CreateControllerUpdatesForTimeIgnoresNRPNsAfterFinalRequestedTime) +{ + const auto channel = 2; + const auto number = 123; + const auto value = 456; + + MidiMessageSequence sequence; + addRpn(sequence, channel, number, value, 0.5); + addRpn(sequence, channel, 111, 222, 1.5); + addRpn(sequence, channel, 333, 444, 2.5); + + Array m; + sequence.createControllerUpdatesForTime(channel, 1.0, m); + + checkRpn(m.begin(), std::next(m.begin(), 4), channel, number, value, 0.5); +} + +TEST_F(MidiMessageSequenceTest, CreateControllerUpdatesForTimeEmitsSeparateNRPNMessagesWhenAppropriate) +{ + const auto channel = 2; + const auto numberA = 1111; + const auto valueA = 9999; + + const auto numberB = 8888; + const auto valueB = 2222; + + const auto numberC = 7777; + const auto valueC = 3333; + + const auto numberD = 6666; + const auto valueD = 4444; + + const auto time = 0.5; + + MidiMessageSequence sequence; + addRpn(sequence, channel, numberA, valueA, time); + addRpn(sequence, channel, numberB, valueB, time); + addNrpn(sequence, channel, numberC, valueC, time); + addNrpn(sequence, channel, numberD, valueD, time); + + Array m; + sequence.createControllerUpdatesForTime(channel, time * 2, m); + + checkRpn(std::next(m.begin(), 0), std::next(m.begin(), 4), channel, numberA, valueA, time); + checkRpn(std::next(m.begin(), 4), std::next(m.begin(), 8), channel, numberB, valueB, time); + checkNrpn(std::next(m.begin(), 8), std::next(m.begin(), 12), channel, numberC, valueC, time); + checkNrpn(std::next(m.begin(), 12), std::next(m.begin(), 16), channel, numberD, valueD, time); +} + +#endif diff --git a/tests/yup_audio_basics/yup_MidiRPN.cpp b/tests/yup_audio_basics/yup_MidiRPN.cpp new file mode 100644 index 000000000..69b829c91 --- /dev/null +++ b/tests/yup_audio_basics/yup_MidiRPN.cpp @@ -0,0 +1,288 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +class MidiRPNDetectorTests : public ::testing::Test +{ +}; + +TEST_F (MidiRPNDetectorTests, IndividualMSBIsParsedAs7Bit) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (2, 101, 0)); + EXPECT_FALSE (detector.tryParse (2, 100, 7)); + + auto parsed = detector.tryParse (2, 6, 42); + EXPECT_TRUE (parsed.has_value()); + + EXPECT_EQ (parsed->channel, 2); + EXPECT_EQ (parsed->parameterNumber, 7); + EXPECT_EQ (parsed->value, 42); + EXPECT_FALSE (parsed->isNRPN); + EXPECT_FALSE (parsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, LSBWithoutPrecedingMSBIsIgnored) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (2, 101, 0)); + EXPECT_FALSE (detector.tryParse (2, 100, 7)); + EXPECT_FALSE (detector.tryParse (2, 38, 42)); +} + +TEST_F (MidiRPNDetectorTests, LSBFollowingMSBIsParsedAs14Bit) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (1, 101, 2)); + EXPECT_FALSE (detector.tryParse (1, 100, 44)); + + EXPECT_TRUE (detector.tryParse (1, 6, 1).has_value()); + + auto lsbParsed = detector.tryParse (1, 38, 94); + EXPECT_TRUE (lsbParsed.has_value()); + + EXPECT_EQ (lsbParsed->channel, 1); + EXPECT_EQ (lsbParsed->parameterNumber, 300); + EXPECT_EQ (lsbParsed->value, 222); + EXPECT_FALSE (lsbParsed->isNRPN); + EXPECT_TRUE (lsbParsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, MultipleLSBFollowingMSBReuseTheMSB) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (1, 101, 2)); + EXPECT_FALSE (detector.tryParse (1, 100, 43)); + + EXPECT_TRUE (detector.tryParse (1, 6, 1).has_value()); + + EXPECT_TRUE (detector.tryParse (1, 38, 94).has_value()); + EXPECT_TRUE (detector.tryParse (1, 38, 95).has_value()); + EXPECT_TRUE (detector.tryParse (1, 38, 96).has_value()); + + auto lsbParsed = detector.tryParse (1, 38, 97); + EXPECT_TRUE (lsbParsed.has_value()); + + EXPECT_EQ (lsbParsed->channel, 1); + EXPECT_EQ (lsbParsed->parameterNumber, 299); + EXPECT_EQ (lsbParsed->value, 225); + EXPECT_FALSE (lsbParsed->isNRPN); + EXPECT_TRUE (lsbParsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, SendingNewMSBResetsTheLSB) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (1, 101, 3)); + EXPECT_FALSE (detector.tryParse (1, 100, 43)); + + EXPECT_TRUE (detector.tryParse (1, 6, 1).has_value()); + EXPECT_TRUE (detector.tryParse (1, 38, 94).has_value()); + + auto newMsb = detector.tryParse (1, 6, 2); + EXPECT_TRUE (newMsb.has_value()); + + EXPECT_EQ (newMsb->channel, 1); + EXPECT_EQ (newMsb->parameterNumber, 427); + EXPECT_EQ (newMsb->value, 2); + EXPECT_FALSE (newMsb->isNRPN); + EXPECT_FALSE (newMsb->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, RPNsOnMultipleChannelsSimultaneously) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (1, 100, 44)); + EXPECT_FALSE (detector.tryParse (2, 101, 0)); + EXPECT_FALSE (detector.tryParse (1, 101, 2)); + EXPECT_FALSE (detector.tryParse (2, 100, 7)); + EXPECT_TRUE (detector.tryParse (1, 6, 1).has_value()); + + auto channelTwo = detector.tryParse (2, 6, 42); + EXPECT_TRUE (channelTwo.has_value()); + + EXPECT_EQ (channelTwo->channel, 2); + EXPECT_EQ (channelTwo->parameterNumber, 7); + EXPECT_EQ (channelTwo->value, 42); + EXPECT_FALSE (channelTwo->isNRPN); + EXPECT_FALSE (channelTwo->is14BitValue); + + auto channelOne = detector.tryParse (1, 38, 94); + EXPECT_TRUE (channelOne.has_value()); + + EXPECT_EQ (channelOne->channel, 1); + EXPECT_EQ (channelOne->parameterNumber, 300); + EXPECT_EQ (channelOne->value, 222); + EXPECT_FALSE (channelOne->isNRPN); + EXPECT_TRUE (channelOne->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, RPNWithValueWithin7BitRange) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (16, 100, 0)); + EXPECT_FALSE (detector.tryParse (16, 101, 0)); + EXPECT_TRUE (detector.tryParse (16, 6, 0).has_value()); + + auto parsed = detector.tryParse (16, 38, 3); + EXPECT_TRUE (parsed.has_value()); + + EXPECT_EQ (parsed->channel, 16); + EXPECT_EQ (parsed->parameterNumber, 0); + EXPECT_EQ (parsed->value, 3); + EXPECT_FALSE (parsed->isNRPN); + EXPECT_TRUE (parsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, InvalidRPNWrongOrder) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (2, 6, 42)); + EXPECT_FALSE (detector.tryParse (2, 101, 0)); + EXPECT_FALSE (detector.tryParse (2, 100, 7)); +} + +TEST_F (MidiRPNDetectorTests, RPNInterspersedWithUnrelatedCCMessages) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (16, 3, 80)); + EXPECT_FALSE (detector.tryParse (16, 100, 0)); + EXPECT_FALSE (detector.tryParse (16, 4, 81)); + EXPECT_FALSE (detector.tryParse (16, 101, 0)); + EXPECT_FALSE (detector.tryParse (16, 5, 82)); + EXPECT_FALSE (detector.tryParse (16, 5, 83)); + EXPECT_TRUE (detector.tryParse (16, 6, 0).has_value()); + EXPECT_FALSE (detector.tryParse (16, 4, 84).has_value()); + EXPECT_FALSE (detector.tryParse (16, 3, 85).has_value()); + + auto parsed = detector.tryParse (16, 38, 3); + EXPECT_TRUE (parsed.has_value()); + + EXPECT_EQ (parsed->channel, 16); + EXPECT_EQ (parsed->parameterNumber, 0); + EXPECT_EQ (parsed->value, 3); + EXPECT_FALSE (parsed->isNRPN); + EXPECT_TRUE (parsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, NRPNTest) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (1, 98, 44)); + EXPECT_FALSE (detector.tryParse (1, 99, 2)); + EXPECT_TRUE (detector.tryParse (1, 6, 1).has_value()); + + auto parsed = detector.tryParse (1, 38, 94); + EXPECT_TRUE (parsed.has_value()); + + EXPECT_EQ (parsed->channel, 1); + EXPECT_EQ (parsed->parameterNumber, 300); + EXPECT_EQ (parsed->value, 222); + EXPECT_TRUE (parsed->isNRPN); + EXPECT_TRUE (parsed->is14BitValue); +} + +TEST_F (MidiRPNDetectorTests, ResetTest) +{ + MidiRPNDetector detector; + EXPECT_FALSE (detector.tryParse (2, 101, 0)); + detector.reset(); + EXPECT_FALSE (detector.tryParse (2, 100, 7)); + EXPECT_FALSE (detector.tryParse (2, 6, 42)); +} + +// Generator tests +class MidiRPNGeneratorTests : public ::testing::Test +{ +protected: + void expectContainsRPN (const MidiBuffer& midiBuffer, + int channel, + int parameterNumber, + int value, + bool isNRPN, + bool is14BitValue) + { + MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue }; + expectContainsRPN (midiBuffer, expected); + } + + void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected) + { + std::optional result; + MidiRPNDetector detector; + + for (const auto metadata : midiBuffer) + { + const auto midiMessage = metadata.getMessage(); + + result = detector.tryParse (midiMessage.getChannel(), + midiMessage.getControllerNumber(), + midiMessage.getControllerValue()); + } + + EXPECT_TRUE (result.has_value()); + EXPECT_EQ (result->channel, expected.channel); + EXPECT_EQ (result->parameterNumber, expected.parameterNumber); + EXPECT_EQ (result->value, expected.value); + EXPECT_EQ (result->isNRPN, expected.isNRPN); + EXPECT_EQ (result->is14BitValue, expected.is14BitValue); + } +}; + +TEST_F (MidiRPNGeneratorTests, GeneratingRPNAndNRPN) +{ + { + MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true); + expectContainsRPN (buffer, 1, 23, 1337, true, true); + } + { + MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false); + expectContainsRPN (buffer, 16, 101, 34, false, false); + } + { + MidiRPNMessage message = { 16, 101, 34, false, false }; + MidiBuffer buffer = MidiRPNGenerator::generate (message); + expectContainsRPN (buffer, message); + } +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_SmoothedValue.cpp b/tests/yup_audio_basics/yup_SmoothedValue.cpp new file mode 100644 index 000000000..903b4443b --- /dev/null +++ b/tests/yup_audio_basics/yup_SmoothedValue.cpp @@ -0,0 +1,365 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +// Template test class for common SmoothedValue tests +template +class CommonSmoothedValueTests : public ::testing::Test +{ +protected: + void runInitialStateTest() + { + SmoothedValueType sv; + + auto value = sv.getCurrentValue(); + EXPECT_EQ (sv.getTargetValue(), value); + + sv.getNextValue(); + EXPECT_EQ (sv.getCurrentValue(), value); + EXPECT_FALSE (sv.isSmoothing()); + } + + void runResettingTest() + { + auto initialValue = 15.0f; + + SmoothedValueType sv (initialValue); + sv.reset (3); + EXPECT_EQ (sv.getCurrentValue(), initialValue); + + auto targetValue = initialValue + 1.0f; + sv.setTargetValue (targetValue); + EXPECT_EQ (sv.getTargetValue(), targetValue); + EXPECT_EQ (sv.getCurrentValue(), initialValue); + EXPECT_TRUE (sv.isSmoothing()); + + auto currentValue = sv.getNextValue(); + EXPECT_GT (currentValue, initialValue); + EXPECT_EQ (sv.getCurrentValue(), currentValue); + EXPECT_EQ (sv.getTargetValue(), targetValue); + EXPECT_TRUE (sv.isSmoothing()); + + sv.reset (5); + + EXPECT_EQ (sv.getCurrentValue(), targetValue); + EXPECT_EQ (sv.getTargetValue(), targetValue); + EXPECT_FALSE (sv.isSmoothing()); + + sv.getNextValue(); + EXPECT_EQ (sv.getCurrentValue(), targetValue); + + sv.setTargetValue (1.5f); + sv.getNextValue(); + + float newStart = 0.2f; + sv.setCurrentAndTargetValue (newStart); + EXPECT_EQ (sv.getNextValue(), newStart); + EXPECT_EQ (sv.getTargetValue(), newStart); + EXPECT_EQ (sv.getCurrentValue(), newStart); + EXPECT_FALSE (sv.isSmoothing()); + } + + void runSampleRateTest() + { + SmoothedValueType svSamples { 3.0f }; + auto svTime = svSamples; + + auto numSamples = 12; + + svSamples.reset (numSamples); + svTime.reset (numSamples * 2, 1.0); + + for (int i = 0; i < numSamples; ++i) + { + svTime.skip (1); + EXPECT_NEAR (svSamples.getNextValue(), svTime.getNextValue(), 1.0e-7f); + } + } + + void runBlockProcessingTest() + { + SmoothedValueType sv (1.0f); + + sv.reset (12); + sv.setTargetValue (2.0f); + + const auto numSamples = 15; + + AudioBuffer referenceData (1, numSamples); + + for (int i = 0; i < numSamples; ++i) + referenceData.setSample (0, i, sv.getNextValue()); + + EXPECT_GT (referenceData.getSample (0, 0), 0); + EXPECT_LT (referenceData.getSample (0, 10), sv.getTargetValue()); + EXPECT_NEAR (referenceData.getSample (0, 11), sv.getTargetValue(), 2.0e-7f); + + auto getUnitData = [] (int numSamplesToGenerate) + { + AudioBuffer result (1, numSamplesToGenerate); + + for (int i = 0; i < numSamplesToGenerate; ++i) + result.setSample (0, i, 1.0f); + + return result; + }; + + auto compareData = [this] (const AudioBuffer& test, + const AudioBuffer& reference) + { + for (int i = 0; i < test.getNumSamples(); ++i) + EXPECT_NEAR (test.getSample (0, i), reference.getSample (0, i), 2.0e-7f); + }; + + auto testData = getUnitData (numSamples); + sv.setCurrentAndTargetValue (1.0f); + sv.setTargetValue (2.0f); + sv.applyGain (testData.getWritePointer (0), numSamples); + compareData (testData, referenceData); + + testData = getUnitData (numSamples); + AudioBuffer destData (1, numSamples); + sv.setCurrentAndTargetValue (1.0f); + sv.setTargetValue (2.0f); + sv.applyGain (destData.getWritePointer (0), + testData.getReadPointer (0), + numSamples); + compareData (destData, referenceData); + compareData (testData, getUnitData (numSamples)); + + testData = getUnitData (numSamples); + sv.setCurrentAndTargetValue (1.0f); + sv.setTargetValue (2.0f); + sv.applyGain (testData, numSamples); + compareData (testData, referenceData); + } + + void runSkipTest() + { + SmoothedValueType sv; + + sv.reset (12); + sv.setCurrentAndTargetValue (1.0f); + sv.setTargetValue (2.0f); + + Array reference; + + for (int i = 0; i < 15; ++i) + reference.add (sv.getNextValue()); + + sv.setCurrentAndTargetValue (1.0f); + sv.setTargetValue (2.0f); + + EXPECT_NEAR (sv.skip (1), reference[0], 1.0e-6f); + EXPECT_NEAR (sv.skip (1), reference[1], 1.0e-6f); + EXPECT_NEAR (sv.skip (2), reference[3], 1.0e-6f); + sv.skip (3); + EXPECT_NEAR (sv.getCurrentValue(), reference[6], 1.0e-6f); + EXPECT_EQ (sv.skip (300), sv.getTargetValue()); + EXPECT_EQ (sv.getCurrentValue(), sv.getTargetValue()); + } + + void runNegativeTest() + { + SmoothedValueType sv; + + auto numValues = 12; + sv.reset (numValues); + + std::vector> ranges = { { -1.0f, -2.0f }, + { -100.0f, -3.0f } }; + + for (auto range : ranges) + { + auto start = range.first, end = range.second; + + sv.setCurrentAndTargetValue (start); + sv.setTargetValue (end); + + auto val = sv.skip (numValues / 2); + + if (end > start) + EXPECT_TRUE (val > start && val < end); + else + EXPECT_TRUE (val < start && val > end); + + auto nextVal = sv.getNextValue(); + EXPECT_TRUE (end > start ? (nextVal > val) : (nextVal < val)); + + auto endVal = sv.skip (500); + EXPECT_EQ (endVal, end); + EXPECT_EQ (sv.getNextValue(), end); + EXPECT_EQ (sv.getCurrentValue(), end); + + sv.setCurrentAndTargetValue (start); + sv.setTargetValue (end); + + SmoothedValueType positiveSv { -start }; + positiveSv.reset (numValues); + positiveSv.setTargetValue (-end); + + for (int i = 0; i < numValues + 2; ++i) + EXPECT_EQ (sv.getNextValue(), -positiveSv.getNextValue()); + } + } +}; + +// Test fixture for Linear SmoothedValue +class LinearSmoothedValueTests : public CommonSmoothedValueTests> +{ +}; + +// Test fixture for Multiplicative SmoothedValue +class MultiplicativeSmoothedValueTests : public CommonSmoothedValueTests> +{ +}; + +// Common tests for Linear SmoothedValue +TEST_F (LinearSmoothedValueTests, InitialState) +{ + runInitialStateTest(); +} + +TEST_F (LinearSmoothedValueTests, Resetting) +{ + runResettingTest(); +} + +TEST_F (LinearSmoothedValueTests, SampleRate) +{ + runSampleRateTest(); +} + +TEST_F (LinearSmoothedValueTests, BlockProcessing) +{ + runBlockProcessingTest(); +} + +TEST_F (LinearSmoothedValueTests, Skip) +{ + runSkipTest(); +} + +TEST_F (LinearSmoothedValueTests, Negative) +{ + runNegativeTest(); +} + +// Common tests for Multiplicative SmoothedValue +TEST_F (MultiplicativeSmoothedValueTests, InitialState) +{ + runInitialStateTest(); +} + +TEST_F (MultiplicativeSmoothedValueTests, Resetting) +{ + runResettingTest(); +} + +TEST_F (MultiplicativeSmoothedValueTests, SampleRate) +{ + runSampleRateTest(); +} + +TEST_F (MultiplicativeSmoothedValueTests, BlockProcessing) +{ + runBlockProcessingTest(); +} + +TEST_F (MultiplicativeSmoothedValueTests, Skip) +{ + runSkipTest(); +} + +TEST_F (MultiplicativeSmoothedValueTests, Negative) +{ + runNegativeTest(); +} + +// Specific tests for SmoothedValue functionality +TEST (SmoothedValueSpecificTests, LinearMovingTarget) +{ + SmoothedValue sv; + + sv.reset (12); + float initialValue = 0.0f; + sv.setCurrentAndTargetValue (initialValue); + sv.setTargetValue (1.0f); + + auto delta = sv.getNextValue() - initialValue; + + sv.skip (6); + + auto newInitialValue = sv.getCurrentValue(); + sv.setTargetValue (newInitialValue + 2.0f); + auto doubleDelta = sv.getNextValue() - newInitialValue; + + EXPECT_NEAR (doubleDelta, delta * 2.0f, 1.0e-7f); +} + +TEST (SmoothedValueSpecificTests, MultiplicativeCurve) +{ + SmoothedValue sv; + + auto numSamples = 12; + AudioBuffer values (2, numSamples + 1); + + sv.reset (numSamples); + sv.setCurrentAndTargetValue (1.0); + sv.setTargetValue (2.0f); + + values.setSample (0, 0, sv.getCurrentValue()); + + for (int i = 1; i < values.getNumSamples(); ++i) + values.setSample (0, i, sv.getNextValue()); + + sv.setTargetValue (1.0f); + values.setSample (1, values.getNumSamples() - 1, sv.getCurrentValue()); + + for (int i = values.getNumSamples() - 2; i >= 0; --i) + values.setSample (1, i, sv.getNextValue()); + + for (int i = 0; i < values.getNumSamples(); ++i) + EXPECT_NEAR (values.getSample (0, i), values.getSample (1, i), 1.0e-9); +} \ No newline at end of file diff --git a/tests/yup_audio_basics/yup_UMP.cpp b/tests/yup_audio_basics/yup_UMP.cpp new file mode 100644 index 000000000..25fc7a906 --- /dev/null +++ b/tests/yup_audio_basics/yup_UMP.cpp @@ -0,0 +1,494 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#if 0 +namespace yup::universal_midi_packets +{ + +constexpr uint8_t operator""_u8 (unsigned long long int i) { return static_cast (i); } + +constexpr uint16_t operator""_u16 (unsigned long long int i) { return static_cast (i); } + +constexpr uint32_t operator""_u32 (unsigned long long int i) { return static_cast (i); } + +constexpr uint64_t operator""_u64 (unsigned long long int i) { return static_cast (i); } + +class UniversalMidiPacketTest : public ::testing::Test +{ +protected: + void SetUp() override + { + random.setSeed(12345); + } + + Random random; + + static Packets toMidi1(const MidiMessage& msg) + { + Packets packets; + Conversion::toMidi1(ump::BytestreamMidiView(&msg), [&](const auto p) + { + packets.add(p); + }); + return packets; + } + + static Packets convertMidi2ToMidi1(const Packets& midi2) + { + Packets r; + + for (const auto& packet : midi2) + Conversion::midi2ToMidi1DefaultTranslation(packet, [&r](const View& v) + { + r.add(v); + }); + + return r; + } + + static Packets convertMidi1ToMidi2(const Packets& midi1) + { + Packets r; + Midi1ToMidi2DefaultTranslator translator; + + for (const auto& packet : midi1) + translator.dispatch(packet, [&r](const View& v) + { + r.add(v); + }); + + return r; + } + + void checkBytestreamConversion(const Packets& actual, const Packets& expected) + { + EXPECT_EQ(actual.size(), expected.size()); + + if (actual.size() != expected.size()) + return; + + auto actualPtr = actual.data(); + + std::for_each(expected.data(), + expected.data() + expected.size(), + [&](const uint32_t word) + { + EXPECT_EQ(*actualPtr++, word); + }); + } + + void checkMidi2ToMidi1Conversion(const Packets& midi2, const Packets& expected) + { + checkBytestreamConversion(convertMidi2ToMidi1(midi2), expected); + } + + void checkMidi1ToMidi2Conversion(const Packets& midi1, const Packets& expected) + { + checkBytestreamConversion(convertMidi1ToMidi2(midi1), expected); + } + + MidiMessage createRandomSysEx(Random& random, size_t sysExBytes) + { + std::vector data; + data.reserve(sysExBytes); + + for (size_t i = 0; i != sysExBytes; ++i) + data.push_back(uint8_t(random.nextInt(0x80))); + + return MidiMessage::createSysExMessage(data.data(), int(data.size())); + } + + PacketX1 createRandomUtilityUMP(Random& random) + { + const auto status = random.nextInt(3); + + return PacketX1 { Utils::bytesToWord(std::byte { 0 }, + std::byte(status << 0x4), + std::byte(status == 0 ? 0 : random.nextInt(0x100)), + std::byte(status == 0 ? 0 : random.nextInt(0x100))) }; + } + + PacketX1 createRandomRealtimeUMP(Random& random) + { + const auto status = [&] + { + switch (random.nextInt(6)) + { + case 0: + return std::byte { 0xf8 }; + case 1: + return std::byte { 0xfa }; + case 2: + return std::byte { 0xfb }; + case 3: + return std::byte { 0xfc }; + case 4: + return std::byte { 0xfe }; + case 5: + return std::byte { 0xff }; + } + + jassertfalse; + return std::byte { 0x00 }; + }(); + + return PacketX1 { Utils::bytesToWord(std::byte { 0x10 }, status, std::byte { 0x00 }, std::byte { 0x00 }) }; + } + + template + void forEachNonSysExTestMessage(Random& random, Fn&& fn) + { + for (uint16_t counter = 0x80; counter != 0x100; ++counter) + { + const auto firstByte = (uint8_t)counter; + + if (firstByte == 0xf0 || firstByte == 0xf7) + continue; // sysEx is tested separately + + const auto length = MidiMessage::getMessageLengthFromFirstByte(firstByte); + const auto getDataByte = [&] + { + return uint8_t(random.nextInt(256) & 0x7f); + }; + + const auto message = [&] + { + switch (length) + { + case 1: + return MidiMessage(firstByte); + case 2: + return MidiMessage(firstByte, getDataByte()); + case 3: + return MidiMessage(firstByte, getDataByte(), getDataByte()); + } + + return MidiMessage(); + }(); + + fn(message); + } + } + + static bool equal(const MidiMessage& a, const MidiMessage& b) noexcept + { + return a.getRawDataSize() == b.getRawDataSize() + && std::equal(a.getRawData(), a.getRawData() + a.getRawDataSize(), b.getRawData()); + } + + static bool equal(const MidiBuffer& a, const MidiBuffer& b) noexcept + { + return a.data == b.data; + } +}; + +} // namespace yup::universal_midi_packets + +TEST_F(UniversalMidiPacketTest, ShortBytestreamMidiMessagesCanBeRoundTrippedThroughUMPConverter) +{ + Midi1ToBytestreamTranslator translator(0); + + forEachNonSysExTestMessage(random, [&](const MidiMessage& m) + { + const auto packets = toMidi1(m); + EXPECT_EQ(packets.size(), 1u); + + // Make sure that the message type is correct + const auto msgType = Utils::getMessageType(packets.data()[0]); + EXPECT_EQ(msgType, ((m.getRawData()[0] >> 0x4) == 0xf ? 0x1 : 0x2)); + + translator.dispatch(View { packets.data() }, + 0, + [&](const BytestreamMidiView& roundTripped) + { + EXPECT_TRUE(equal(m, roundTripped.getMessage())); + }); + }); +} + +TEST_F(UniversalMidiPacketTest, BytestreamSysExConvertsToUniversalPackets) +{ + { + // Zero length message + const auto packets = toMidi1(createRandomSysEx(random, 0)); + EXPECT_EQ(packets.size(), 2u); + + EXPECT_EQ(packets.data()[0], 0x30000000u); + EXPECT_EQ(packets.data()[1], 0x00000000u); + } + + { + const auto message = createRandomSysEx(random, 1); + const auto packets = toMidi1(message); + EXPECT_EQ(packets.size(), 2u); + + const auto* sysEx = message.getSysExData(); + EXPECT_EQ(packets.data()[0], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x01 }, std::byte { sysEx[0] }, std::byte { 0 })); + EXPECT_EQ(packets.data()[1], 0x00000000u); + } + + { + const auto message = createRandomSysEx(random, 6); + const auto packets = toMidi1(message); + EXPECT_EQ(packets.size(), 2u); + + const auto* sysEx = message.getSysExData(); + EXPECT_EQ(packets.data()[0], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x06 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); + EXPECT_EQ(packets.data()[1], Utils::bytesToWord(std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); + } + + { + const auto message = createRandomSysEx(random, 12); + const auto packets = toMidi1(message); + EXPECT_EQ(packets.size(), 4u); + + const auto* sysEx = message.getSysExData(); + EXPECT_EQ(packets.data()[0], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x16 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); + EXPECT_EQ(packets.data()[1], Utils::bytesToWord(std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); + EXPECT_EQ(packets.data()[2], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x36 }, std::byte { sysEx[6] }, std::byte { sysEx[7] })); + EXPECT_EQ(packets.data()[3], Utils::bytesToWord(std::byte { sysEx[8] }, std::byte { sysEx[9] }, std::byte { sysEx[10] }, std::byte { sysEx[11] })); + } + + { + const auto message = createRandomSysEx(random, 13); + const auto packets = toMidi1(message); + EXPECT_EQ(packets.size(), 6u); + + const auto* sysEx = message.getSysExData(); + EXPECT_EQ(packets.data()[0], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x16 }, std::byte { sysEx[0] }, std::byte { sysEx[1] })); + EXPECT_EQ(packets.data()[1], Utils::bytesToWord(std::byte { sysEx[2] }, std::byte { sysEx[3] }, std::byte { sysEx[4] }, std::byte { sysEx[5] })); + EXPECT_EQ(packets.data()[2], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x26 }, std::byte { sysEx[6] }, std::byte { sysEx[7] })); + EXPECT_EQ(packets.data()[3], Utils::bytesToWord(std::byte { sysEx[8] }, std::byte { sysEx[9] }, std::byte { sysEx[10] }, std::byte { sysEx[11] })); + EXPECT_EQ(packets.data()[4], Utils::bytesToWord(std::byte { 0x30 }, std::byte { 0x31 }, std::byte { sysEx[12] }, std::byte { 0 })); + EXPECT_EQ(packets.data()[5], 0x00000000u); + } +} + +TEST_F(UniversalMidiPacketTest, LongSysExBytestreamMidiMessagesCanBeRoundTrippedThroughUMPConverter) +{ + ToBytestreamDispatcher converter(0); + Packets packets; + + const auto checkRoundTrip = [&](const MidiBuffer& expected) + { + for (const auto meta : expected) + Conversion::toMidi1(ump::BytestreamMidiView(meta), [&](const auto p) + { + packets.add(p); + }); + + MidiBuffer output; + converter.dispatch(packets.data(), + packets.data() + packets.size(), + 0, + [&](const BytestreamMidiView& roundTripped) + { + output.addEvent(roundTripped.getMessage(), int(roundTripped.timestamp)); + }); + packets.clear(); + + EXPECT_TRUE(equal(expected, output)); + }; + + for (auto length : { 0, 1, 2, 3, 4, 5, 6, 7, 13, 20, 100, 1000 }) + { + MidiBuffer expected; + expected.addEvent(createRandomSysEx(random, size_t(length)), 0); + checkRoundTrip(expected); + } +} + +TEST_F(UniversalMidiPacketTest, UMPSysEx7MessagesInterspersedWithUtilityMessagesConvertToBytestream) +{ + ToBytestreamDispatcher converter(0); + + const auto sysEx = createRandomSysEx(random, 100); + const auto originalPackets = toMidi1(sysEx); + + Packets modifiedPackets; + + const auto addRandomUtilityUMP = [&] + { + const auto newPacket = createRandomUtilityUMP(random); + modifiedPackets.add(View(newPacket.data())); + }; + + for (const auto& packet : originalPackets) + { + addRandomUtilityUMP(); + modifiedPackets.add(packet); + addRandomUtilityUMP(); + } + + MidiBuffer output; + converter.dispatch(modifiedPackets.data(), + modifiedPackets.data() + modifiedPackets.size(), + 0, + [&](const BytestreamMidiView& roundTripped) + { + output.addEvent(roundTripped.getMessage(), int(roundTripped.timestamp)); + }); + + // All Utility messages should have been ignored + EXPECT_EQ(output.getNumEvents(), 1); + + for (const auto meta : output) + EXPECT_TRUE(equal(meta.getMessage(), sysEx)); +} + +// Due to the size and complexity of the original test, I'll include a few key tests here +// Additional tests would follow the same pattern... + +TEST_F(UniversalMidiPacketTest, Midi2ToMidi1NoteOnConversions) +{ + { + Packets midi2; + midi2.add(PacketX2 { 0x41946410, 0x12345678 }); + + Packets midi1; + midi1.add(PacketX1 { 0x21946409 }); + + checkMidi2ToMidi1Conversion(midi2, midi1); + } + + { + // If the velocity is close to 0, the output velocity should still be 1 + Packets midi2; + midi2.add(PacketX2 { 0x4295327f, 0x00345678 }); + + Packets midi1; + midi1.add(PacketX1 { 0x22953201 }); + + checkMidi2ToMidi1Conversion(midi2, midi1); + } +} + +TEST_F(UniversalMidiPacketTest, Midi2ToMidi1NoteOffConversion) +{ + Packets midi2; + midi2.add(PacketX2 { 0x448b0520, 0xfedcba98 }); + + Packets midi1; + midi1.add(PacketX1 { 0x248b057f }); + + checkMidi2ToMidi1Conversion(midi2, midi1); +} + +TEST_F(UniversalMidiPacketTest, WideningConversionsWork) +{ + // This is similar to the 'slow' example code from the MIDI 2.0 spec + const auto baselineScale = [](uint32_t srcVal, uint32_t srcBits, uint32_t dstBits) + { + const auto scaleBits = (uint32_t)(dstBits - srcBits); + + auto bitShiftedValue = (uint32_t)(srcVal << scaleBits); + + const auto srcCenter = (uint32_t)(1 << (srcBits - 1)); + + if (srcVal <= srcCenter) + return bitShiftedValue; + + const auto repeatBits = (uint32_t)(srcBits - 1); + const auto repeatMask = (uint32_t)((1 << repeatBits) - 1); + + auto repeatValue = (uint32_t)(srcVal & repeatMask); + + if (scaleBits > repeatBits) + repeatValue <<= scaleBits - repeatBits; + else + repeatValue >>= repeatBits - scaleBits; + + while (repeatValue != 0) + { + bitShiftedValue |= repeatValue; + repeatValue >>= repeatBits; + } + + return bitShiftedValue; + }; + + const auto baselineScale7To8 = [&](uint8_t in) + { + return baselineScale(in, 7, 8); + }; + + const auto baselineScale7To16 = [&](uint8_t in) + { + return baselineScale(in, 7, 16); + }; + + const auto baselineScale14To16 = [&](uint16_t in) + { + return baselineScale(in, 14, 16); + }; + + const auto baselineScale7To32 = [&](uint8_t in) + { + return baselineScale(in, 7, 32); + }; + + const auto baselineScale14To32 = [&](uint16_t in) + { + return baselineScale(in, 14, 32); + }; + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint8_t)random.nextInt(0x80); + EXPECT_EQ(Conversion::scaleTo8(rand), baselineScale7To8(rand)); + } + + EXPECT_EQ(Conversion::scaleTo16((uint8_t)0x00), 0x0000); + EXPECT_EQ(Conversion::scaleTo16((uint8_t)0x0a), 0x1400); + EXPECT_EQ(Conversion::scaleTo16((uint8_t)0x40), 0x8000); + EXPECT_EQ(Conversion::scaleTo16((uint8_t)0x57), 0xaeba); + EXPECT_EQ(Conversion::scaleTo16((uint8_t)0x7f), 0xffff); + + for (auto i = 0; i != 100; ++i) + { + const auto rand = (uint8_t)random.nextInt(0x80); + EXPECT_EQ(Conversion::scaleTo16(rand), baselineScale7To16(rand)); + } +} + +#endif diff --git a/tests/yup_core/yup_BufferedInputStream.cpp b/tests/yup_core/yup_BufferedInputStream.cpp new file mode 100644 index 000000000..4ec5ea990 --- /dev/null +++ b/tests/yup_core/yup_BufferedInputStream.cpp @@ -0,0 +1,174 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +class BufferedInputStreamTests : public ::testing::Test +{ +protected: + template + static void applyImpl (Fn&& fn, std::index_sequence, Values&& values) + { + fn (std::get (values)...); + } + + template + static void apply (Fn&& fn, std::tuple values) + { + applyImpl (fn, std::make_index_sequence(), values); + } + + template + static void allCombinationsImpl (Fn&& fn, Values&& values) + { + apply (fn, values); + } + + template + static void allCombinationsImpl (Fn&& fn, Values&& values, Range&& range, Ranges&&... ranges) + { + for (auto& item : range) + allCombinationsImpl (fn, std::tuple_cat (values, std::tie (item)), ranges...); + } + + template + static void allCombinations (Fn&& fn, Ranges&&... ranges) + { + allCombinationsImpl (fn, std::tie(), ranges...); + } + + void runTest (const MemoryBlock& data, const int readSize, const bool peek) + { + MemoryInputStream mi (data, true); + + BufferedInputStream stream (mi, jmin (200, (int) data.getSize())); + + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + MemoryBlock readBuffer (data.getSize()); + + while (numBytesRead < data.getSize()) + { + if (peek) + EXPECT_EQ (stream.peekByte(), *(char*) (data.begin() + numBytesRead)); + + const auto startingPos = numBytesRead; + numBytesRead += (size_t) stream.read (readBuffer.begin() + numBytesRead, readSize); + + EXPECT_TRUE (std::equal (readBuffer.begin() + startingPos, + readBuffer.begin() + numBytesRead, + data.begin() + startingPos, + data.begin() + numBytesRead)); + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + + EXPECT_TRUE (readBuffer == data); + + // Skip test + stream.setPosition (0); + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + numBytesRead = 0; + const int numBytesToSkip = 5; + + while (numBytesRead < data.getSize()) + { + EXPECT_EQ (stream.peekByte(), *(char*) (data.begin() + numBytesRead)); + + stream.skipNextBytes (numBytesToSkip); + numBytesRead += numBytesToSkip; + numBytesRead = std::min (numBytesRead, data.getSize()); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + } +}; + +TEST_F (BufferedInputStreamTests, ReadAndSkipCombinations) +{ + const MemoryBlock testBufferA ("abcdefghijklmnopqrstuvwxyz", 26); + + const auto testBufferB = [] + { + MemoryBlock mb { 8192 }; + auto r = Random::getSystemRandom(); + + std::for_each (mb.begin(), mb.end(), [&] (char& item) + { + item = (char) r.nextInt (std::numeric_limits::max()); + }); + + return mb; + }(); + + const MemoryBlock buffers[] { testBufferA, testBufferB }; + const int readSizes[] { 3, 10, 50 }; + const bool shouldPeek[] { false, true }; + + allCombinations ([this] (const MemoryBlock& data, const int readSize, const bool peek) + { + runTest (data, readSize, peek); + }, + buffers, + readSizes, + shouldPeek); +} diff --git a/tests/yup_core/yup_FileInputStream.cpp b/tests/yup_core/yup_FileInputStream.cpp new file mode 100644 index 000000000..30c17ac58 --- /dev/null +++ b/tests/yup_core/yup_FileInputStream.cpp @@ -0,0 +1,128 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +TEST (FileInputStreamTests, OpenStreamNonExistentFile) +{ + auto tempFile = File::createTempFile (".txt"); + EXPECT_FALSE (tempFile.exists()); + + FileInputStream stream (tempFile); + EXPECT_TRUE (stream.failedToOpen()); +} + +TEST (FileInputStreamTests, OpenStreamExistingFile) +{ + auto tempFile = File::createTempFile (".txt"); + tempFile.create(); + EXPECT_TRUE (tempFile.exists()); + + FileInputStream stream (tempFile); + EXPECT_TRUE (stream.openedOk()); +} + +TEST (FileInputStreamTests, Read) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); + File f (File::createTempFile (".txt")); + f.appendData (data.getData(), data.getSize()); + FileInputStream stream (f); + + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + MemoryBlock readBuffer (data.getSize()); + + while (numBytesRead < data.getSize()) + { + numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + + EXPECT_TRUE (readBuffer == data); +} + +TEST (FileInputStreamTests, Skip) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); + File f (File::createTempFile (".txt")); + f.appendData (data.getData(), data.getSize()); + FileInputStream stream (f); + + stream.setPosition (0); + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + const int numBytesToSkip = 5; + + while (numBytesRead < data.getSize()) + { + stream.skipNextBytes (numBytesToSkip); + numBytesRead += numBytesToSkip; + numBytesRead = std::min (numBytesRead, data.getSize()); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + + f.deleteFile(); +} \ No newline at end of file diff --git a/tests/yup_core/yup_FileSearchPath.cpp b/tests/yup_core/yup_FileSearchPath.cpp new file mode 100644 index 000000000..89a1cd1e5 --- /dev/null +++ b/tests/yup_core/yup_FileSearchPath.cpp @@ -0,0 +1,77 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +TEST (FileSearchPathTests, RemoveRedundantPaths) +{ +#if YUP_WINDOWS + const String prefix = "C:"; +#else + const String prefix = ""; +#endif + + { + FileSearchPath fsp { prefix + "/a/b/c/d;" + prefix + "/a/b/c/e;" + prefix + "/a/b/c" }; + fsp.removeRedundantPaths(); + EXPECT_EQ (fsp.toString(), prefix + "/a/b/c"); + } + + { + FileSearchPath fsp { prefix + "/a/b/c;" + prefix + "/a/b/c/d;" + prefix + "/a/b/c/e" }; + fsp.removeRedundantPaths(); + EXPECT_EQ (fsp.toString(), prefix + "/a/b/c"); + } + + { + FileSearchPath fsp { prefix + "/a/b/c/d;" + prefix + "/a/b/c;" + prefix + "/a/b/c/e" }; + fsp.removeRedundantPaths(); + EXPECT_EQ (fsp.toString(), prefix + "/a/b/c"); + } + + { + FileSearchPath fsp { "%FOO%;" + prefix + "/a/b/c;%FOO%;" + prefix + "/a/b/c/d" }; + fsp.removeRedundantPaths(); + EXPECT_EQ (fsp.toString(), "%FOO%;" + prefix + "/a/b/c"); + } +} \ No newline at end of file diff --git a/tests/yup_core/yup_IPAddress.cpp b/tests/yup_core/yup_IPAddress.cpp new file mode 100644 index 000000000..8ee4cdfa2 --- /dev/null +++ b/tests/yup_core/yup_IPAddress.cpp @@ -0,0 +1,106 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +TEST (IPAddressTests, Constructors) +{ + // Default IPAdress should be null + IPAddress defaultConstructed; + EXPECT_TRUE (defaultConstructed.isNull()); + + auto local = IPAddress::local(); + EXPECT_FALSE (local.isNull()); + + IPAddress ipv4 { 1, 2, 3, 4 }; + EXPECT_FALSE (ipv4.isNull()); + EXPECT_FALSE (ipv4.isIPv6); + EXPECT_EQ (ipv4.toString(), "1.2.3.4"); +} + +TEST (IPAddressTests, FindAllAddresses) +{ + Array ipv4Addresses; + Array allAddresses; + + IPAddress::findAllAddresses (ipv4Addresses, false); + IPAddress::findAllAddresses (allAddresses, true); + + EXPECT_GE (allAddresses.size(), ipv4Addresses.size()); + + for (auto& a : ipv4Addresses) + { + EXPECT_FALSE (a.isNull()); + EXPECT_FALSE (a.isIPv6); + } + + for (auto& a : allAddresses) + { + EXPECT_FALSE (a.isNull()); + } +} + +TEST (IPAddressTests, FindBroadcastAddress) +{ + Array addresses; + + // Only IPv4 interfaces have broadcast + IPAddress::findAllAddresses (addresses, false); + + for (auto& a : addresses) + { + EXPECT_FALSE (a.isNull()); + + auto broadcastAddress = IPAddress::getInterfaceBroadcastAddress (a); + + // If we retrieve an address, it should be an IPv4 address + if (! broadcastAddress.isNull()) + { + EXPECT_FALSE (a.isIPv6); + } + } + + // Expect to fail to find a broadcast for this address + IPAddress address { 1, 2, 3, 4 }; + EXPECT_TRUE (IPAddress::getInterfaceBroadcastAddress (address).isNull()); +} \ No newline at end of file diff --git a/tests/yup_core/yup_MemoryInputStream.cpp b/tests/yup_core/yup_MemoryInputStream.cpp new file mode 100644 index 000000000..9b3e35375 --- /dev/null +++ b/tests/yup_core/yup_MemoryInputStream.cpp @@ -0,0 +1,156 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ +String createRandomWideCharString (Random& r) +{ + yup_wchar buffer[50] = { 0 }; + + for (int i = 0; i < numElementsInArray (buffer) - 1; ++i) + { + if (r.nextBool()) + { + do + { + buffer[i] = (yup_wchar) (1 + r.nextInt (0x10ffff - 1)); + } while (! CharPointer_UTF16::canRepresent (buffer[i])); + } + else + buffer[i] = (yup_wchar) (1 + r.nextInt (0xff)); + } + + return CharPointer_UTF32 (buffer); +} +} // namespace + +TEST (MemoryInputStreamTests, Basics) +{ + Random r = Random::getSystemRandom(); + + int randomInt = r.nextInt(); + int64 randomInt64 = r.nextInt64(); + double randomDouble = r.nextDouble(); + String randomString (createRandomWideCharString (r)); + + MemoryOutputStream mo; + mo.writeInt (randomInt); + mo.writeIntBigEndian (randomInt); + mo.writeCompressedInt (randomInt); + mo.writeString (randomString); + mo.writeInt64 (randomInt64); + mo.writeInt64BigEndian (randomInt64); + mo.writeDouble (randomDouble); + mo.writeDoubleBigEndian (randomDouble); + + MemoryInputStream mi (mo.getData(), mo.getDataSize(), false); + EXPECT_EQ (mi.readInt(), randomInt); + EXPECT_EQ (mi.readIntBigEndian(), randomInt); + EXPECT_EQ (mi.readCompressedInt(), randomInt); + EXPECT_EQ (mi.readString(), randomString); + EXPECT_EQ (mi.readInt64(), randomInt64); + EXPECT_EQ (mi.readInt64BigEndian(), randomInt64); + EXPECT_EQ (mi.readDouble(), randomDouble); + EXPECT_EQ (mi.readDoubleBigEndian(), randomDouble); +} + +TEST (MemoryInputStreamTests, Read) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); + MemoryInputStream stream (data, true); + + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + MemoryBlock readBuffer (data.getSize()); + + while (numBytesRead < data.getSize()) + { + numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + + EXPECT_TRUE (readBuffer == data); +} + +TEST (MemoryInputStreamTests, Skip) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyz", 26); + MemoryInputStream stream (data, true); + + stream.setPosition (0); + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + const int numBytesToSkip = 5; + + while (numBytesRead < data.getSize()) + { + stream.skipNextBytes (numBytesToSkip); + numBytesRead += numBytesToSkip; + numBytesRead = std::min (numBytesRead, data.getSize()); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (data.getSize() - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == data.getSize())); + } + + EXPECT_EQ (stream.getPosition(), (int64) data.getSize()); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); +} diff --git a/tests/yup_core/yup_NamedPipe.cpp b/tests/yup_core/yup_NamedPipe.cpp new file mode 100644 index 000000000..af2ab770b --- /dev/null +++ b/tests/yup_core/yup_NamedPipe.cpp @@ -0,0 +1,243 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#if ! YUP_WASM + +using namespace yup; + +namespace +{ +struct NamedPipeThread : public Thread +{ + NamedPipeThread (const String& tName, const String& pName, bool shouldCreatePipe, WaitableEvent& completed) + : Thread (tName) + , pipeName (pName) + , workCompleted (completed) + { + if (shouldCreatePipe) + pipe.createNewPipe (pipeName); + else + pipe.openExisting (pipeName); + } + + NamedPipe pipe; + const String& pipeName; + WaitableEvent& workCompleted; + + int result = -2; +}; + +struct SenderThread final : public NamedPipeThread +{ + SenderThread (const String& pName, bool shouldCreatePipe, WaitableEvent& completed, int sData) + : NamedPipeThread ("NamePipeSender", pName, shouldCreatePipe, completed) + , sendData (sData) + { + } + + ~SenderThread() override + { + stopThread (100); + } + + void run() override + { + result = pipe.write (&sendData, sizeof (sendData), 2000); + workCompleted.signal(); + } + + const int sendData; +}; + +struct ReceiverThread final : public NamedPipeThread +{ + ReceiverThread (const String& pName, bool shouldCreatePipe, WaitableEvent& completed) + : NamedPipeThread ("NamePipeSender", pName, shouldCreatePipe, completed) + { + } + + ~ReceiverThread() override + { + stopThread (100); + } + + void run() override + { + result = pipe.read (&recvData, sizeof (recvData), 2000); + workCompleted.signal(); + } + + int recvData = -2; +}; +} // namespace + +class NamedPipeTests : public ::testing::Test +{ +protected: + void SetUp() override + { + pipeName = "TestPipe" + String ((intptr_t) Thread::getCurrentThreadId()); + } + + String pipeName; +}; + +TEST_F (NamedPipeTests, PreTestCleanup) +{ + NamedPipe pipe; + EXPECT_TRUE (pipe.createNewPipe (pipeName, false)); +} + +TEST_F (NamedPipeTests, CreatePipe) +{ + NamedPipe pipe; + EXPECT_FALSE (pipe.isOpen()); + + EXPECT_TRUE (pipe.createNewPipe (pipeName, true)); + EXPECT_TRUE (pipe.isOpen()); + + EXPECT_TRUE (pipe.createNewPipe (pipeName, false)); + EXPECT_TRUE (pipe.isOpen()); + + NamedPipe otherPipe; + EXPECT_FALSE (otherPipe.createNewPipe (pipeName, true)); + EXPECT_FALSE (otherPipe.isOpen()); +} + +TEST_F (NamedPipeTests, ExistingPipe) +{ + NamedPipe pipe; + + EXPECT_FALSE (pipe.openExisting (pipeName)); + EXPECT_FALSE (pipe.isOpen()); + + EXPECT_TRUE (pipe.createNewPipe (pipeName, true)); + + NamedPipe otherPipe; + EXPECT_TRUE (otherPipe.openExisting (pipeName)); + EXPECT_TRUE (otherPipe.isOpen()); +} + +TEST_F (NamedPipeTests, ReceiveMessageCreatedPipe) +{ + NamedPipe pipe; + EXPECT_TRUE (pipe.createNewPipe (pipeName, true)); + + const int sendData = 4684682; + WaitableEvent senderFinished; + SenderThread sender (pipeName, false, senderFinished, sendData); + + sender.startThread(); + + int recvData = -1; + auto bytesRead = pipe.read (&recvData, sizeof (recvData), 2000); + + EXPECT_TRUE (senderFinished.wait (4000)); + + EXPECT_EQ (bytesRead, (int) sizeof (recvData)); + EXPECT_EQ (sender.result, (int) sizeof (sendData)); + EXPECT_EQ (recvData, sendData); +} + +TEST_F (NamedPipeTests, ReceiveMessageExistingPipe) +{ + const int sendData = 4684682; + WaitableEvent senderFinished; + SenderThread sender (pipeName, true, senderFinished, sendData); + + NamedPipe pipe; + EXPECT_TRUE (pipe.openExisting (pipeName)); + + sender.startThread(); + + int recvData = -1; + auto bytesRead = pipe.read (&recvData, sizeof (recvData), 2000); + + EXPECT_TRUE (senderFinished.wait (4000)); + + EXPECT_EQ (bytesRead, (int) sizeof (recvData)); + EXPECT_EQ (sender.result, (int) sizeof (sendData)); + EXPECT_EQ (recvData, sendData); +} + +TEST_F (NamedPipeTests, SendMessageCreatedPipe) +{ + NamedPipe pipe; + EXPECT_TRUE (pipe.createNewPipe (pipeName, true)); + + const int sendData = 4684682; + WaitableEvent receiverFinished; + ReceiverThread receiver (pipeName, false, receiverFinished); + + receiver.startThread(); + + auto bytesWritten = pipe.write (&sendData, sizeof (sendData), 2000); + + EXPECT_TRUE (receiverFinished.wait (4000)); + + EXPECT_EQ (bytesWritten, (int) sizeof (sendData)); + EXPECT_EQ (receiver.result, (int) sizeof (receiver.recvData)); + EXPECT_EQ (receiver.recvData, sendData); +} + +TEST_F (NamedPipeTests, SendMessageExistingPipe) +{ + const int sendData = 4684682; + WaitableEvent receiverFinished; + ReceiverThread receiver (pipeName, true, receiverFinished); + + NamedPipe pipe; + EXPECT_TRUE (pipe.openExisting (pipeName)); + + receiver.startThread(); + + auto bytesWritten = pipe.write (&sendData, sizeof (sendData), 2000); + + EXPECT_TRUE (receiverFinished.wait (4000)); + + EXPECT_EQ (bytesWritten, (int) sizeof (sendData)); + EXPECT_EQ (receiver.result, (int) sizeof (receiver.recvData)); + EXPECT_EQ (receiver.recvData, sendData); +} + +#endif diff --git a/tests/yup_core/yup_Socket.cpp b/tests/yup_core/yup_Socket.cpp new file mode 100644 index 000000000..4be10cae7 --- /dev/null +++ b/tests/yup_core/yup_Socket.cpp @@ -0,0 +1,98 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +#if 0 +TEST (SocketTests, StreamingSocket) +{ + auto localHost = IPAddress::local(); + int portNum = 12345; + + StreamingSocket socketServer; + + EXPECT_FALSE (socketServer.isConnected()); + EXPECT_TRUE (socketServer.getHostName().isEmpty()); + EXPECT_EQ (socketServer.getBoundPort(), -1); + EXPECT_EQ (static_cast (socketServer.getRawSocketHandle()), invalidSocket); + + EXPECT_TRUE (socketServer.createListener (portNum, localHost.toString())); + + StreamingSocket socket; + + EXPECT_TRUE (socket.connect (localHost.toString(), portNum)); + + EXPECT_TRUE (socket.isConnected()); + EXPECT_EQ (socket.getHostName(), localHost.toString()); + EXPECT_NE (socket.getBoundPort(), -1); + EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); + + socket.close(); + + EXPECT_FALSE (socket.isConnected()); + EXPECT_TRUE (socket.getHostName().isEmpty()); + EXPECT_EQ (socket.getBoundPort(), -1); + EXPECT_EQ (static_cast (socket.getRawSocketHandle()), invalidSocket); +} + +TEST (SocketTests, DatagramSocket) +{ + auto localHost = IPAddress::local(); + int portNum = 12345; + + DatagramSocket socket; + + EXPECT_EQ (socket.getBoundPort(), -1); + EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); + + EXPECT_TRUE (socket.bindToPort (portNum, localHost.toString())); + + EXPECT_EQ (socket.getBoundPort(), portNum); + EXPECT_NE (static_cast (socket.getRawSocketHandle()), invalidSocket); + + socket.shutdown(); + + EXPECT_EQ (socket.getBoundPort(), -1); + EXPECT_EQ (static_cast (socket.getRawSocketHandle()), invalidSocket); +} +#endif diff --git a/tests/yup_core/yup_SubregionStream.cpp b/tests/yup_core/yup_SubregionStream.cpp new file mode 100644 index 000000000..478b1acd8 --- /dev/null +++ b/tests/yup_core/yup_SubregionStream.cpp @@ -0,0 +1,114 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +TEST (SubregionInputStreamTests, Read) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", 52); + MemoryInputStream mi (data, true); + + const int offset = Random::getSystemRandom().nextInt ((int) data.getSize()); + const size_t subregionSize = data.getSize() - (size_t) offset; + + SubregionStream stream (&mi, offset, (int) subregionSize, false); + + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) subregionSize); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + MemoryBlock readBuffer (subregionSize); + + while (numBytesRead < subregionSize) + { + numBytesRead += (size_t) stream.read (&readBuffer[numBytesRead], 3); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (subregionSize - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == subregionSize)); + } + + EXPECT_EQ (stream.getPosition(), (int64) subregionSize); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); + + const MemoryBlock memoryBlockToCheck (data.begin() + (size_t) offset, data.getSize() - (size_t) offset); + EXPECT_TRUE (readBuffer == memoryBlockToCheck); +} + +TEST (SubregionInputStreamTests, Skip) +{ + const MemoryBlock data ("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", 52); + MemoryInputStream mi (data, true); + + const int offset = Random::getSystemRandom().nextInt ((int) data.getSize()); + const size_t subregionSize = data.getSize() - (size_t) offset; + + SubregionStream stream (&mi, offset, (int) subregionSize, false); + + stream.setPosition (0); + EXPECT_EQ (stream.getPosition(), (int64) 0); + EXPECT_EQ (stream.getTotalLength(), (int64) subregionSize); + EXPECT_EQ (stream.getNumBytesRemaining(), stream.getTotalLength()); + EXPECT_FALSE (stream.isExhausted()); + + size_t numBytesRead = 0; + const int64 numBytesToSkip = 5; + + while (numBytesRead < subregionSize) + { + stream.skipNextBytes (numBytesToSkip); + numBytesRead += numBytesToSkip; + numBytesRead = std::min (numBytesRead, subregionSize); + + EXPECT_EQ (stream.getPosition(), (int64) numBytesRead); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) (subregionSize - numBytesRead)); + EXPECT_EQ (stream.isExhausted(), (numBytesRead == subregionSize)); + } + + EXPECT_EQ (stream.getPosition(), (int64) subregionSize); + EXPECT_EQ (stream.getNumBytesRemaining(), (int64) 0); + EXPECT_TRUE (stream.isExhausted()); +} diff --git a/tests/yup_core/yup_Time.cpp b/tests/yup_core/yup_Time.cpp index 16565338b..439119ce4 100644 --- a/tests/yup_core/yup_Time.cpp +++ b/tests/yup_core/yup_Time.cpp @@ -193,6 +193,12 @@ TEST (TimeTests, ToISO8601) { Time time (1625000000000); EXPECT_FALSE (time.toISO8601 (true).isEmpty()); + + if (Time {}.getTimeZone() == "GMT") + { + EXPECT_EQ (Time (2025, 2, 30, 0, 59, 59, 999, false).toISO8601 (true), "2025-03-30T00:59:59.999Z"); + EXPECT_EQ (Time (2025, 2, 30, 1, 00, 00, 000, false).toISO8601 (true), "2025-03-30T02:00:00.000+01:00"); + } } TEST (TimeTests, FromISO8601) @@ -201,6 +207,15 @@ TEST (TimeTests, FromISO8601) EXPECT_EQ (time.getYear(), 2021); EXPECT_EQ (time.getMonth(), 5); // June EXPECT_EQ (time.getDayOfMonth(), 29); + + EXPECT_EQ (Time::fromISO8601 (Time (0).toISO8601 (true)), Time (0)); +} + +TEST (TimeTests, BackAndForthISO8601) +{ + Time zero (0); + auto t1 = zero.toISO8601 (true); + EXPECT_EQ (t1, Time::fromISO8601 (t1).toISO8601 (true)); } TEST (TimeTests, AddRelativeTime) @@ -301,7 +316,7 @@ TEST (TimeTests, DISABLED_SetSystemTimeToThisTime) EXPECT_TRUE (now.setSystemTimeToThisTime()); } -TEST (TimeTests, xxx) +TEST (TimeTests, StockTests) { Time t = Time::getCurrentTime(); EXPECT_TRUE (t > Time()); diff --git a/tests/yup_graphics/yup_Point.cpp b/tests/yup_graphics/yup_Point.cpp index 86a45fc7f..75f536000 100644 --- a/tests/yup_graphics/yup_Point.cpp +++ b/tests/yup_graphics/yup_Point.cpp @@ -106,7 +106,9 @@ TEST (PointTests, Distance_Calculations) EXPECT_FLOAT_EQ (p1.distanceTo (p2), 5.0f); EXPECT_FLOAT_EQ (p1.distanceToSquared (p2), 25.0f); EXPECT_FLOAT_EQ (p1.horizontalDistanceTo (p2), 3.0f); + EXPECT_FLOAT_EQ (p2.horizontalDistanceTo (p1), -3.0f); EXPECT_FLOAT_EQ (p1.verticalDistanceTo (p2), 4.0f); + EXPECT_FLOAT_EQ (p2.verticalDistanceTo (p1), -4.0f); EXPECT_FLOAT_EQ (p1.manhattanDistanceTo (p2), 7.0f); } @@ -693,8 +695,10 @@ TEST (PointTests, DistanceCalculations_EdgeCases) // Test with negative coordinates Point negative (-3.0f, -4.0f); EXPECT_FLOAT_EQ (origin.distanceTo (negative), 5.0f); - EXPECT_FLOAT_EQ (origin.horizontalDistanceTo (negative), 3.0f); - EXPECT_FLOAT_EQ (origin.verticalDistanceTo (negative), 4.0f); + EXPECT_FLOAT_EQ (origin.horizontalDistanceTo (negative), -3.0f); + EXPECT_FLOAT_EQ (negative.horizontalDistanceTo (origin), 3.0f); + EXPECT_FLOAT_EQ (origin.verticalDistanceTo (negative), -4.0f); + EXPECT_FLOAT_EQ (negative.verticalDistanceTo (origin), 4.0f); // Test with infinity Point inf (std::numeric_limits::infinity(), 0.0f);