Refactoring of track management and implementation of a secure access proxy

This commit is contained in:
Sylvain Schneider
2026-07-24 01:17:39 +02:00
parent 888765ef6b
commit cbe165aa85
6 changed files with 346 additions and 417 deletions

View File

@@ -1,61 +0,0 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <mutex>
#include <span>
#include <vector>
//--------------------------------------------------------------
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<NoteEvent> activeNotes; // The notes that are currently active (long notes)
std::span<const NoteEvent> upcomingNotes; // The notes that are about to start
std::unique_lock<std::mutex> 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<uint8_t> 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
};
//--------------------------------------------------------------

View File

@@ -4,11 +4,24 @@
#include <choc/audio/choc_MIDIFile.h>
#include <fstream>
#include <iostream>
#include <map>
#include <ranges>
#include <span>
#include <syncstream>
#include <unordered_map>
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<uint8_t> midiBytes)
void Track::loadFromMemory(const std::span<uint8_t> 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
try
{
// Parse the MIDI data 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;
// Prepare structures to store notes and active notes
NotesBuffer notes;
std::unordered_map<uint16_t, size_t> activeNotes;
int trackID = 0;
for (const auto &track : midiFile.tracks)
// Lambda functions to handle enabling and disabling notes based on MIDI messages
auto makeKey = [](const choc::midi::ShortMessage &message) -> uint16_t
{
for (const auto &event : track.events)
return static_cast<uint16_t>((message.getChannel0to15() << 8) | message.getNoteNumber().note);
};
const auto disableNote = [makeKey, &notes, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
{
const auto &msg = event.message;
// 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, &notes, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
{
// Process tempo change events (Magic number 0x51 indicates a tempo change event)
if (msg.length() >= 6 && msg.getMetaEventType() == 0x51)
// 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)
{
const auto msPerQuarterNote = static_cast<uint32_t>(msg.data()[3] << 16) |
static_cast<uint32_t>(msg.data()[4] << 8) |
static_cast<uint32_t>(msg.data()[5]);
MidiTempoEvents tempoEvent{ .tick = event.tickPosition,
.microsecondsPerQuarterNote = static_cast<double>(msPerQuarterNote),
.timeMs = Timestamp(0) }; // Time in milliseconds will be calculated later
m_tempoEvents.push_back(tempoEvent);
}
disableNote(message, time);
return;
}
// 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);
}
// If the note is already active, disable it before enabling it again
if (activeNotes.contains(keyNote))
disableNote(message, time);
// 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);
}
}
++trackID;
}
// --- 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<double>(midiFile.timeFormat);
}
else if (midiFile.timeFormat < 0)
{
// Manage SMPTE time format (negative value) to calculate ticks per beat
const int framesPerSecond = -static_cast<int>(midiFile.timeFormat >> 8);
const int ticksPerFrame = static_cast<int>(midiFile.timeFormat & 0xFF);
ticksPerBeat = static_cast<double>(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)
{
constexpr MidiTempoEvents startTempoEvent{ .tick = 0,
.microsecondsPerQuarterNote = defaultTempoPerQuarterNote,
.timeMs = Timestamp(0) }; // Time in milliseconds will be calculated later
m_tempoEvents.insert(m_tempoEvents.begin(), startTempoEvent);
}
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<double>(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<int64_t>((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;
}
}
// --- 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
{
// 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<double>(tick - tempoEvent.tick);
// Convert tick delta to milliseconds
const auto timeDeltaMs = static_cast<int64_t>((tickDelta * tempoEvent.microsecondsPerQuarterNote) / (ticksPerBeat * 1000.0));
return tempoEvent.timeMs + Timestamp(timeDeltaMs);
// 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;
};
// --- 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<int, MidiNoteEvent> activeNotesMap;
map<std::pair<int, int>, 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 &noteEvent : m_noteEvents)
// 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)
{
if (noteEvent.noteOn)
// Update the last event time
lastTime = Seconds(timeInSeconds);
// Process short messages (Note On, Note Off, etc.)
if (message.isShortMessage())
{
activeNotesMap[{ noteEvent.pitch, noteEvent.channel }] = noteEvent; // Store the Note On event in the active notes map
if (message.isNoteOn())
{
// Enable note
enableNote(message, Seconds(timeInSeconds));
}
else if (message.isNoteOff())
{
// Disable note
disableNote(message, Seconds(timeInSeconds));
}
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())
// Handle other short messages if needed
}
} });
// After processing all events, ensure that any remaining active notes are properly closed
for (const auto &noteIndex : activeNotes | views::values)
notes[noteIndex].endTime = lastTime;
// Store the parsed notes in the track's data structure and log statistics
{
const auto &noteOnEvent = node.mapped();
scoped_lock lock(m_notes.mtx); // Lock the mutex to protect access to the data structures
m_notes.list = std::move(notes);
// 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);
ostringstream logMessage;
logMessage << "MIDI parsing completed\n"
<< " Total notes: " << m_notes.list.size() << "\n"
<< " Total duration: " << lastTime;
logInfo(logMessage.str());
}
}
}
// 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 &noteOnEvent : activeNotesMap | views::values)
catch (const std::exception &e)
{
// 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);
logError(std::string("Unexpected error while parsing MIDI data: ") + e.what());
}
// --- 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 &note : 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 &noteEvent : m_notesByStart)
{
cout << format("Type: {}, pitch={}, velocity={}, start={}, end={}\n",
static_cast<int>(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<size_t>(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<NoteEvent> 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);
}
//--------------------------------------------------------------

View File

@@ -1,27 +1,21 @@
#pragma once
#include "ITrack.h"
#include "trackDefs.h"
#include "trackLockProxy.h"
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <mutex>
#include <span>
#include <string>
#include <vector>
namespace track
{
//--------------------------------------------------------------
class Track : public ITrack
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<uint8_t> 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<uint8_t> 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<MidiNoteEvent> m_noteEvents; // Vector to store note events
std::vector<MidiTempoEvents> m_tempoEvents; // Vector to store tempo events
// Vectors to store note events sorted by start and end time for efficient retrieval
std::vector<NoteEvent> m_notesByStart; // Vector to store note events sorted by start time
std::vector<NoteEvent> 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

View File

@@ -0,0 +1,45 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <span>
#include <string>
#include <vector>
namespace track
{
//--------------------------------------------------------------
using Seconds = std::chrono::duration<double>;
struct Note; // Forward declaration of the Note structure
using NotesView = std::span<const Note>;
using NotesBuffer = std::vector<Note>;
//--------------------------------------------------------------
// 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

View File

@@ -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 &note : 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();
}
//--------------------------------------------------------------

View File

@@ -0,0 +1,45 @@
#pragma once
#include "trackDefs.h"
#include <mutex>
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<std::mutex> 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