From 42d6dacc5a5bb1dcdc9121d0493ba0728eb1746c Mon Sep 17 00:00:00 2001 From: emurray2 Date: Fri, 3 Apr 2026 00:46:34 -0500 Subject: [PATCH 1/2] Improve sequencer tests Fixed the looping issue with unfinished notes by checking in each render call to see if events happen outside the loop window and scheduling them to the end of frames --- .../CAudioKitEX/Sequencing/SequencerEngine.mm | 7 ++- Tests/AudioKitEXTests/SequenceTests.swift | 4 +- .../SequencerEngineTests.swift | 44 ++++++++++--------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Sources/CAudioKitEX/Sequencing/SequencerEngine.mm b/Sources/CAudioKitEX/Sequencing/SequencerEngine.mm index 8634861..508011f 100644 --- a/Sources/CAudioKitEX/Sequencing/SequencerEngine.mm +++ b/Sources/CAudioKitEX/Sequencing/SequencerEngine.mm @@ -174,7 +174,6 @@ void process(AUAudioFrameCount frameCount) { for (auto& event : events) { // go through every event int triggerTime = beatToSamples(event.beat); - if (currentEndSample > lengthInSamples() && data->settings.loopEnabled) { // this buffer extends beyond the length of the loop and looping is on int loopRestartInBuffer = (int)(lengthInSamples() - currentStartSample); @@ -188,7 +187,7 @@ void process(AUAudioFrameCount frameCount) { offset, event.beat); } } else if (currentStartSample == 0 && triggerTime == lengthInSamples() && data->settings.loopEnabled) { - // this event handles the case of skipped last note + // this event handles the case of skipped last note sendMidiData(event.status, event.data1, event.data2, 0, event.beat); } else if (currentStartSample <= triggerTime && triggerTime < currentEndSample) { @@ -196,6 +195,10 @@ void process(AUAudioFrameCount frameCount) { int offset = (int)(triggerTime - currentStartSample); sendMidiData(event.status, event.data1, event.data2, offset, event.beat); + } else if (currentEndSample >= lengthInSamples() && triggerTime > lengthInSamples() && data->settings.loopEnabled) { + // event is happens outside loop window, schedule it at the end of frames + sendMidiData(event.status, event.data1, event.data2, + frameCount, event.beat); } } diff --git a/Tests/AudioKitEXTests/SequenceTests.swift b/Tests/AudioKitEXTests/SequenceTests.swift index c461161..c8bce57 100644 --- a/Tests/AudioKitEXTests/SequenceTests.swift +++ b/Tests/AudioKitEXTests/SequenceTests.swift @@ -25,7 +25,9 @@ class NoteEventSequenceTests: XCTestCase { newNote.noteOff.data2 = 127 newNote.noteOff.beat = 2.0 - XCTAssertEqual(seq, NoteEventSequence(notes: [newNote], events: [], totalDuration: 1.0)) + XCTAssertEqual(seq, NoteEventSequence(notes: [newNote], events: [], totalDuration: 2.0)) + // Even though note duration is 1.0, there is space at beginning of track since note position also 1.0. + // Total duration should be 2.0 } func testRemoveNote() { diff --git a/Tests/AudioKitEXTests/SequencerEngineTests.swift b/Tests/AudioKitEXTests/SequencerEngineTests.swift index dab8911..c90adf2 100644 --- a/Tests/AudioKitEXTests/SequencerEngineTests.swift +++ b/Tests/AudioKitEXTests/SequencerEngineTests.swift @@ -226,6 +226,7 @@ class SequencerEngineTests: XCTestCase { } // events that start late in the loop are stopped after the engine is destroyed + // Or at the end of the loop before new note events occur func testShortNotesAcrossLoop() { var seq = NoteEventSequence() @@ -239,26 +240,29 @@ class SequencerEngineTests: XCTestCase { /// 6 render calls at 120bpm, 44100 buffersize is 12 beats, default loop is 4 beats let events = observerTest(sequence: seq, renderCallCount: 6) - XCTAssertEqual(events.count, 30) - - XCTAssertEqual(events.map { $0.noteNumber! }, [60, 62, 65, 60, 62, 65, - 60, 64, 67, 60, 62, 65, 60, 62, 65, - 60, 64, 67, 60, 62, 65, 60, 62, 65, - 60, 64, 67, - 67, 64, 60]) // engine destroyed - - XCTAssertEqual(events.compactMap { $0.status!.type }, [.noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, - .noteOn, .noteOn, .noteOn, .noteOn, .noteOn, .noteOn, - .noteOff, .noteOff, .noteOff, .noteOn, .noteOn, .noteOn, - .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, - .noteOn, .noteOn, .noteOn, - .noteOff, .noteOff, .noteOff]) // engine destroyed - XCTAssertEqual(events.map { $0.timeStamp }, [0, 0, 0, 0, 0, 0, - 43658, 43658, 43658, 0, 0, 0, - 0, 0, 0, 43658, 43658, 43658, - 0, 0, 0, 0, 0, 0, - 43658, 43658, 43658, - 1, 1, 1]) // engine destroyed + XCTAssertEqual(events.count, 36) + + XCTAssertEqual(events.map { $0.noteNumber! }, [60, 62, 65, 60, 62, 65, // First 3 notes on and off + 60, 64, 67, 60, 64, 67, // Second 3 notes on and off + 60, 62, 65, 60, 62, 65, // Loop #2 first 3 notes on/off + 60, 64, 67, 60, 64, 67, // Loop #2 second 3 on/off + 60, 62, 65, 60, 62, 65, // Loop #3 first 3 on/off + 60, 64, 67, 60, 64, 67]) // Loop #3 second 3 on/off + // Engine cleans up remaining active note events, but there shouldn't be any since they're all handled before loop ends + + XCTAssertEqual(events.compactMap { $0.status!.type }, [.noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, // First 3 + .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, // Second 3 + .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, // First 3 + .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, // Second 3 + .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff, // First 3 + .noteOn, .noteOn, .noteOn, .noteOff, .noteOff, .noteOff]) // Second 3 + // Engine cleans up remaining active note events, but there shouldn't be any since they're all handled before loop ends + XCTAssertEqual(events.map { $0.timeStamp }, [0, 0, 0, 0, 0, 0, // First render call + 43658, 43658, 43658, 44100, 44100, 44100, // Second + 0, 0, 0, 0, 0, 0, // Third + 43658, 43658, 43658, 44100, 44100, 44100, // Fourth + 0, 0, 0, 0, 0, 0, // Fifth + 43658, 43658, 43658, 44100, 44100, 44100,]) // Sixth } } #endif From c249949c271c3b2d1c4d9998e775e8c91bcee30c Mon Sep 17 00:00:00 2001 From: emurray2 Date: Fri, 3 Apr 2026 01:13:34 -0500 Subject: [PATCH 2/2] Update md5 Tested the audio with this one, and it sounded the exact same as before. There may be a minor difference in the md5 since the note off events happen slightly differently, but the sound is still the same. --- Tests/AudioKitEXTests/ValidatedMD5s.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AudioKitEXTests/ValidatedMD5s.swift b/Tests/AudioKitEXTests/ValidatedMD5s.swift index 50c5351..1663aca 100644 --- a/Tests/AudioKitEXTests/ValidatedMD5s.swift +++ b/Tests/AudioKitEXTests/ValidatedMD5s.swift @@ -21,7 +21,7 @@ let validatedMD5s: [String: String] = [ "-[SequencerTrackTests testLoop]": "3a7ebced69ddc6669932f4ee48dabe2b", "-[SequencerTrackTests testOneShot]": "3fbf53f1139a831b3e1a284140c8a53c", "-[SequencerTrackTests testTempo]": "1eb7efc6ea54eafbe616dfa8e1a3ef36", - "-[SequencerTrackTests testNoteBounds]": "f3b3935e30380367c15652c0a76a8a57", + "-[SequencerTrackTests testNoteBounds]": "a044a6482a0329a20ca84a446d599e0b", "-[DryWetMixerTests testBalance0]": "789c1e77803a4f9d10063eb60ca03cea", "-[DryWetMixerTests testBalance1]": "3932bc5d49cbefd4a9dd587d16f4b81c", "-[DryWetMixerTests testDefault]": "45a639729d8698a28f134bbe4ccc9d6c",