museaikit
Version:
A powerful music-focused AI toolkit
532 lines • 21.6 kB
JavaScript
import { NoteSequence } from '../protobuf/index';
import * as constants from './constants';
const QUANTIZE_CUTOFF = 0.5;
export class MultipleTimeSignatureException extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class BadTimeSignatureException extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class NegativeTimeException extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class MultipleTempoException extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class QuantizationStatusException extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
function isPowerOf2(n) {
return n && (n & (n - 1)) === 0;
}
export function clone(ns) {
return NoteSequence.decode(NoteSequence.encode(ns).finish());
}
export function stepsPerQuarterToStepsPerSecond(stepsPerQuarter, qpm) {
return stepsPerQuarter * qpm / 60.0;
}
export function quantizeToStep(unquantizedSeconds, stepsPerSecond, quantizeCutoff = QUANTIZE_CUTOFF) {
const unquantizedSteps = unquantizedSeconds * stepsPerSecond;
return Math.floor(unquantizedSteps + (1 - quantizeCutoff));
}
function getQuantizedTimeEvents(ns) {
return ns.controlChanges.concat(ns.textAnnotations);
}
function quantizeNotesAndEvents(ns, stepsPerSecond) {
for (const note of ns.notes) {
note.quantizedStartStep = quantizeToStep(note.startTime, stepsPerSecond);
note.quantizedEndStep = quantizeToStep(note.endTime, stepsPerSecond);
if (note.quantizedEndStep === note.quantizedStartStep) {
note.quantizedEndStep += 1;
}
if (note.quantizedStartStep < 0 || note.quantizedEndStep < 0) {
throw new NegativeTimeException(`Got negative note time: start_step = ` +
`${note.quantizedStartStep}, end_step = ` +
`${note.quantizedEndStep}`);
}
if (note.quantizedEndStep > ns.totalQuantizedSteps) {
ns.totalQuantizedSteps = note.quantizedEndStep;
}
}
getQuantizedTimeEvents(ns).forEach(event => {
event.quantizedStep = quantizeToStep(event.time, stepsPerSecond);
if (event.quantizedStep < 0) {
throw new NegativeTimeException(`Got negative event time: step = ${event.quantizedStep}`);
}
});
}
function assertSingleTempo(ns) {
if (!ns.tempos || ns.tempos.length === 0) {
return;
}
ns.tempos.sort((a, b) => a.time - b.time);
if (ns.tempos[0].time !== 0 &&
ns.tempos[0].qpm !== constants.DEFAULT_QUARTERS_PER_MINUTE) {
throw new MultipleTempoException('NoteSequence has an implicit tempo change from initial ' +
`${constants.DEFAULT_QUARTERS_PER_MINUTE} qpm to ` +
`${ns.tempos[0].qpm} qpm at ${ns.tempos[0].time} seconds.`);
}
for (let i = 1; i < ns.tempos.length; i++) {
if (ns.tempos[i].qpm !== ns.tempos[0].qpm) {
throw new MultipleTempoException('NoteSequence has at least one tempo change from ' +
`${ns.tempos[0].qpm} qpm to ${ns.tempos[i].qpm}` +
`qpm at ${ns.tempos[i].time} seconds.`);
}
}
}
export function quantizeNoteSequence(ns, stepsPerQuarter) {
const qns = clone(ns);
qns.quantizationInfo =
NoteSequence.QuantizationInfo.create({ stepsPerQuarter });
if (qns.timeSignatures.length > 0) {
qns.timeSignatures.sort((a, b) => a.time - b.time);
if (qns.timeSignatures[0].time !== 0 &&
!(qns.timeSignatures[0].numerator === 4 &&
qns.timeSignatures[0].denominator === 4)) {
throw new MultipleTimeSignatureException('NoteSequence has an implicit change from initial 4/4 time ' +
`signature to ${qns.timeSignatures[0].numerator}/` +
`${qns.timeSignatures[0].denominator} at ` +
`${qns.timeSignatures[0].time} seconds.`);
}
for (let i = 1; i < qns.timeSignatures.length; i++) {
const timeSignature = qns.timeSignatures[i];
if (timeSignature.numerator !== qns.timeSignatures[0].numerator ||
timeSignature.denominator !== qns.timeSignatures[0].denominator) {
throw new MultipleTimeSignatureException('NoteSequence has at least one time signature change from ' +
`${qns.timeSignatures[0].numerator}/` +
`${qns.timeSignatures[0].denominator} to ` +
`${timeSignature.numerator}/${timeSignature.denominator} ` +
`at ${timeSignature.time} seconds`);
}
}
qns.timeSignatures[0].time = 0;
qns.timeSignatures = [qns.timeSignatures[0]];
}
else {
const timeSignature = NoteSequence.TimeSignature.create({ numerator: 4, denominator: 4, time: 0 });
qns.timeSignatures.push(timeSignature);
}
const firstTS = qns.timeSignatures[0];
if (!isPowerOf2(firstTS.denominator)) {
throw new BadTimeSignatureException('Denominator is not a power of 2. Time signature: ' +
`${firstTS.numerator}/${firstTS.denominator}`);
}
if (firstTS.numerator === 0) {
throw new BadTimeSignatureException('Numerator is 0. Time signature: ' +
`${firstTS.numerator}/${firstTS.denominator}`);
}
if (qns.tempos.length > 0) {
assertSingleTempo(qns);
qns.tempos[0].time = 0;
qns.tempos = [qns.tempos[0]];
}
else {
const tempo = NoteSequence.Tempo.create({ qpm: constants.DEFAULT_QUARTERS_PER_MINUTE, time: 0 });
qns.tempos.push(tempo);
}
const stepsPerSecond = stepsPerQuarterToStepsPerSecond(stepsPerQuarter, qns.tempos[0].qpm);
qns.totalQuantizedSteps = quantizeToStep(ns.totalTime, stepsPerSecond);
quantizeNotesAndEvents(qns, stepsPerSecond);
return qns;
}
export function isQuantizedSequence(ns) {
return ns.quantizationInfo &&
(ns.quantizationInfo.stepsPerQuarter > 0 ||
ns.quantizationInfo.stepsPerSecond > 0);
}
export function assertIsQuantizedSequence(ns) {
if (!isQuantizedSequence(ns)) {
throw new QuantizationStatusException(`NoteSequence ${ns.id} is not quantized (missing quantizationInfo)`);
}
}
export function isRelativeQuantizedSequence(ns) {
return ns.quantizationInfo && ns.quantizationInfo.stepsPerQuarter > 0;
}
export function assertIsRelativeQuantizedSequence(ns) {
if (!isRelativeQuantizedSequence(ns)) {
throw new QuantizationStatusException(`NoteSequence ${ns.id} is not quantized or is quantized based on absolute timing`);
}
}
export function isAbsoluteQuantizedSequence(ns) {
return ns.quantizationInfo && ns.quantizationInfo.stepsPerSecond > 0;
}
export function assertIsAbsoluteQuantizedSequence(ns) {
if (!isAbsoluteQuantizedSequence(ns)) {
throw new QuantizationStatusException(`NoteSequence ${ns.id} is not quantized or is quantized based on relative timing`);
}
}
export function unquantizeSequence(qns, qpm) {
assertIsRelativeQuantizedSequence(qns);
assertSingleTempo(qns);
const ns = clone(qns);
if (qpm) {
if (ns.tempos && ns.tempos.length > 0) {
ns.tempos[0].qpm = qpm;
}
else {
ns.tempos.push(NoteSequence.Tempo.create({ time: 0, qpm }));
}
}
else {
qpm = (qns.tempos && qns.tempos.length > 0) ?
ns.tempos[0].qpm :
constants.DEFAULT_QUARTERS_PER_MINUTE;
}
const stepToSeconds = (step) => step / ns.quantizationInfo.stepsPerQuarter * (60 / qpm);
ns.totalTime = stepToSeconds(ns.totalQuantizedSteps);
ns.notes.forEach(n => {
n.startTime = stepToSeconds(n.quantizedStartStep);
n.endTime = stepToSeconds(n.quantizedEndStep);
ns.totalTime = Math.max(ns.totalTime, n.endTime);
delete n.quantizedStartStep;
delete n.quantizedEndStep;
});
getQuantizedTimeEvents(ns).forEach(event => {
event.time = stepToSeconds(event.time);
});
delete ns.totalQuantizedSteps;
delete ns.quantizationInfo;
return ns;
}
export function createQuantizedNoteSequence(stepsPerQuarter = constants.DEFAULT_STEPS_PER_QUARTER, qpm = constants.DEFAULT_QUARTERS_PER_MINUTE) {
return NoteSequence.create({ quantizationInfo: { stepsPerQuarter }, tempos: [{ qpm }] });
}
export function mergeInstruments(ns) {
const result = clone(ns);
const events = result.notes.concat(result.pitchBends).concat(result.controlChanges);
const programs = Array.from(new Set(events.filter(e => !e.isDrum).map(e => e.program)));
events.forEach(e => {
if (e.isDrum) {
e.program = 0;
e.instrument = programs.length;
}
else {
e.instrument = programs.indexOf(e.program);
}
});
return result;
}
export function replaceInstruments(originalSequence, replaceSequence) {
const instrumentsInOriginal = new Set(originalSequence.notes.map(n => n.instrument));
const instrumentsInReplace = new Set(replaceSequence.notes.map(n => n.instrument));
const newNotes = [];
originalSequence.notes.forEach(n => {
if (!instrumentsInReplace.has(n.instrument)) {
newNotes.push(NoteSequence.Note.create(n));
}
});
replaceSequence.notes.forEach(n => {
if (instrumentsInOriginal.has(n.instrument)) {
newNotes.push(NoteSequence.Note.create(n));
}
});
const output = clone(originalSequence);
output.notes = newNotes.sort((a, b) => {
const voiceCompare = a.instrument - b.instrument;
if (voiceCompare) {
return voiceCompare;
}
return a.quantizedStartStep - b.quantizedStartStep;
});
return output;
}
export function mergeConsecutiveNotes(sequence) {
assertIsQuantizedSequence(sequence);
const output = clone(sequence);
output.notes = [];
const newNotes = sequence.notes.sort((a, b) => {
const voiceCompare = a.instrument - b.instrument;
if (voiceCompare) {
return voiceCompare;
}
return a.quantizedStartStep - b.quantizedStartStep;
});
const note = new NoteSequence.Note();
note.pitch = newNotes[0].pitch;
note.instrument = newNotes[0].instrument;
note.quantizedStartStep = newNotes[0].quantizedStartStep;
note.quantizedEndStep = newNotes[0].quantizedEndStep;
output.notes.push(note);
let o = 0;
for (let i = 1; i < newNotes.length; i++) {
const thisNote = newNotes[i];
const previousNote = output.notes[o];
if (previousNote.instrument === thisNote.instrument &&
previousNote.pitch === thisNote.pitch &&
thisNote.quantizedStartStep === previousNote.quantizedEndStep &&
thisNote.quantizedStartStep % 16 !== 0) {
output.notes[o].quantizedEndStep +=
thisNote.quantizedEndStep - thisNote.quantizedStartStep;
}
else {
const note = new NoteSequence.Note();
note.pitch = newNotes[i].pitch;
note.instrument = newNotes[i].instrument;
note.quantizedStartStep = newNotes[i].quantizedStartStep;
note.quantizedEndStep = newNotes[i].quantizedEndStep;
output.notes.push(note);
o++;
}
}
return output;
}
export function applySustainControlChanges(noteSequence, sustainControlNumber = 64) {
let MessageType;
(function (MessageType) {
MessageType[MessageType["SUSTAIN_ON"] = 0] = "SUSTAIN_ON";
MessageType[MessageType["SUSTAIN_OFF"] = 1] = "SUSTAIN_OFF";
MessageType[MessageType["NOTE_ON"] = 2] = "NOTE_ON";
MessageType[MessageType["NOTE_OFF"] = 3] = "NOTE_OFF";
})(MessageType || (MessageType = {}));
const isQuantized = isQuantizedSequence(noteSequence);
if (isQuantized) {
throw new Error('Can only apply sustain to unquantized NoteSequence.');
}
const sequence = clone(noteSequence);
const events = [];
for (const note of sequence.notes) {
if (note.isDrum === false) {
if (note.startTime !== null) {
events.push({
time: note.startTime,
type: MessageType.NOTE_ON,
event: note
});
}
if (note.endTime !== null) {
events.push({
time: note.endTime,
type: MessageType.NOTE_OFF,
event: note
});
}
}
}
for (const cc of sequence.controlChanges) {
if (cc.controlNumber === sustainControlNumber) {
const value = cc.controlValue;
if ((value < 0) || (value > 127)) {
}
if (value >= 64) {
events.push({
time: cc.time,
type: MessageType.SUSTAIN_ON,
event: cc
});
}
else if (value < 64) {
events.push({
time: cc.time,
type: MessageType.SUSTAIN_OFF,
event: cc
});
}
}
}
events.sort((a, b) => a.time - b.time);
const activeNotes = {};
const susActive = {};
let time = 0;
for (const item of events) {
time = item.time;
const type = item.type;
const event = item.event;
if (type === MessageType.SUSTAIN_ON) {
susActive[event.instrument] = true;
}
else if (type === MessageType.SUSTAIN_OFF) {
susActive[event.instrument] = false;
const newActiveNotes = [];
if (!(event.instrument in activeNotes)) {
activeNotes[event.instrument] = [];
}
for (const note of activeNotes[event.instrument]) {
if (note.endTime < time) {
note.endTime = time;
if (time > sequence.totalTime) {
sequence.totalTime = time;
}
}
else {
newActiveNotes.push(note);
}
}
activeNotes[event.instrument] = newActiveNotes;
}
else if (type === MessageType.NOTE_ON) {
if (susActive[event.instrument] === true) {
const newActiveNotes = [];
if (!(event.instrument in activeNotes)) {
activeNotes[event.instrument] = [];
}
for (const note of activeNotes[event.instrument]) {
if (note.pitch === event.pitch) {
note.endTime = time;
if (note.startTime === note.endTime) {
sequence.notes.push(note);
}
}
else {
newActiveNotes.push(note);
}
}
activeNotes[event.instrument] = newActiveNotes;
}
if (!(event.instrument in activeNotes)) {
activeNotes[event.instrument] = [];
}
activeNotes[event.instrument].push(event);
}
else if (type === MessageType.NOTE_OFF) {
if (susActive[event.instrument] === true) {
}
else {
const index = activeNotes[event.instrument].indexOf(event);
if (index > -1) {
activeNotes[event.instrument].splice(index, 1);
}
}
}
}
for (const instrument of Object.values(activeNotes)) {
for (const note of instrument) {
note.endTime = time;
sequence.totalTime = time;
}
}
return sequence;
}
export function concatenate(concatenateSequences, sequenceDurations) {
if (sequenceDurations &&
sequenceDurations.length !== concatenateSequences.length) {
throw new Error(`Number of sequences to concatenate and their individual
durations does not match.`);
}
if (isQuantizedSequence(concatenateSequences[0])) {
for (let i = 0; i < concatenateSequences.length; ++i) {
assertIsQuantizedSequence(concatenateSequences[i]);
if (concatenateSequences[i].quantizationInfo.stepsPerQuarter !==
concatenateSequences[0].quantizationInfo.stepsPerQuarter) {
throw new Error('Not all sequences have the same quantizationInfo');
}
}
return concatenateHelper(concatenateSequences, 'totalQuantizedSteps', 'quantizedStartStep', 'quantizedEndStep', sequenceDurations);
}
else {
return concatenateHelper(concatenateSequences, 'totalTime', 'startTime', 'endTime', sequenceDurations);
}
}
export function trim(ns, start, end, truncateEndNotes) {
return isQuantizedSequence(ns) ?
trimHelper(ns, start, end, 'totalQuantizedSteps', 'quantizedStartStep', 'quantizedEndStep', truncateEndNotes) :
trimHelper(ns, start, end, 'totalTime', 'startTime', 'endTime', truncateEndNotes);
}
function concatenateHelper(seqs, totalKey, startKey, endKey, sequenceDurations) {
let concatSeq;
let totalDuration = 0;
for (let i = 0; i < seqs.length; ++i) {
const seqDuration = sequenceDurations ? sequenceDurations[i] : seqs[i][totalKey];
if (seqDuration === 0) {
throw Error(`Sequence ${seqs[i].id} has no ${totalKey}, and no individual duration was provided.`);
}
if (i === 0) {
concatSeq = clone(seqs[0]);
}
else {
Array.prototype.push.apply(concatSeq.notes, seqs[i].notes.map(n => {
const newN = NoteSequence.Note.create(n);
newN[startKey] += totalDuration;
newN[endKey] += totalDuration;
return newN;
}));
}
totalDuration += seqDuration;
}
concatSeq[totalKey] = totalDuration;
return concatSeq;
}
function trimHelper(ns, start, end, totalKey, startKey, endKey, truncateEndNotes) {
const result = clone(ns);
result[totalKey] = end;
result.notes = result.notes.filter(n => n[startKey] >= start && n[startKey] <= end &&
(truncateEndNotes || n[endKey] <= end));
result[totalKey] -= start;
for (let i = 0; i < result.notes.length; i++) {
result.notes[i][startKey] -= start;
result.notes[i][endKey] -= start;
if (truncateEndNotes) {
result.notes[i][endKey] = Math.min(result.notes[i][endKey], result[totalKey]);
}
}
return result;
}
export function split(seq, chunkSize) {
assertIsQuantizedSequence(seq);
const ns = clone(seq);
const notesBystartStep = ns.notes.sort((a, b) => a.quantizedStartStep - b.quantizedStartStep);
const chunks = [];
let startStep = 0;
let currentNotes = [];
for (let i = 0; i < notesBystartStep.length; i++) {
const note = notesBystartStep[i];
const originalStartStep = note.quantizedStartStep;
const originalEndStep = note.quantizedEndStep;
note.quantizedStartStep -= startStep;
note.quantizedEndStep -= startStep;
if (note.quantizedStartStep < 0) {
continue;
}
if (note.quantizedEndStep <= chunkSize) {
currentNotes.push(note);
}
else {
if (note.quantizedStartStep < chunkSize) {
const newNote = NoteSequence.Note.create(note);
newNote.quantizedEndStep = chunkSize;
newNote.startTime = newNote.endTime = undefined;
currentNotes.push(newNote);
note.quantizedStartStep = startStep + chunkSize;
note.quantizedEndStep = originalEndStep;
}
else {
note.quantizedStartStep = originalStartStep;
note.quantizedEndStep = originalEndStep;
}
if (note.quantizedEndStep > chunkSize ||
note.quantizedStartStep > chunkSize) {
i = i - 1;
}
if (currentNotes.length !== 0) {
const newSequence = clone(ns);
newSequence.notes = currentNotes;
newSequence.totalQuantizedSteps = chunkSize;
chunks.push(newSequence);
}
currentNotes = [];
startStep += chunkSize;
}
}
if (currentNotes.length !== 0) {
const newSequence = clone(ns);
newSequence.notes = currentNotes;
newSequence.totalQuantizedSteps = chunkSize;
chunks.push(newSequence);
}
return chunks;
}
//# sourceMappingURL=sequences.js.map