Skip to content

Instantly share code, notes, and snippets.

@andigamesandmusic
Last active April 26, 2022 00:30
Show Gist options
  • Save andigamesandmusic/94be52eff000013d7cdf5daef54bc4aa to your computer and use it in GitHub Desktop.
Save andigamesandmusic/94be52eff000013d7cdf5daef54bc4aa to your computer and use it in GitHub Desktop.
Export Cubase or Nuendo sample-precise markers from .cpr/.npr project files to JSON
var fs = require('fs');
var path = require('path');
var debugEnabled = false;
function main(params)
{
var output = {
'Projects': [],
};
for (var i = 0; i < params.length; i++)
{
var param = params[i];
if (param.startsWith('-'))
{
if (param === '-h' || param === '--help')
{
console.error('Usage: CubaseMarkers <Cubase .cpr project file> ...')
return;
}
if (param === '-d' || param === '--debug')
{
debugEnabled = true;
debug('DEBUG enabled');
}
continue;
}
debug('Reading file', param);
output.Projects.push(readMarkersFromFile(param));
}
console.log(JSON.stringify(output, null, 2));
}
function debug(...args)
{
if (debugEnabled)
console.error(...args);
}
function readMarkersFromFile(file)
{
var buffer = fs.readFileSync(file);
var sampleRate = readSampleRate(buffer);
var nuendo = isNuendo(buffer);
var markerTracks = readMarkerTracks(buffer, nuendo, sampleRate);
return {
'Project': path.parse(file).name,
'IsNuendo': nuendo,
'SampleRate': sampleRate,
'MarkerTracks': markerTracks,
};
}
function isNuendo(buffer)
{
return buffer.indexOf(Buffer.from('000000084761634461746100', 'hex')) > 0;
}
function readSampleRate(buffer)
{
var audioSampleRateStart = buffer.indexOf('AudioSampleRate');
if (audioSampleRateStart < 0)
throw (new Error('Could not locate sample rate'));
var floatStart = buffer.indexOf('Float', audioSampleRateStart);
if (floatStart < 0)
throw (new Error('Could not locate sample rate value start'));
var sampleRate = buffer.readDoubleBE(floatStart + 8);
if (!(sampleRate > 10000 && sampleRate < 200000))
throw (new Error('Sample rate was invalid'));
return sampleRate;
}
function readMarkerTracks(buffer, nuendo, sampleRate)
{
var markerTracks = [];
var markerTrackEventStart = buffer.indexOf('MMarkerTrackEvent');
if (markerTrackEventStart < 0)
throw (new Error('Could not locate marker tracks'));
var nextMarkerTrack = buffer.indexOf(Buffer.from('800000BF', 'hex'), markerTrackEventStart);
if (nextMarkerTrack < markerTrackEventStart)
throw (new Error('Could not locate marker track start'));
nextMarkerTrack += 8;
var trackIndex = 0;
while (nextMarkerTrack >= 0)
{
nextMarkerTrack = readMarkerTrack(buffer, nuendo, sampleRate, trackIndex, nextMarkerTrack, markerTracks);
trackIndex += 1;
}
return markerTracks;
}
function minSentinel(buffer, offset, hexList)
{
sentinel = Infinity;
for (var i = 0; i < hexList.length; i++)
{
var x = buffer.indexOf(Buffer.from(hexList[i], 'hex'), offset);
if (x > 0 && x < sentinel)
sentinel = x;
}
if (sentinel === Infinity)
sentinel = -1;
return sentinel;
}
function readMarkerTrack(buffer, nuendo, sampleRate, trackIndex, offset, markerTracks)
{
debug('Read marker track at', offset);
var endOfTracksSentinel = minSentinel(buffer, offset, [
'566964656F5F55726C',
'00000000FFFFFFFF',
]);
debug('End of tracks sentinel', endOfTracksSentinel);
var nextTrackStart = buffer.indexOf(Buffer.from('800000BF', 'hex'), offset);
debug('Next track start:', nextTrackStart);
if (nextTrackStart > offset)
nextTrackStart += 8;
var hasNextTrack = nextTrackStart > offset && nextTrackStart < endOfTracksSentinel;
var maxOffset = hasNextTrack ? nextTrackStart : endOfTracksSentinel;
debug('Max offset', maxOffset);
var trackNameOffset = offset;
var trackNameLength = buffer.readUInt32BE(trackNameOffset);
if (trackNameLength > 256)
throw (new Error('Marker track name invalid'));
var trackName = buffer.toString('utf8', trackNameOffset + 4, trackNameOffset + 4 + trackNameLength - 4);
debug('Track name:', trackName);
var timebaseMode = buffer.readUInt8(trackNameOffset - 24);
if (timebaseMode === 0x40)
isMusicalTimebase = false;
else if (timebaseMode === 0x41)
isMusicalTimebase = true;
else
throw (new Error('Could not determine timebase'));
debug(isMusicalTimebase ? 'Detected musical timebase' : 'Detected linear timebase');
var trackData = {
'Name': trackName,
'Timebase': isMusicalTimebase ? 'Musical' : 'Linear',
'Markers': []
};
var trackDataStart = trackNameOffset + 4 + trackNameLength + 16;
var trackDataHeader = buffer.readUInt32BE(trackDataStart);
debug('Track data header', trackDataHeader.toString(16));
if (trackDataHeader === 0xfffffffe)
{
debug('First track');
readMarkerTrackData(buffer, nuendo, isMusicalTimebase, sampleRate, trackDataStart + 51, maxOffset, trackData);
}
else if (trackIndex === 0)
{
debug('First track (skipping intervening events)')
var rangeMarkerStart = buffer.indexOf(Buffer.from('MRangeMarkerEvent'), trackDataStart);
if (rangeMarkerStart < 0)
throw (new Error('Cannot find start of first track marker data'));
readMarkerTrackData(buffer, nuendo, isMusicalTimebase, sampleRate, rangeMarkerStart + 20, maxOffset, trackData);
}
else if (trackDataHeader != 0)
{
debug('Next track');
readMarkerTrackData(buffer, nuendo, isMusicalTimebase, sampleRate, trackDataStart + 4, maxOffset, trackData);
}
else
{
debug('No marker data');
}
markerTracks.push(trackData);
return (hasNextTrack ? nextTrackStart : -1);
}
function readMarkerTrackData(buffer, nuendo, isMusicalTimebase, sampleRate, offset, maxOffset, trackData)
{
while (offset >= 0)
{
var header = buffer.readUInt32BE(offset);
debug('Marker header', header.toString(16));
if (header == 0x62726146)
{
offset = -1;
continue;
}
offset += 4;
var markerNameLength = buffer.readUInt32BE(offset);
if (markerNameLength > 256)
throw (new Error('Marker name length invalid'));
offset += 4;
var markerName = buffer.toString('utf8', offset, offset + markerNameLength - 4);
debug('Marker name:', markerName);
offset += markerNameLength;
var markerID = buffer.readUInt32BE(offset);
offset += 4;
offset += 2;
var division;
if (isMusicalTimebase)
division = 480;
else
division = 1;
var markerStart = buffer.readDoubleBE(offset) / division;
offset += 8;
var markerLength = buffer.readDoubleBE(offset) / division;
offset += 8;
debug('Marker data end: ', offset);
var nextMarkerDataEnd = buffer.indexOf(Buffer.from('414D41740022', 'hex'), offset + 17);
if (nextMarkerDataEnd > 0)
nextMarkerDataEnd -= 16;
debug('Next marker data end: ', nextMarkerDataEnd);
if (nextMarkerDataEnd < 0 || nextMarkerDataEnd > maxOffset)
{
offset = -1;
}
else
{
offset = nextMarkerDataEnd - 22;
for (var i = 0; i < 256; i++)
{
if (buffer.readUInt32BE(offset - 4 - i) === i)
{
offset -= 8 + i;
debug('Found next marker at:', offset);
break;
}
}
}
markerType = markerLength === 0 ? 'Marker' : 'Cycle';
if (isMusicalTimebase)
{
trackData.Markers.push({
'Type': markerType,
'ID': markerID,
'Name': markerName,
'Beats': {
'Start': markerStart,
'Length': markerLength !== 0 ? markerLength : undefined,
'End': markerStart + markerLength,
}
});
}
else
{
trackData.Markers.push({
'Type': markerType,
'ID': markerID,
'Name': markerName,
'Seconds': {
'Start': markerStart,
'Length': markerLength !== 0 ? markerLength : undefined,
'End': markerLength !== 0 ? markerStart + markerLength : undefined,
},
'Samples': {
'Start': Math.round(markerStart * sampleRate),
'Length': markerLength !== 0 ? Math.round(markerLength * sampleRate) : undefined,
'End': markerLength !== 0 ? Math.round((markerStart + markerLength) * sampleRate) : undefined,
}
});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment