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 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",