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 <choc/audio/choc_MIDIFile.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <map>
|
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <span>
|
#include <syncstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
using namespace std;
|
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 */
|
/* Load a MIDI file from disk */
|
||||||
void Track::loadFromFile(const std::filesystem::path &filePath)
|
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 */
|
/* 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
|
try
|
||||||
|
{
|
||||||
// Clear any existing data before loading new MIDI data
|
// Parse the MIDI data from memory using choc::midi::File
|
||||||
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;
|
choc::midi::File midiFile;
|
||||||
midiFile.load(midiBytes.data(), midiBytes.size());
|
midiFile.load(midiBytes.data(), midiBytes.size());
|
||||||
|
|
||||||
// --- Step 1: Extract note events and tempo changes from the parsed MIDI data ---
|
// Prepare structures to store notes and active notes
|
||||||
// Default tempo in microseconds per quarter note (500,000 us = 120 BPM)
|
NotesBuffer notes;
|
||||||
constexpr double defaultTempoPerQuarterNote = 500000.0;
|
std::unordered_map<uint16_t, size_t> activeNotes;
|
||||||
|
|
||||||
int trackID = 0;
|
// Lambda functions to handle enabling and disabling notes based on MIDI messages
|
||||||
for (const auto &track : midiFile.tracks)
|
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, ¬es, &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 the note is not active, return early
|
||||||
if (msg.length() == 0) [[unlikely]]
|
const auto it = activeNotes.find(keyNote);
|
||||||
continue;
|
if (it == activeNotes.end())
|
||||||
|
return;
|
||||||
|
|
||||||
// Check for Meta events (e.g., tempo changes, time signature changes, etc.)
|
// Update the end timestamp of the note and remove it from the active notes map
|
||||||
if (msg.isMetaEvent())
|
notes[it->second].endTime = time;
|
||||||
|
activeNotes.erase(it);
|
||||||
|
};
|
||||||
|
const auto enableNote = [makeKey, disableNote, ¬es, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
|
||||||
{
|
{
|
||||||
// Process tempo change events (Magic number 0x51 indicates a tempo change event)
|
// Create a unique key for the note based on channel and pitch
|
||||||
if (msg.length() >= 6 && msg.getMetaEventType() == 0x51)
|
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) |
|
disableNote(message, time);
|
||||||
static_cast<uint32_t>(msg.data()[4] << 8) |
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Note On events
|
// If the note is already active, disable it before enabling it again
|
||||||
// Due to a bug on the choc::midi::Message class, we need to check the
|
if (activeNotes.contains(keyNote))
|
||||||
// length of the message before checking for Note On and Note Off events
|
disableNote(message, time);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for Note Off events
|
// Enable note
|
||||||
// Due to a bug on the choc::midi::Message class, we need to check the
|
const auto note = Note{
|
||||||
// length of the message before checking for Note On and Note Off events
|
.channel = message.getChannel0to15(),
|
||||||
else if (msg.length() >= 3 && msg.isNoteOff())
|
.pitch = message.getNoteNumber().note,
|
||||||
{
|
.velocity = message.getVelocity(),
|
||||||
MidiNoteEvent noteEvent{ .tick = event.tickPosition,
|
.name = string(message.getNoteNumber().getNameWithSharps()),
|
||||||
.pitch = msg.getNoteNumber(),
|
.octave = message.getNoteNumber().getOctaveNumber(),
|
||||||
.velocity = msg.getVelocity(),
|
.frequency = message.getNoteNumber().getFrequency(),
|
||||||
.track = trackID,
|
.startTime = time,
|
||||||
.channel = msg.getChannel0to15(),
|
.endTime = time
|
||||||
.noteOn = false };
|
};
|
||||||
m_noteEvents.push_back(noteEvent);
|
notes.push_back(note);
|
||||||
}
|
activeNotes[keyNote] = notes.size() - 1;
|
||||||
}
|
|
||||||
|
|
||||||
++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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Step 4: Create NoteEvent objects with start and end timestamps ---
|
// Iterate over all events in the MIDI file and print their details
|
||||||
// Sort the note events by tick to ensure they are in chronological order
|
Seconds lastTime;
|
||||||
std::ranges::sort(m_noteEvents, std::less<>{}, &MidiNoteEvent::tick);
|
midiFile.iterateEvents([this, enableNote, disableNote, &lastTime](const choc::midi::MessageView &message, const double timeInSeconds)
|
||||||
|
|
||||||
// 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)
|
// 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
|
else
|
||||||
{
|
{
|
||||||
// Note Off event: extract the corresponding Note On event from the active notes map
|
// Handle other short messages if needed
|
||||||
auto node = activeNotesMap.extract({ noteEvent.pitch, noteEvent.channel });
|
}
|
||||||
if (!node.empty())
|
} });
|
||||||
|
|
||||||
|
// After processing all events, ensure that any remaining active notes are properly closed
|
||||||
|
for (const auto ¬eIndex : activeNotes | views::values)
|
||||||
|
notes[noteIndex].endTime = lastTime;
|
||||||
|
|
||||||
|
// Store the parsed notes in the track's data structure and log statistics
|
||||||
{
|
{
|
||||||
const auto ¬eOnEvent = 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
|
ostringstream logMessage;
|
||||||
NoteEvent note;
|
logMessage << "MIDI parsing completed\n"
|
||||||
note.startTimestamp = tickToMs(noteOnEvent.tick);
|
<< " Total notes: " << m_notes.list.size() << "\n"
|
||||||
note.endTimestamp = tickToMs(noteEvent.tick);
|
<< " Total duration: " << lastTime;
|
||||||
note.trackType = TrackType::Unknown; // Can be determined based on channel or other criteria
|
logInfo(logMessage.str());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (const std::exception &e)
|
||||||
|
|
||||||
// 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)
|
logError(std::string("Unexpected error while parsing MIDI data: ") + e.what());
|
||||||
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 */
|
/* Clear the track data */
|
||||||
void Track::clear()
|
void Track::clear()
|
||||||
{
|
{
|
||||||
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
|
||||||
|
m_notes.list.clear();
|
||||||
m_noteEvents.clear();
|
|
||||||
m_tempoEvents.clear();
|
|
||||||
m_notesByStart.clear();
|
|
||||||
m_notesByEnd.clear();
|
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
/* Check if a MIDI file is loaded */
|
/* Check if a MIDI file is loaded */
|
||||||
bool Track::isLoaded() const
|
bool Track::isLoaded() 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
|
||||||
|
return !m_notes.list.empty();
|
||||||
return !m_noteEvents.empty();
|
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
/* Get the duration of the track in milliseconds */
|
/* Get the duration of the track */
|
||||||
ITrack::Timestamp Track::getDuration() const
|
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())
|
if (m_notes.list.empty())
|
||||||
return Timestamp(0);
|
return Seconds(0);
|
||||||
|
|
||||||
const auto &lastNote = m_notesByEnd.back();
|
const auto &lastNote = m_notes.list.back();
|
||||||
return lastNote.endTimestamp; // Return the end timestamp of the last note
|
return lastNote.endTime; // Return the end timestamp of the last note
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
/* Debug function to print the track data */
|
/* 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();
|
logInfo(std::format(
|
||||||
auto s = ms / 1000;
|
"Note: channel={:<2} | note={:<3} | startTime={:>7.3f}s | duration={:>7.3f}s | frequency={:>7.2f} Hz",
|
||||||
auto m = s / 60;
|
note.channel,
|
||||||
auto h = m / 60;
|
std::format("{}{}", note.name, note.octave),
|
||||||
ms = ms % 1000;
|
note.startTime.count(),
|
||||||
s = s % 60;
|
(note.endTime - note.startTime).count(),
|
||||||
m = m % 60;
|
note.frequency));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
cout << endl;
|
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
/* Get notes in a given time window */
|
/* Get a lock proxy to access the notes safely without copying the data */
|
||||||
ITrack::TrackWindow Track::getTrackWindow(const Timestamp startTime, const Timestamp endTime) const
|
TrackLockProxy Track::getNotes() const
|
||||||
{
|
{
|
||||||
std::unique_lock lock(m_mtx);
|
// The TrackLockProxy is automatically moved when returned (NRVO)
|
||||||
|
return TrackLockProxy(m_notes.mtx, m_notes.list);
|
||||||
// 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) };
|
|
||||||
}
|
}
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ITrack.h"
|
#include "trackDefs.h"
|
||||||
|
#include "trackLockProxy.h"
|
||||||
|
|
||||||
//--------------------------------------------------------------
|
#include <chrono>
|
||||||
class Track : public ITrack
|
#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:
|
public:
|
||||||
Track() = default; // Default constructor
|
Track() = default; // Default constructor
|
||||||
virtual ~Track() = default; // Default destructor
|
virtual ~Track() = default; // Default destructor
|
||||||
@@ -30,26 +24,27 @@ class Track : public ITrack
|
|||||||
Track &operator=(const Track &obj) = delete; // Copy assignment operator
|
Track &operator=(const Track &obj) = delete; // Copy assignment operator
|
||||||
Track &operator=(Track &&obj) noexcept = delete; // Move 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 ---
|
// --- File management ---
|
||||||
void loadFromFile(const std::filesystem::path &filePath) override; // Load a MIDI file from disk
|
void loadFromFile(const std::filesystem::path &filePath); // Load a MIDI file from disk
|
||||||
void loadFromMemory(std::span<uint8_t> midiBytes) override; // Load a MIDI file from memory
|
void loadFromMemory(std::span<uint8_t> midiBytes); // Load a MIDI file from memory
|
||||||
void clear() override; // Clear the track data
|
void clear(); // Clear the track data
|
||||||
[[nodiscard]] bool isLoaded() const override; // Check if a MIDI file is loaded
|
[[nodiscard]] bool isLoaded() const; // Check if a MIDI file is loaded
|
||||||
[[nodiscard]] Timestamp getDuration() const override; // Get the duration of the track in milliseconds
|
[[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 ---
|
// --- Notes access ---
|
||||||
[[nodiscard]] TrackWindow getTrackWindow(Timestamp startTime, Timestamp endTime) const override; // Get notes in a given time window
|
TrackLockProxy getNotes() const; // Get a lock proxy to access the notes safely without copying the data
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
mutable std::mutex m_mtx; // Mutex to protect access to the data structures
|
struct
|
||||||
|
{
|
||||||
std::vector<MidiNoteEvent> m_noteEvents; // Vector to store note events
|
mutable std::mutex mtx; // Protects access to the notes vector
|
||||||
std::vector<MidiTempoEvents> m_tempoEvents; // Vector to store tempo events
|
NotesBuffer list; // Vector to store notes
|
||||||
|
} m_notes;
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
//--------------------------------------------------------------
|
//--------------------------------------------------------------
|
||||||
|
} // 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