Last active
April 26, 2022 00:30
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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