<commit_before>// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/

#import "TargetConditionals.h"

#if !TARGET_OS_TV

#include "SequencerEngine.h"
#include <vector>
#include <bitset>
#include <stdio.h>
#include <atomic>
#include "Internals/RingBuffer.h"
#include "Internals/AtomicDataPtr.h"

using AudioKit::RingBuffer;

/// NOTE: To support more than a single channel, RunningStatus can be made larger
/// e.g. typedef std::bitset<128 * 16> RunningStatus; would track 16 channels of notes
typedef std::bitset<128> RunningStatus;

struct SequencerEvent {
    bool notesOff = false;
    double seekPosition = NAN;
    double tempo = NAN;
};

struct SequencerData {
    double sampleRate = 44100;
    std::vector<SequenceEvent> events;
    SequenceSettings settings = {
        .maximumPlayCount = 0,
        .length = 4.0,
        .tempo = 120.0,
        .loopEnabled = true,
        .loopCount = 0
    };

    AUScheduleMIDIEventBlock midiBlock = nullptr;
};

struct SequencerEngineImpl;

/// This uses another level of indirection to ensure that SequencerEngineImpl
/// is not destroyed while a render observer is still active.
struct SequencerEngine {
    std::shared_ptr<SequencerEngineImpl> impl;
};

struct SequencerEngineImpl {
    RunningStatus runningStatus;
    long positionInSamples = 0;
    UInt64 framesCounted = 0;
    std::atomic<bool> isStarted{false};

    AtomicDataPtr<SequencerData> data;

    RingBuffer<SequencerEvent> eventQueue;

    // Current position as reported to the UI.
    std::atomic<double> uiPosition{0};

    SequencerEngineImpl() {
        runningStatus.reset();
    }

    ~SequencerEngineImpl() {
        stopAllPlayingNotes();
    }

    int beatToSamples(double beat) const {
        return (int)(beat / data->settings.tempo * 60 * data->sampleRate);
    }

    long lengthInSamples() const {
        return beatToSamples(data->settings.length);
    }

    long positionModulo() const {
        long length = lengthInSamples();
        if (positionInSamples == 0 || length == 0) {
            return 0;
        } else if (positionInSamples < 0) {
            return positionInSamples;
        } else {
            return positionInSamples % length;
        }
    }

    double currentPositionInBeats() const {
        return (double)positionModulo() / data->sampleRate * (data->settings.tempo / 60);
    }

    bool validTriggerTime(double beat) {
        return true;
    }

    void sendMidiData(UInt8 status, UInt8 data1, UInt8 data2, int offset, double time) {
        if(data->midiBlock) {
            UInt8 midiBytes[3] = {status, data1, data2};
            updateRunningStatus(status, data1, data2);
            data->midiBlock(AUEventSampleTimeImmediate + offset, 0, 3, midiBytes);
        }
    }

    /// Update note playing status
    void updateRunningStatus(UInt8 status, UInt8 data1, UInt8 data2) {
        if(status == MIDI_NOTE_OFF) {
            runningStatus.set(data1, 0);
        }
        if(status == MIDI_NOTE_ON) {
            runningStatus.set(data1, 1);
        }
    }

    /// Stop all notes whose running status is currently on
    /// If panic is set to true, a note-off message will be sent for all notes
    void stopAllPlayingNotes(bool panic = false) {
        if(runningStatus.any() || (panic == true)) {
            for(int i = (int)runningStatus.size() - 1; i >= 0; i--) {
                if(runningStatus[i] == 1 || (panic == true)) {
                    sendMidiData(MIDI_NOTE_OFF, (UInt8)i, 0, 1, 0);
                }
            }
        }
    }

    void stop() {
        isStarted = false;
        stopAllPlayingNotes();
    }

    void seekTo(double position) {
        positionInSamples = beatToSamples(position);
    }

    void processEvents() {

        eventQueue.popAll([this](const SequencerEvent& event) {
            if(event.notesOff) {
                stopAllPlayingNotes();
            }

            if(!isnan(event.seekPosition)) {
                seekTo(event.seekPosition);
            }

            if(!isnan(event.tempo)) {
                double lastPosition = currentPositionInBeats(); // 1) save where we are before we manipulate time
                data->settings.tempo = event.tempo;             // 2) manipulate time
                seekTo(lastPosition);                           // 3) go back to where we were before time manipulation
            }
        });

    }

    void process(AUAudioFrameCount frameCount) {

        data.update();
        processEvents();

        if (isStarted) {
            if (positionInSamples >= lengthInSamples()) {
                if (!data->settings.loopEnabled) { //stop if played enough
                    stop();
                    return;
                }
            }

            auto& events = data->events;

            long currentStartSample = positionModulo();
            long currentEndSample = currentStartSample + 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);
                    int samplesOfBufferForNewLoop = frameCount - loopRestartInBuffer;
                    if (triggerTime < samplesOfBufferForNewLoop) {
                        // this event would trigger early enough in the next loop that it should happen in this buffer
                        // ie. this buffer contains events from the previous loop, and the next loop
                        int offset = (int)triggerTime + loopRestartInBuffer;
                        sendMidiData(event.status, event.data1, event.data2,
                                     offset, event.beat);
                    }
                } else if (currentStartSample <= triggerTime && triggerTime < currentEndSample) {
                    // this event is supposed to trigger between currentStartSample and currentEndSample
                    int offset = (int)(triggerTime - currentStartSample);
                    sendMidiData(event.status, event.data1, event.data2,
                                 offset, event.beat);
                }
            }

            positionInSamples += frameCount;
        }
        framesCounted += frameCount;

        uiPosition = currentPositionInBeats();
    }
};

/// Creates the audio-thread-only state for the sequencer.
SequencerEngineRef akSequencerEngineCreate(void) {
    return new SequencerEngine { .impl = std::make_shared<SequencerEngineImpl>() };
}

void akSequencerEngineRelease(SequencerEngineRef engine) {
    engine->impl->stopAllPlayingNotes();
    delete engine;
}

/// Updates the sequence and returns a new render observer.<commit_msg>This commit tries to fix issue #2536<commit_after>// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/

#import "TargetConditionals.h"

#if !TARGET_OS_TV

#include "SequencerEngine.h"
#include <vector>
#include <bitset>
#include <stdio.h>
#include <atomic>
#include "Internals/RingBuffer.h"
#include "Internals/AtomicDataPtr.h"

using AudioKit::RingBuffer;

/// NOTE: To support more than a single channel, RunningStatus can be made larger
/// e.g. typedef std::bitset<128 * 16> RunningStatus; would track 16 channels of notes
typedef std::bitset<128> RunningStatus;

struct SequencerEvent {
    bool notesOff = false;
    double seekPosition = NAN;
    double tempo = NAN;
};

struct SequencerData {
    double sampleRate = 44100;
    std::vector<SequenceEvent> events;
    SequenceSettings settings = {
        .maximumPlayCount = 0,
        .length = 4.0,
        .tempo = 120.0,
        .loopEnabled = true,
        .loopCount = 0
    };

    AUScheduleMIDIEventBlock midiBlock = nullptr;
};

struct SequencerEngineImpl;

/// This uses another level of indirection to ensure that SequencerEngineImpl
/// is not destroyed while a render observer is still active.
struct SequencerEngine {
    std::shared_ptr<SequencerEngineImpl> impl;
};

struct SequencerEngineImpl {
    RunningStatus runningStatus;
    long positionInSamples = 0;
    UInt64 framesCounted = 0;
    std::atomic<bool> isStarted{false};

    AtomicDataPtr<SequencerData> data;

    RingBuffer<SequencerEvent> eventQueue;

    // Current position as reported to the UI.
    std::atomic<double> uiPosition{0};

    SequencerEngineImpl() {
        runningStatus.reset();
    }

    ~SequencerEngineImpl() {
        stopAllPlayingNotes();
    }

    int beatToSamples(double beat) const {
        return (int)(beat / data->settings.tempo * 60 * data->sampleRate);
    }

    long lengthInSamples() const {
        return beatToSamples(data->settings.length);
    }

    long positionModulo() const {
        long length = lengthInSamples();
        if (positionInSamples == 0 || length == 0) {
            return 0;
        } else if (positionInSamples < 0) {
            return positionInSamples;
        } else {
            return positionInSamples % length;
        }
    }

    double currentPositionInBeats() const {
        return (double)positionModulo() / data->sampleRate * (data->settings.tempo / 60);
    }

    bool validTriggerTime(double beat) {
        return true;
    }

    void sendMidiData(UInt8 status, UInt8 data1, UInt8 data2, int offset, double time) {
        if(data->midiBlock) {
            UInt8 midiBytes[3] = {status, data1, data2};
            updateRunningStatus(status, data1, data2);
            data->midiBlock(AUEventSampleTimeImmediate + offset, 0, 3, midiBytes);
        }
    }

    /// Update note playing status
    void updateRunningStatus(UInt8 status, UInt8 data1, UInt8 data2) {
        if(status == MIDI_NOTE_OFF) {
            runningStatus.set(data1, 0);
        }
        if(status == MIDI_NOTE_ON) {
            runningStatus.set(data1, 1);
        }
    }

    /// Stop all notes whose running status is currently on
    /// If panic is set to true, a note-off message will be sent for all notes
    void stopAllPlayingNotes(bool panic = false) {
        if(runningStatus.any() || (panic == true)) {
            for(int i = (int)runningStatus.size() - 1; i >= 0; i--) {
                if(runningStatus[i] == 1 || (panic == true)) {
                    sendMidiData(MIDI_NOTE_OFF, (UInt8)i, 0, 1, 0);
                }
            }
        }
    }

    void stop() {
        isStarted = false;
        stopAllPlayingNotes();
    }

    void seekTo(double position) {
        positionInSamples = beatToSamples(position);
    }

    void processEvents() {

        eventQueue.popAll([this](const SequencerEvent& event) {
            if(event.notesOff) {
                stopAllPlayingNotes();
            }

            if(!isnan(event.seekPosition)) {
                seekTo(event.seekPosition);
            }

            if(!isnan(event.tempo)) {
                double lastPosition = currentPositionInBeats(); // 1) save where we are before we manipulate time
                data->settings.tempo = event.tempo;             // 2) manipulate time
                seekTo(lastPosition);                           // 3) go back to where we were before time manipulation
            }
        });

    }

    void process(AUAudioFrameCount frameCount) {

        data.update();
        processEvents();

        if (isStarted) {
            if (positionInSamples >= lengthInSamples()) {
                if (!data->settings.loopEnabled) { //stop if played enough
                    stop();
                    return;
                }
            }

            auto& events = data->events;

            long currentStartSample = positionModulo();
            long currentEndSample = currentStartSample + 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);
                    int samplesOfBufferForNewLoop = frameCount - loopRestartInBuffer;
                    int triggerTimeInLoop = triggerTime % lengthInSamples();
                    if (triggerTimeInLoop < samplesOfBufferForNewLoop) {
                        // this event would trigger early enough in the next loop that it should happen in this buffer
                        // ie. this buffer contains events from the previous loop, and the next loop
                        int offset = (int)triggerTimeInLoop + loopRestartInBuffer;
                        sendMidiData(event.status, event.data1, event.data2,
                                     offset, event.beat);
                    }
                } else if (currentStartSample <= triggerTime && triggerTime < currentEndSample) {
                    // this event is supposed to trigger between currentStartSample and currentEndSample
                    int offset = (int)(triggerTime - currentStartSample);
                    sendMidiData(event.status, event.data1, event.data2,
                                 offset, event.beat);
                }
            }

            positionInSamples += frameCount;
        }
        framesCounted += frameCount;

        uiPosition = currentPositionInBeats();
    }
};

/// Creates the audio-thread-only state for the sequencer.
SequencerEngineRef akSequencerEngineCreate(void) {
    return new SequencerEngine { .impl = std::make_shared<SequencerEngineImpl>() };
}

void akSequencerEngineRelease(SequencerEngineRef engine) {
    engine->impl->stopAllPlayingNotes();
    delete engine;
}

/// Updates the sequence and returns a new render observer.
{'fraction_non_alphanumeric': 0.09476238047666619, 'fraction_numerical': 0.010560867703724847, 'mean_word_length': 2.074139065584558, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 61, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3947953', 'n_tokens_mistral': 3953, 'n_tokens_neox': 3658, 'n_words': 1009}