diff --git a/src/core/Track/ITrack.h b/src/core/Track/ITrack.h deleted file mode 100644 index ec9ac20..0000000 --- a/src/core/Track/ITrack.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -//-------------------------------------------------------------- -class ITrack -{ - public: - using Timestamp = std::chrono::milliseconds; - - enum class TrackType : uint8_t - { - Unknown = 0, - LeftHand = 1, - RightHand = 2 - }; - - struct NoteEvent - { - Timestamp startTimestamp; // The timestamp when the note event starts - Timestamp endTimestamp; // The timestamp when the note event ends - TrackType trackType; // The type of the track (LeftHand, RightHand, ...) - int pitch; // The MIDI note number (0-127) - int velocity; // The velocity of the note (0-127) - bool noteOn; // True if the event is a Note On event, false otherwise (Note Off) - }; - - struct TrackWindow - { - std::vector activeNotes; // The notes that are currently active (long notes) - std::span upcomingNotes; // The notes that are about to start - - std::unique_lock lock; // Lock to protect access to the data structures - }; - - public: - ITrack() = default; // Default constructor - virtual ~ITrack() = default; // Default destructor - ITrack(const ITrack &obj) = delete; // Copy constructor - ITrack(ITrack &&obj) noexcept = delete; // Move constructor - ITrack &operator=(const ITrack &obj) = delete; // Copy assignment operator - ITrack &operator=(ITrack &&obj) noexcept = delete; // Move assignment operator - - // --- File management --- - virtual void loadFromFile(const std::filesystem::path &filePath) = 0; // Load a MIDI file from disk - virtual void loadFromMemory(std::span midiBytes) = 0; // Load a MIDI file from memory - virtual void clear() = 0; // Clear the track data - [[nodiscard]] virtual bool isLoaded() const = 0; // Check if a MIDI file is loaded - [[nodiscard]] virtual Timestamp getDuration() const = 0; // Get the duration of the track in milliseconds - - virtual void debug() = 0; // Debug function to print the track data - - // --- Rendering --- - [[nodiscard]] virtual TrackWindow getTrackWindow(Timestamp startTime, Timestamp endTime) const = 0; // Get notes in a given time window -}; -//-------------------------------------------------------------- diff --git a/src/core/Track/Track.cpp b/src/core/Track/Track.cpp index d3034e6..6c342b2 100644 --- a/src/core/Track/Track.cpp +++ b/src/core/Track/Track.cpp @@ -4,11 +4,24 @@ #include #include #include -#include #include -#include +#include +#include using namespace std; +using namespace track; +//-------------------------------------------------------------- +/* Log an informational message */ +void Track::logInfo(const std::string &message) const +{ + osyncstream(cout) << message << std::endl; +} +//-------------------------------------------------------------- +/* Log an error message */ +void Track::logError(const std::string &message) const +{ + osyncstream(cerr) << message << std::endl; +} //-------------------------------------------------------------- /* Load a MIDI file from disk */ void Track::loadFromFile(const std::filesystem::path &filePath) @@ -33,366 +46,166 @@ void Track::loadFromFile(const std::filesystem::path &filePath) } //-------------------------------------------------------------- /* Load a MIDI file from memory */ -void Track::loadFromMemory(std::span midiBytes) +void Track::loadFromMemory(const std::span midiBytes) { - cout << "Loading MIDI data from memory, size: " << midiBytes.size() << " bytes" << endl; + logInfo("Loading MIDI data from memory, size: " + std::to_string(midiBytes.size()) + " bytes"); - scoped_lock lock(m_mtx); // Lock the mutex to protect access to the data structures - - // Clear any existing data before loading new MIDI data - m_noteEvents.clear(); - m_tempoEvents.clear(); - m_notesByStart.clear(); - m_notesByEnd.clear(); - - // Load midi sequence from memory using choc::midi::File - choc::midi::File midiFile; - midiFile.load(midiBytes.data(), midiBytes.size()); - - // --- Step 1: Extract note events and tempo changes from the parsed MIDI data --- - // Default tempo in microseconds per quarter note (500,000 us = 120 BPM) - constexpr double defaultTempoPerQuarterNote = 500000.0; - - int trackID = 0; - for (const auto &track : midiFile.tracks) + try { - for (const auto &event : track.events) + // Parse the MIDI data from memory using choc::midi::File + choc::midi::File midiFile; + midiFile.load(midiBytes.data(), midiBytes.size()); + + // Prepare structures to store notes and active notes + NotesBuffer notes; + std::unordered_map activeNotes; + + // Lambda functions to handle enabling and disabling notes based on MIDI messages + auto makeKey = [](const choc::midi::ShortMessage &message) -> uint16_t { - const auto &msg = event.message; + return static_cast((message.getChannel0to15() << 8) | message.getNoteNumber().note); + }; + const auto disableNote = [makeKey, ¬es, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time) + { + // Create a unique key for the note based on channel and pitch + const uint16_t keyNote = makeKey(message); - // Security check to ensure the message has enough bytes for processing - if (msg.length() == 0) [[unlikely]] - continue; + // If the note is not active, return early + const auto it = activeNotes.find(keyNote); + if (it == activeNotes.end()) + return; - // Check for Meta events (e.g., tempo changes, time signature changes, etc.) - if (msg.isMetaEvent()) + // Update the end timestamp of the note and remove it from the active notes map + notes[it->second].endTime = time; + activeNotes.erase(it); + }; + const auto enableNote = [makeKey, disableNote, ¬es, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time) + { + // Create a unique key for the note based on channel and pitch + const uint16_t keyNote = makeKey(message); + + // If the velocity is zero, treat it as a Note Off event and disable the note + if (message.getVelocity() == 0) { - // Process tempo change events (Magic number 0x51 indicates a tempo change event) - if (msg.length() >= 6 && msg.getMetaEventType() == 0x51) + disableNote(message, time); + return; + } + + // If the note is already active, disable it before enabling it again + if (activeNotes.contains(keyNote)) + disableNote(message, time); + + // Enable note + const auto note = Note{ + .channel = message.getChannel0to15(), + .pitch = message.getNoteNumber().note, + .velocity = message.getVelocity(), + .name = string(message.getNoteNumber().getNameWithSharps()), + .octave = message.getNoteNumber().getOctaveNumber(), + .frequency = message.getNoteNumber().getFrequency(), + .startTime = time, + .endTime = time + }; + notes.push_back(note); + activeNotes[keyNote] = notes.size() - 1; + }; + + // Iterate over all events in the MIDI file and print their details + Seconds lastTime; + midiFile.iterateEvents([this, enableNote, disableNote, &lastTime](const choc::midi::MessageView &message, const double timeInSeconds) + { + // Update the last event time + lastTime = Seconds(timeInSeconds); + + // Process short messages (Note On, Note Off, etc.) + if (message.isShortMessage()) + { + if (message.isNoteOn()) { - const auto msPerQuarterNote = static_cast(msg.data()[3] << 16) | - static_cast(msg.data()[4] << 8) | - static_cast(msg.data()[5]); - MidiTempoEvents tempoEvent{ .tick = event.tickPosition, - .microsecondsPerQuarterNote = static_cast(msPerQuarterNote), - .timeMs = Timestamp(0) }; // Time in milliseconds will be calculated later - m_tempoEvents.push_back(tempoEvent); + // Enable note + enableNote(message, Seconds(timeInSeconds)); } - } - // Check for Note On events - // Due to a bug on the choc::midi::Message class, we need to check the - // length of the message before checking for Note On and Note Off events - else if (msg.length() >= 3 && msg.isNoteOn()) - { - MidiNoteEvent noteEvent{ .tick = event.tickPosition, - .pitch = msg.getNoteNumber(), - .velocity = msg.getVelocity(), - .track = trackID, - .channel = msg.getChannel0to15(), - .noteOn = true }; - m_noteEvents.push_back(noteEvent); - } + else if (message.isNoteOff()) + { + // Disable note + disableNote(message, Seconds(timeInSeconds)); + } - // Check for Note Off events - // Due to a bug on the choc::midi::Message class, we need to check the - // length of the message before checking for Note On and Note Off events - else if (msg.length() >= 3 && msg.isNoteOff()) - { - MidiNoteEvent noteEvent{ .tick = event.tickPosition, - .pitch = msg.getNoteNumber(), - .velocity = msg.getVelocity(), - .track = trackID, - .channel = msg.getChannel0to15(), - .noteOn = false }; - m_noteEvents.push_back(noteEvent); - } - } + else + { + // Handle other short messages if needed + } + } }); - ++trackID; - } + // After processing all events, ensure that any remaining active notes are properly closed + for (const auto ¬eIndex : activeNotes | views::values) + notes[noteIndex].endTime = lastTime; - // --- Step 2: Convert ticks to timestamps in milliseconds --- - // Extract the time format from the MIDI file to determine ticks per beat - double ticksPerBeat = 480.0; - if (midiFile.timeFormat > 0) - { - ticksPerBeat = static_cast(midiFile.timeFormat); - } - else if (midiFile.timeFormat < 0) - { - // Manage SMPTE time format (negative value) to calculate ticks per beat - const int framesPerSecond = -static_cast(midiFile.timeFormat >> 8); - const int ticksPerFrame = static_cast(midiFile.timeFormat & 0xFF); - ticksPerBeat = static_cast(framesPerSecond * ticksPerFrame); - } - // If ticksPerBeat is zero or negative, set it to a default value of 480.0 - if (ticksPerBeat <= 0.0) [[unlikely]] - { - ticksPerBeat = 480.0; - } - - // Calculate the time in milliseconds for each tempo change event - if (m_tempoEvents.empty()) - { - // If no tempo events were found, use the default tempo for the entire track - m_tempoEvents.push_back({ .tick = 0, - .microsecondsPerQuarterNote = defaultTempoPerQuarterNote, - .timeMs = Timestamp(0) }); - } - else - { - // Sort the tempo events by tick to ensure they are in chronological order - std::ranges::sort(m_tempoEvents, std::less<>{}, &MidiTempoEvents::tick); - - // Security check: if the first tempo event is not at tick 0, insert a default tempo event at tick 0 - if (m_tempoEvents.front().tick > 0) + // Store the parsed notes in the track's data structure and log statistics { - constexpr MidiTempoEvents startTempoEvent{ .tick = 0, - .microsecondsPerQuarterNote = defaultTempoPerQuarterNote, - .timeMs = Timestamp(0) }; // Time in milliseconds will be calculated later - m_tempoEvents.insert(m_tempoEvents.begin(), startTempoEvent); - } + scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures + m_notes.list = std::move(notes); - auto currentTimestamp = Timestamp(0); - int64_t currentTick = 0; - auto currentTempo = m_tempoEvents[0].microsecondsPerQuarterNote; - m_tempoEvents[0].timeMs = Timestamp(0); // The first tempo event starts at time 0 - for (size_t i = 1; i < m_tempoEvents.size(); i++) - { - const double tickDelta = static_cast(m_tempoEvents[i].tick - currentTick); - - // Calculate the time delta in milliseconds based on the current tempo - // Time in milliseconds = (tickDelta * microsecondsPerQuarterNote) / (ticksPerBeat * 1000) - const auto timeDeltaMs = static_cast((tickDelta * currentTempo) / (ticksPerBeat * 1000.0)); - - // Update the current timestamp and store it in the tempo event - currentTimestamp += Timestamp(timeDeltaMs); - currentTick = m_tempoEvents[i].tick; - currentTempo = m_tempoEvents[i].microsecondsPerQuarterNote; - - // Store the calculated time in milliseconds for the tempo event - m_tempoEvents[i].timeMs = currentTimestamp; + ostringstream logMessage; + logMessage << "MIDI parsing completed\n" + << " Total notes: " << m_notes.list.size() << "\n" + << " Total duration: " << lastTime; + logInfo(logMessage.str()); } } - - // --- Step 3: Convert note events from ticks to timestamps in milliseconds --- - // Helper function to convert a tick value to a timestamp in milliseconds based on the tempo events - const auto tickToMs = [this, ticksPerBeat](const int64_t tick) -> Timestamp + catch (const std::exception &e) { - // Find the last tempo event that occurs before or at the given tick - size_t tempoIndex = 0; - for (size_t i = 0; i < m_tempoEvents.size(); i++) - { - if (m_tempoEvents[i].tick <= tick) - tempoIndex = i; - else - break; - } - - const auto &tempoEvent = m_tempoEvents[tempoIndex]; - const double tickDelta = static_cast(tick - tempoEvent.tick); - - // Convert tick delta to milliseconds - const auto timeDeltaMs = static_cast((tickDelta * tempoEvent.microsecondsPerQuarterNote) / (ticksPerBeat * 1000.0)); - - return tempoEvent.timeMs + Timestamp(timeDeltaMs); - }; - - // --- Step 4: Create NoteEvent objects with start and end timestamps --- - // Sort the note events by tick to ensure they are in chronological order - std::ranges::sort(m_noteEvents, std::less<>{}, &MidiNoteEvent::tick); - - // Create a map to keep track of active notes (notes that have been started but not yet ended) - // unordered_map activeNotesMap; - map, MidiNoteEvent> activeNotesMap; - - // Estimate the number of notes to reserve space in the vectors for performance - // Half of the note events are expected to be Note On events, so we reserve half the size - m_notesByStart.reserve(m_noteEvents.size() / 2); - - for (auto ¬eEvent : m_noteEvents) - { - if (noteEvent.noteOn) - { - activeNotesMap[{ noteEvent.pitch, noteEvent.channel }] = noteEvent; // Store the Note On event in the active notes map - } - else - { - // Note Off event: extract the corresponding Note On event from the active notes map - auto node = activeNotesMap.extract({ noteEvent.pitch, noteEvent.channel }); - if (!node.empty()) - { - const auto ¬eOnEvent = node.mapped(); - - // Create a NoteEvent with start and end timestamps - NoteEvent note; - note.startTimestamp = tickToMs(noteOnEvent.tick); - note.endTimestamp = tickToMs(noteEvent.tick); - note.trackType = TrackType::Unknown; // Can be determined based on channel or other criteria - note.pitch = noteOnEvent.pitch; - note.velocity = noteOnEvent.velocity; - note.noteOn = true; - - // Manage channel-specific logic to determine track type (LeftHand or RightHand) based on the track number - if (noteOnEvent.track == 0) - note.trackType = TrackType::RightHand; - else if (noteOnEvent.track == 1) - note.trackType = TrackType::LeftHand; - - // Add the NoteEvent to the vector and remove it from the active notes map - m_notesByStart.push_back(note); - } - } + logError(std::string("Unexpected error while parsing MIDI data: ") + e.what()); } - - // Handle any remaining active notes (notes without a corresponding note off) - // These will be extended to a reasonable default duration (e.g., 100ms) - for (const auto ¬eOnEvent : activeNotesMap | views::values) - { - // Create a NoteEvent with a default end time (e.g., 100ms after the start time) - NoteEvent note; - note.startTimestamp = tickToMs(noteOnEvent.tick); - note.endTimestamp = tickToMs(noteOnEvent.tick) + 100ms; - note.trackType = TrackType::Unknown; - note.pitch = noteOnEvent.pitch; - note.velocity = noteOnEvent.velocity; - note.noteOn = true; - - // Add the NoteEvent to the vector - m_notesByStart.push_back(note); - } - - // --- Step 5: Sort notes for efficient retrieval --- - // Sort by start time - ranges::sort(m_notesByStart, std::less<>{}, &NoteEvent::startTimestamp); - - // Create a sorted-by-end-time vector - m_notesByEnd = m_notesByStart; - ranges::sort(m_notesByEnd, std::less<>{}, &NoteEvent::endTimestamp); - - cout << "Loaded " << m_notesByStart.size() << " notes and " << m_tempoEvents.size() << " tempo changes." << endl; } //-------------------------------------------------------------- /* Clear the track data */ void Track::clear() { - scoped_lock lock(m_mtx); // Lock the mutex to protect access to the data structures - - m_noteEvents.clear(); - m_tempoEvents.clear(); - m_notesByStart.clear(); - m_notesByEnd.clear(); + scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures + m_notes.list.clear(); } //-------------------------------------------------------------- /* Check if a MIDI file is loaded */ bool Track::isLoaded() const { - scoped_lock lock(m_mtx); // Lock the mutex to protect access to the data structures - - return !m_noteEvents.empty(); + scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures + return !m_notes.list.empty(); } //-------------------------------------------------------------- -/* Get the duration of the track in milliseconds */ -ITrack::Timestamp Track::getDuration() const +/* Get the duration of the track */ +Seconds Track::getDuration() const { - scoped_lock lock(m_mtx); // Lock the mutex to protect access to the data structures + scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures - if (m_notesByEnd.empty()) - return Timestamp(0); + if (m_notes.list.empty()) + return Seconds(0); - const auto &lastNote = m_notesByEnd.back(); - return lastNote.endTimestamp; // Return the end timestamp of the last note + const auto &lastNote = m_notes.list.back(); + return lastNote.endTime; // Return the end timestamp of the last note } //-------------------------------------------------------------- /* Debug function to print the track data */ -void Track::debug() +void Track::debug() const { - const auto timeToStr = [](const Timestamp &t) -> std::string + scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures + for (const auto ¬e : m_notes.list) { - auto ms = t.count(); - auto s = ms / 1000; - auto m = s / 60; - auto h = m / 60; - ms = ms % 1000; - s = s % 60; - m = m % 60; - return std::format("{:02}:{:02}:{:02}.{:03}", h, m, s, ms); - }; - - for (const auto ¬eEvent : m_notesByStart) - { - cout << format("Type: {}, pitch={}, velocity={}, start={}, end={}\n", - static_cast(noteEvent.trackType), - noteEvent.pitch, - noteEvent.velocity, - timeToStr(noteEvent.startTimestamp), - timeToStr(noteEvent.endTimestamp)); + logInfo(std::format( + "Note: channel={:<2} | note={:<3} | startTime={:>7.3f}s | duration={:>7.3f}s | frequency={:>7.2f} Hz", + note.channel, + std::format("{}{}", note.name, note.octave), + note.startTime.count(), + (note.endTime - note.startTime).count(), + note.frequency)); } - cout << endl; } //-------------------------------------------------------------- -/* Get notes in a given time window */ -ITrack::TrackWindow Track::getTrackWindow(const Timestamp startTime, const Timestamp endTime) const +/* Get a lock proxy to access the notes safely without copying the data */ +TrackLockProxy Track::getNotes() const { - std::unique_lock lock(m_mtx); - - // Check if notes are loaded - if (m_noteEvents.empty()) - return { .activeNotes = {}, - .upcomingNotes = {}, - .lock = {} }; - - // --- Preparation of the upcoming notes --- - // Find the first note that starts after or at the startTime - const auto upStart = std::ranges::lower_bound(m_notesByStart, - startTime, - std::less<>{}, - &NoteEvent::startTimestamp); - - // Find the first note that starts after or at the endTime - const auto upEnd = std::ranges::lower_bound(upStart, - m_notesByStart.end(), - endTime, - std::less<>{}, - &NoteEvent::startTimestamp); - - // Create a span for the upcoming notes - const auto count = static_cast(std::distance(upStart, upEnd)); - if (count == 0) - return { .activeNotes = {}, - .upcomingNotes = {}, - .lock = {} }; - const std::span upcomingNotes(m_notesByStart.data() + std::distance(m_notesByStart.begin(), upStart), count); - - // --- Preparation of the active notes --- - // Find the first note that ends after or at the startTime - const auto activeStart = std::ranges::lower_bound(m_notesByEnd, - startTime, - std::less<>{}, - &NoteEvent::endTimestamp); - - // Filter the active notes to include only those that have started before or at the endTime - std::vector activeNotes; - activeNotes.reserve(32); // Reserve space for 32 notes, which is often sufficient for a frame - - for (auto it = activeStart; it != m_notesByEnd.end(); ++it) - { - // Check if the note is active at the startTime - // (i.e., it has started before or at startTime and has not - // ended yet) - if (it->startTimestamp <= startTime && it->endTimestamp > startTime) - activeNotes.push_back(*it); - - // Stop if the note starts after the endTime, as we only - // want active notes in the window - if (it->startTimestamp > endTime) - break; - } - - // --- Create and return the track window with the active and upcoming notes --- - return { .activeNotes = std::move(activeNotes), - .upcomingNotes = upcomingNotes, - .lock = std::move(lock) }; + // The TrackLockProxy is automatically moved when returned (NRVO) + return TrackLockProxy(m_notes.mtx, m_notes.list); } //-------------------------------------------------------------- diff --git a/src/core/Track/Track.h b/src/core/Track/Track.h index 1f6ff4b..b41b4c5 100644 --- a/src/core/Track/Track.h +++ b/src/core/Track/Track.h @@ -1,27 +1,21 @@ #pragma once -#include "ITrack.h" +#include "trackDefs.h" +#include "trackLockProxy.h" -//-------------------------------------------------------------- -class Track : public ITrack +#include +#include +#include +#include +#include +#include +#include + +namespace track +{ +//-------------------------------------------------------------- +class Track { - public: - struct MidiNoteEvent - { - int64_t tick; // The tick at which the event occurs - int pitch; // The MIDI note number (0-127) - int velocity; // The velocity of the note (0-127) - int track; // The track number (0-based index) - int channel; // The MIDI channel (1-16, or 0 for no channel) - bool noteOn; // True if the event is a Note On event, false otherwise (Note Off) - }; - struct MidiTempoEvents - { - int64_t tick; // The tick at which the tempo change occurs - double microsecondsPerQuarterNote; // The new tempo in microseconds per quarter note - Timestamp timeMs; // The time in milliseconds at which the tempo change occurs - }; - public: Track() = default; // Default constructor virtual ~Track() = default; // Default destructor @@ -30,26 +24,27 @@ class Track : public ITrack Track &operator=(const Track &obj) = delete; // Copy assignment operator Track &operator=(Track &&obj) noexcept = delete; // Move assignment operator + void logInfo(const std::string &message) const; // Log an informational message + void logError(const std::string &message) const; // Log an error message + // --- File management --- - void loadFromFile(const std::filesystem::path &filePath) override; // Load a MIDI file from disk - void loadFromMemory(std::span midiBytes) override; // Load a MIDI file from memory - void clear() override; // Clear the track data - [[nodiscard]] bool isLoaded() const override; // Check if a MIDI file is loaded - [[nodiscard]] Timestamp getDuration() const override; // Get the duration of the track in milliseconds + void loadFromFile(const std::filesystem::path &filePath); // Load a MIDI file from disk + void loadFromMemory(std::span midiBytes); // Load a MIDI file from memory + void clear(); // Clear the track data + [[nodiscard]] bool isLoaded() const; // Check if a MIDI file is loaded + [[nodiscard]] Seconds getDuration() const; // Get the duration of the track - void debug() override; // Debug function to print the track data + void debug() const; // Debug function to print the track data - // --- Rendering --- - [[nodiscard]] TrackWindow getTrackWindow(Timestamp startTime, Timestamp endTime) const override; // Get notes in a given time window + // --- Notes access --- + TrackLockProxy getNotes() const; // Get a lock proxy to access the notes safely without copying the data protected: - mutable std::mutex m_mtx; // Mutex to protect access to the data structures - - std::vector m_noteEvents; // Vector to store note events - std::vector m_tempoEvents; // Vector to store tempo events - - // Vectors to store note events sorted by start and end time for efficient retrieval - std::vector m_notesByStart; // Vector to store note events sorted by start time - std::vector m_notesByEnd; // Vector to store note events sorted by end time + struct + { + mutable std::mutex mtx; // Protects access to the notes vector + NotesBuffer list; // Vector to store notes + } m_notes; }; //-------------------------------------------------------------- +} // namespace track diff --git a/src/core/track/trackDefs.h b/src/core/track/trackDefs.h new file mode 100644 index 0000000..0e6c7ff --- /dev/null +++ b/src/core/track/trackDefs.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace track +{ +//-------------------------------------------------------------- +using Seconds = std::chrono::duration; + +struct Note; // Forward declaration of the Note structure +using NotesView = std::span; +using NotesBuffer = std::vector; +//-------------------------------------------------------------- +// The Note structure represents a single MIDI note event with +// its properties. +struct Note +{ + uint8_t channel; // MIDI channel (0-15) + uint8_t pitch; // Midi note number (0-127) + uint8_t velocity; // Velocity of the note event (0-127) + + std::string name; // Name of the note without octave (e.g., "C", "D#") + int octave; // Octave number of the note (e.g., 4 for C4) + float frequency; // Frequency of the note in Hz (e.g., 440.0 for A4) + + Seconds startTime; // Start time of the note event (time when the note starts) + Seconds endTime; // End time of the note event (time when the note ends) +}; +//-------------------------------------------------------------- +// The ActiveNotes structure holds two buffers of notes: +// - playing notes: startTime <= t < endTime +// - upcoming notes: t + window <= startTime < t + window + upcomingWindow +// This structure is used to efficiently manage and access the notes +// that are relevant for playback at a given time. +struct ActiveNotes +{ + NotesBuffer playing; // Currently playing notes (t <= startTime < t + window) + NotesBuffer upcoming; // Upcoming notes (t + window <= startTime < t + window + upcomingWindow) +}; +//-------------------------------------------------------------- +} // namespace track diff --git a/src/core/track/trackLockProxy.cpp b/src/core/track/trackLockProxy.cpp new file mode 100644 index 0000000..19f2376 --- /dev/null +++ b/src/core/track/trackLockProxy.cpp @@ -0,0 +1,92 @@ +#include "TrackLockProxy.h" + +using namespace std; +using namespace track; +//-------------------------------------------------------------- +/* Constructor */ +TrackLockProxy::TrackLockProxy(std::mutex &mtx, const NotesView notes) + : m_lock(mtx) + , m_notes(notes) +{ +} +//-------------------------------------------------------------- +/* Access a note by index */ +const Note &TrackLockProxy::operator[](const size_t index) const +{ + return m_notes[index]; +} +//-------------------------------------------------------------- +/* Get the active notes at a specific time with a lookahead window */ +ActiveNotes TrackLockProxy::getActiveNotesAt(const Seconds currentTime, const Seconds lookaheadWindow) const +{ + ActiveNotes result; + getActiveNotesAt(currentTime, lookaheadWindow, result); + return result; +} +//-------------------------------------------------------------- +/* Get the active notes at a specific time with a lookahead window */ +void TrackLockProxy::getActiveNotesAt(const Seconds currentTime, const Seconds lookaheadWindow, ActiveNotes &outNotes) const +{ + // Clear the output buffers before filling them with active notes + // without releasing the allocated memory + outNotes.playing.clear(); // Clear the currently playing notes buffer + outNotes.upcoming.clear(); // Clear the upcoming notes buffer + + const Seconds lookaheadEnd = currentTime + lookaheadWindow; + + for (const auto ¬e : m_notes) + { + // Extract the notes that are currently playing + if (note.startTime <= currentTime && note.endTime > currentTime) + outNotes.playing.push_back(note); + + // Extract the notes that are upcoming in the lookahead window + if (note.startTime <= lookaheadEnd && note.endTime >= currentTime) + outNotes.upcoming.push_back(note); + + // Early exit: Notes are sorted by startTime, so if we reach a note + // that starts after the lookahead window, we can stop searching + if (note.startTime > lookaheadEnd) + break; + } +} +//-------------------------------------------------------------- +/* Get the duration of the track */ +Seconds TrackLockProxy::getDuration() const +{ + if (empty()) + return Seconds(0); + + return m_notes.back().endTime; +} +//-------------------------------------------------------------- +/* Get the span of notes */ +NotesView TrackLockProxy::get() const noexcept +{ + return m_notes; +} +//-------------------------------------------------------------- +/* Get the first iterator of the notes */ +auto TrackLockProxy::begin() const noexcept +{ + return m_notes.begin(); +} +//-------------------------------------------------------------- +/* Get the end iterator of the notes */ +auto TrackLockProxy::end() const noexcept +{ + return m_notes.end(); +} +//-------------------------------------------------------------- +/* Check if the notes span is empty */ +bool TrackLockProxy::empty() const noexcept +{ + return m_notes.empty(); +} +//-------------------------------------------------------------- +/* Get the size of the notes span */ +size_t TrackLockProxy::size() const noexcept +{ + return m_notes.size(); +} +//-------------------------------------------------------------- diff --git a/src/core/track/trackLockProxy.h b/src/core/track/trackLockProxy.h new file mode 100644 index 0000000..eb4e429 --- /dev/null +++ b/src/core/track/trackLockProxy.h @@ -0,0 +1,45 @@ +#pragma once + +#include "trackDefs.h" + +#include + +namespace track +{ +//-------------------------------------------------------------- +class TrackLockProxy final +{ + public: + TrackLockProxy() = delete; // Default constructor + ~TrackLockProxy() = default; // Default destructor + TrackLockProxy(const TrackLockProxy &obj) = delete; // Copy constructor + TrackLockProxy(TrackLockProxy &&obj) noexcept = default; // Move constructor + TrackLockProxy &operator=(const TrackLockProxy &obj) = delete; // Copy assignment operator + TrackLockProxy &operator=(TrackLockProxy &&obj) noexcept = default; // Move assignment operator + + explicit TrackLockProxy(std::mutex &mtx, NotesView notes); // Constructor + + const Note &operator[](size_t index) const; // Access a note by index + + [[nodiscard]] ActiveNotes getActiveNotesAt(Seconds currentTime, // Get the active notes at a specific time with a lookahead window + Seconds lookaheadWindow) const; + void getActiveNotesAt(Seconds currentTime, // Get the active notes at a specific time with a lookahead window + Seconds lookaheadWindow, + ActiveNotes &outNotes) const; + + [[nodiscard]] Seconds getDuration() const; // Get the duration of the track + + // Utility functions to access the notes into for-range loops or other algorithms + [[nodiscard]] NotesView get() const noexcept; // Get the span of notes + [[nodiscard]] auto begin() const noexcept; // Get the first iterator of the notes + [[nodiscard]] auto end() const noexcept; // Get the end iterator of the notes + [[nodiscard]] bool empty() const noexcept; // Check if the notes span is empty + [[nodiscard]] size_t size() const noexcept; // Get the size of the notes span + + protected: + std::unique_lock m_lock; // Keeps the mutex locked for the lifetime of the proxy + + NotesView m_notes; // Span of notes to access without copying the data +}; +//-------------------------------------------------------------- +} // namespace track