Refactoring of track management and implementation of a secure access proxy
This commit is contained in:
@@ -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
|
||||
};
|
||||
//--------------------------------------------------------------
|
||||
@@ -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
|
||||
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<uint16_t, size_t> 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<uint16_t>((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<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);
|
||||
// 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<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)
|
||||
// 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<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;
|
||||
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<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);
|
||||
};
|
||||
|
||||
// --- 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 ¬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<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);
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "ITrack.h"
|
||||
#include "trackDefs.h"
|
||||
#include "trackLockProxy.h"
|
||||
|
||||
//--------------------------------------------------------------
|
||||
class Track : public ITrack
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<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
|
||||
|
||||
45
src/core/track/trackDefs.h
Normal file
45
src/core/track/trackDefs.h
Normal 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
|
||||
92
src/core/track/trackLockProxy.cpp
Normal file
92
src/core/track/trackLockProxy.cpp
Normal 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 ¬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();
|
||||
}
|
||||
//--------------------------------------------------------------
|
||||
45
src/core/track/trackLockProxy.h
Normal file
45
src/core/track/trackLockProxy.h
Normal 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
|
||||
Reference in New Issue
Block a user