@magenta/music
Version:
Make music with machine learning, in the browser.
565 lines • 23.2 kB
JavaScript
import * as sr from 'staffrender';
import { MAX_MIDI_PITCH, MIN_MIDI_PITCH } from './constants';
import * as logging from './logging';
import * as sequences from './sequences';
const MIN_NOTE_LENGTH = 1;
export class BaseVisualizer {
constructor(sequence, config = {}) {
const isQuantized = sequences.isQuantizedSequence(sequence);
const qpm = (sequence.tempos && sequence.tempos.length > 0) ?
sequence.tempos[0].qpm : undefined;
this.noteSequence = isQuantized ?
sequences.unquantizeSequence(sequence, qpm) : sequence;
const defaultPixelsPerTimeStep = 30;
this.config = {
noteHeight: config.noteHeight || 6,
noteSpacing: config.noteSpacing || 1,
pixelsPerTimeStep: config.pixelsPerTimeStep || defaultPixelsPerTimeStep,
noteRGB: config.noteRGB || '8, 41, 64',
activeNoteRGB: config.activeNoteRGB || '240, 84, 119',
minPitch: config.minPitch,
maxPitch: config.maxPitch,
};
const size = this.getSize();
this.width = size.width;
this.height = size.height;
}
updateMinMaxPitches(noExtraPadding = false) {
if (this.config.minPitch && this.config.maxPitch) {
return;
}
if (this.config.minPitch === undefined) {
this.config.minPitch = MAX_MIDI_PITCH;
}
if (this.config.maxPitch === undefined) {
this.config.maxPitch = MIN_MIDI_PITCH;
}
for (const note of this.noteSequence.notes) {
this.config.minPitch = Math.min(note.pitch, this.config.minPitch);
this.config.maxPitch = Math.max(note.pitch, this.config.maxPitch);
}
if (!noExtraPadding) {
this.config.minPitch -= 2;
this.config.maxPitch += 2;
}
}
getSize() {
this.updateMinMaxPitches();
const height = (this.config.maxPitch - this.config.minPitch) * this.config.noteHeight;
const endTime = this.noteSequence.totalTime;
if (!endTime) {
throw new Error('The sequence you are using with the visualizer does not have a ' +
'totalQuantizedSteps or totalTime ' +
'field set, so the visualizer can\'t be horizontally ' +
'sized correctly.');
}
const width = (endTime * this.config.pixelsPerTimeStep);
return { width, height };
}
getNotePosition(note, noteIndex) {
const duration = this.getNoteEndTime(note) - this.getNoteStartTime(note);
const x = (this.getNoteStartTime(note) * this.config.pixelsPerTimeStep);
const w = Math.max(this.config.pixelsPerTimeStep * duration - this.config.noteSpacing, MIN_NOTE_LENGTH);
const y = this.height -
((note.pitch - this.config.minPitch) * this.config.noteHeight);
return { x, y, w, h: this.config.noteHeight };
}
scrollIntoViewIfNeeded(scrollIntoView, activeNotePosition) {
if (scrollIntoView && this.parentElement) {
const containerWidth = this.parentElement.getBoundingClientRect().width;
if (activeNotePosition >
(this.parentElement.scrollLeft + containerWidth)) {
this.parentElement.scrollLeft = activeNotePosition - 20;
}
}
}
getNoteStartTime(note) {
return Math.round(note.startTime * 100000000) / 100000000;
}
getNoteEndTime(note) {
return Math.round(note.endTime * 100000000) / 100000000;
}
isPaintingActiveNote(note, playedNote) {
const isPlayedNote = this.getNoteStartTime(note) === this.getNoteStartTime(playedNote);
const heldDownDuringPlayedNote = this.getNoteStartTime(note) <= this.getNoteStartTime(playedNote) &&
this.getNoteEndTime(note) >= this.getNoteEndTime(playedNote);
return isPlayedNote || heldDownDuringPlayedNote;
}
}
export class PianoRollCanvasVisualizer extends BaseVisualizer {
constructor(sequence, canvas, config = {}) {
super(sequence, config);
this.ctx = canvas.getContext('2d');
this.parentElement = canvas.parentElement;
const dpr = window.devicePixelRatio || 1;
if (this.ctx) {
this.ctx.canvas.width = dpr * this.width;
this.ctx.canvas.height = dpr * this.height;
canvas.style.width = `${this.width}px`;
canvas.style.height = `${this.height}px`;
this.ctx.scale(dpr, dpr);
}
this.redraw();
}
redraw(activeNote, scrollIntoView) {
this.clear();
let activeNotePosition;
for (let i = 0; i < this.noteSequence.notes.length; i++) {
const note = this.noteSequence.notes[i];
const size = this.getNotePosition(note, i);
const opacityBaseline = 0.2;
const opacity = note.velocity ? note.velocity / 100 + opacityBaseline : 1;
const isActive = activeNote && this.isPaintingActiveNote(note, activeNote);
const fill = `rgba(${isActive ? this.config.activeNoteRGB : this.config.noteRGB},
${opacity})`;
this.redrawNote(size.x, size.y, size.w, size.h, fill);
if (isActive && note === activeNote) {
activeNotePosition = size.x;
}
}
this.scrollIntoViewIfNeeded(scrollIntoView, activeNotePosition);
return activeNotePosition;
}
clear() {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
clearActiveNotes() {
this.redraw();
}
redrawNote(x, y, w, h, fill) {
this.ctx.fillStyle = fill;
this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
}
}
export class Visualizer extends PianoRollCanvasVisualizer {
constructor(sequence, canvas, config = {}) {
super(sequence, canvas, config);
logging.log('mm.Visualizer is deprecated, and will be removed in a future \
version. Please use mm.PianoRollCanvasVisualizer instead', 'mm.Visualizer', 5);
}
}
export class BaseSVGVisualizer extends BaseVisualizer {
constructor(sequence, config = {}) {
super(sequence, config);
this.drawn = false;
}
redraw(activeNote, scrollIntoView) {
if (!this.drawn) {
this.draw();
}
if (!activeNote) {
return null;
}
this.unfillActiveRect(this.svg);
let activeNotePosition;
for (let i = 0; i < this.noteSequence.notes.length; i++) {
const note = this.noteSequence.notes[i];
const isActive = activeNote && this.isPaintingActiveNote(note, activeNote);
if (!isActive) {
continue;
}
const el = this.svg.querySelector(`rect[data-index="${i}"]`);
this.fillActiveRect(el, note);
if (note === activeNote) {
activeNotePosition = parseFloat(el.getAttribute('x'));
}
}
this.scrollIntoViewIfNeeded(scrollIntoView, activeNotePosition);
return activeNotePosition;
}
fillActiveRect(el, note) {
el.setAttribute('fill', this.getNoteFillColor(note, true));
el.classList.add('active');
}
unfillActiveRect(svg) {
const els = svg.querySelectorAll('rect.active');
for (let i = 0; i < els.length; ++i) {
const el = els[i];
const fill = this.getNoteFillColor(this.noteSequence.notes[parseInt(el.getAttribute('data-index'), 10)], false);
el.setAttribute('fill', fill);
el.classList.remove('active');
}
}
draw() {
for (let i = 0; i < this.noteSequence.notes.length; i++) {
const note = this.noteSequence.notes[i];
const size = this.getNotePosition(note, i);
const fill = this.getNoteFillColor(note, false);
const dataAttributes = [
['index', i],
['instrument', note.instrument],
['program', note.program],
['isDrum', note.isDrum === true],
['pitch', note.pitch],
];
const cssProperties = [
['--midi-velocity',
String(note.velocity !== undefined ? note.velocity : 127)]
];
this.drawNote(size.x, size.y, size.w, size.h, fill, dataAttributes, cssProperties);
}
this.drawn = true;
}
getNoteFillColor(note, isActive) {
const opacityBaseline = 0.2;
const opacity = note.velocity ? note.velocity / 100 + opacityBaseline : 1;
const fill = `rgba(${isActive ? this.config.activeNoteRGB : this.config.noteRGB},
${opacity})`;
return fill;
}
drawNote(x, y, w, h, fill, dataAttributes, cssProperties) {
if (!this.svg) {
return;
}
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.classList.add('note');
rect.setAttribute('fill', fill);
rect.setAttribute('x', `${Math.round(x)}`);
rect.setAttribute('y', `${Math.round(y)}`);
rect.setAttribute('width', `${Math.round(w)}`);
rect.setAttribute('height', `${Math.round(h)}`);
dataAttributes.forEach(([key, value]) => {
if (value !== undefined) {
rect.dataset[key] = `${value}`;
}
});
cssProperties.forEach(([key, value]) => {
rect.style.setProperty(key, value);
});
this.svg.appendChild(rect);
}
clear() {
this.svg.innerHTML = '';
this.drawn = false;
}
clearActiveNotes() {
this.unfillActiveRect(this.svg);
}
}
export class PianoRollSVGVisualizer extends BaseSVGVisualizer {
constructor(sequence, svg, config = {}) {
super(sequence, config);
if (!(svg instanceof SVGSVGElement)) {
throw new Error('This visualizer requires an <svg> element to display the visualization');
}
this.svg = svg;
this.parentElement = svg.parentElement;
const size = this.getSize();
this.width = size.width;
this.height = size.height;
this.svg.style.width = `${this.width}px`;
this.svg.style.height = `${this.height}px`;
this.clear();
this.draw();
}
}
export class WaterfallSVGVisualizer extends BaseSVGVisualizer {
constructor(sequence, parentElement, config = {}) {
super(sequence, config);
this.NOTES_PER_OCTAVE = 12;
this.WHITE_NOTES_PER_OCTAVE = 7;
this.LOW_C = 24;
this.firstDrawnOctave = 0;
this.lastDrawnOctave = 6;
if (!(parentElement instanceof HTMLDivElement)) {
throw new Error('This visualizer requires a <div> element to display the visualization');
}
this.config.whiteNoteWidth = config.whiteNoteWidth || 20;
this.config.blackNoteWidth =
config.blackNoteWidth || this.config.whiteNoteWidth * 2 / 3;
this.config.whiteNoteHeight = config.whiteNoteHeight || 70;
this.config.blackNoteHeight = config.blackNoteHeight || (2 * 70 / 3);
this.config.showOnlyOctavesUsed = config.showOnlyOctavesUsed;
this.setupDOM(parentElement);
const size = this.getSize();
this.width = size.width;
this.height = size.height;
this.svg.style.width = `${this.width}px`;
this.svg.style.height = `${this.height}px`;
this.svgPiano.style.width = `${this.width}px`;
this.svgPiano.style.height = `${this.config.whiteNoteHeight}px`;
this.parentElement.style.width =
`${this.width + this.config.whiteNoteWidth}px`;
this.parentElement.scrollTop = this.parentElement.scrollHeight;
this.clear();
this.drawPiano();
this.draw();
}
setupDOM(container) {
this.parentElement = document.createElement('div');
this.parentElement.classList.add('waterfall-notes-container');
const height = Math.max(container.getBoundingClientRect().height, 200);
this.parentElement.style.paddingTop =
`${height - this.config.whiteNoteHeight}px`;
this.parentElement.style.height =
`${height - this.config.whiteNoteHeight}px`;
this.parentElement.style.boxSizing = 'border-box';
this.parentElement.style.overflowX = 'hidden';
this.parentElement.style.overflowY = 'auto';
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svgPiano =
document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg.classList.add('waterfall-notes');
this.svgPiano.classList.add('waterfall-piano');
this.parentElement.appendChild(this.svg);
container.innerHTML = '';
container.appendChild(this.parentElement);
container.appendChild(this.svgPiano);
}
redraw(activeNote, scrollIntoView) {
if (!this.drawn) {
this.draw();
}
if (!activeNote) {
return null;
}
this.clearActiveNotes();
this.parentElement.style.paddingTop = this.parentElement.style.height;
for (let i = 0; i < this.noteSequence.notes.length; i++) {
const note = this.noteSequence.notes[i];
const isActive = activeNote && this.isPaintingActiveNote(note, activeNote);
if (!isActive) {
continue;
}
const el = this.svg.querySelector(`rect[data-index="${i}"]`);
this.fillActiveRect(el, note);
const key = this.svgPiano.querySelector(`rect[data-pitch="${note.pitch}"]`);
this.fillActiveRect(key, note);
if (note === activeNote) {
const y = parseFloat(el.getAttribute('y'));
const height = parseFloat(el.getAttribute('height'));
if (y < (this.parentElement.scrollTop - height)) {
this.parentElement.scrollTop = y + height;
}
return y;
}
}
return null;
}
getSize() {
this.updateMinMaxPitches(true);
let whiteNotesDrawn = 52;
if (this.config.showOnlyOctavesUsed) {
let foundFirst = false, foundLast = false;
for (let i = 1; i < 7; i++) {
const c = this.LOW_C + this.NOTES_PER_OCTAVE * i;
if (!foundFirst && c > this.config.minPitch) {
this.firstDrawnOctave = i - 1;
foundFirst = true;
}
if (!foundLast && c > this.config.maxPitch) {
this.lastDrawnOctave = i - 1;
foundLast = true;
}
}
whiteNotesDrawn = (this.lastDrawnOctave - this.firstDrawnOctave + 1) *
this.WHITE_NOTES_PER_OCTAVE;
}
const width = whiteNotesDrawn * this.config.whiteNoteWidth;
const endTime = this.noteSequence.totalTime;
if (!endTime) {
throw new Error('The sequence you are using with the visualizer does not have a ' +
'totalQuantizedSteps or totalTime ' +
'field set, so the visualizer can\'t be horizontally ' +
'sized correctly.');
}
const height = Math.max(endTime * this.config.pixelsPerTimeStep, MIN_NOTE_LENGTH);
return { width, height };
}
getNotePosition(note, noteIndex) {
const rect = this.svgPiano.querySelector(`rect[data-pitch="${note.pitch}"]`);
if (!rect) {
return null;
}
const len = this.getNoteEndTime(note) - this.getNoteStartTime(note);
const x = Number(rect.getAttribute('x'));
const w = Number(rect.getAttribute('width'));
const h = Math.max(this.config.pixelsPerTimeStep * len - this.config.noteSpacing, MIN_NOTE_LENGTH);
const y = this.height -
(this.getNoteStartTime(note) * this.config.pixelsPerTimeStep) - h;
return { x, y, w, h };
}
drawPiano() {
this.svgPiano.innerHTML = '';
const blackNoteOffset = this.config.whiteNoteWidth - this.config.blackNoteWidth / 2;
const blackNoteIndexes = [1, 3, 6, 8, 10];
let x = 0;
let currentPitch = 0;
if (this.config.showOnlyOctavesUsed) {
currentPitch =
(this.firstDrawnOctave * this.NOTES_PER_OCTAVE) + this.LOW_C;
}
else {
currentPitch = this.LOW_C - 3;
this.drawWhiteKey(currentPitch, x);
this.drawWhiteKey(currentPitch + 2, this.config.whiteNoteWidth);
currentPitch += 3;
x = 2 * this.config.whiteNoteWidth;
}
for (let o = this.firstDrawnOctave; o <= this.lastDrawnOctave; o++) {
for (let i = 0; i < this.NOTES_PER_OCTAVE; i++) {
if (blackNoteIndexes.indexOf(i) === -1) {
this.drawWhiteKey(currentPitch, x);
x += this.config.whiteNoteWidth;
}
currentPitch++;
}
}
if (this.config.showOnlyOctavesUsed) {
currentPitch =
(this.firstDrawnOctave * this.NOTES_PER_OCTAVE) + this.LOW_C;
x = -this.config.whiteNoteWidth;
}
else {
this.drawWhiteKey(currentPitch, x);
currentPitch = this.LOW_C - 3;
this.drawBlackKey(currentPitch + 1, blackNoteOffset);
currentPitch += 3;
x = this.config.whiteNoteWidth;
}
for (let o = this.firstDrawnOctave; o <= this.lastDrawnOctave; o++) {
for (let i = 0; i < this.NOTES_PER_OCTAVE; i++) {
if (blackNoteIndexes.indexOf(i) !== -1) {
this.drawBlackKey(currentPitch, x + blackNoteOffset);
}
else {
x += this.config.whiteNoteWidth;
}
currentPitch++;
}
}
}
drawWhiteKey(index, x) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.dataset.pitch = String(index);
rect.setAttribute('x', String(x));
rect.setAttribute('y', '0');
rect.setAttribute('width', String(this.config.whiteNoteWidth));
rect.setAttribute('height', String(this.config.whiteNoteHeight));
rect.setAttribute('fill', 'white');
rect.setAttribute('original-fill', 'white');
rect.setAttribute('stroke', 'black');
rect.setAttribute('stroke-width', '3px');
rect.classList.add('white');
this.svgPiano.appendChild(rect);
return rect;
}
drawBlackKey(index, x) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.dataset.pitch = String(index);
rect.setAttribute('x', String(x));
rect.setAttribute('y', '0');
rect.setAttribute('width', String(this.config.blackNoteWidth));
rect.setAttribute('height', String(this.config.blackNoteHeight));
rect.setAttribute('fill', 'black');
rect.setAttribute('original-fill', 'black');
rect.setAttribute('stroke', 'black');
rect.setAttribute('stroke-width', '3px');
rect.classList.add('black');
this.svgPiano.appendChild(rect);
return rect;
}
clearActiveNotes() {
super.unfillActiveRect(this.svg);
const els = this.svgPiano.querySelectorAll('rect.active');
for (let i = 0; i < els.length; ++i) {
const el = els[i];
el.setAttribute('fill', el.getAttribute('original-fill'));
el.classList.remove('active');
}
}
}
export var ScrollType;
(function (ScrollType) {
ScrollType[ScrollType["PAGE"] = 0] = "PAGE";
ScrollType[ScrollType["NOTE"] = 1] = "NOTE";
ScrollType[ScrollType["BAR"] = 2] = "BAR";
})(ScrollType || (ScrollType = {}));
export class StaffSVGVisualizer extends BaseVisualizer {
constructor(sequence, div, config = {}) {
super(sequence, config);
if (config.pixelsPerTimeStep === undefined ||
config.pixelsPerTimeStep <= 0) {
this.config.pixelsPerTimeStep = 0;
}
this.instruments = config.instruments || [];
this.render = new sr.StaffSVGRender(this.getScoreInfo(sequence), {
noteHeight: this.config.noteHeight,
noteSpacing: this.config.noteSpacing,
pixelsPerTimeStep: this.config.pixelsPerTimeStep,
noteRGB: this.config.noteRGB,
activeNoteRGB: this.config.activeNoteRGB,
defaultKey: config.defaultKey || 0,
scrollType: config.scrollType || ScrollType.PAGE,
}, div);
this.drawnNotes = sequence.notes.length;
this.clear();
this.redraw();
}
clear() {
this.render.clear();
}
redraw(activeNote, scrollIntoView) {
if (this.drawnNotes !== this.noteSequence.notes.length) {
this.render.scoreInfo = this.getScoreInfo(this.noteSequence);
}
const activeNoteInfo = activeNote ? this.getNoteInfo(activeNote) : undefined;
return this.render.redraw(activeNoteInfo, scrollIntoView);
}
isNoteInInstruments(note) {
if (note.instrument === undefined || this.instruments.length === 0) {
return true;
}
else {
return this.instruments.indexOf(note.instrument) >= 0;
}
}
timeToQuarters(time) {
const q = time * this.noteSequence.tempos[0].qpm / 60;
return Math.round(q * 16) / 16;
}
getNoteInfo(note) {
const startQ = this.timeToQuarters(note.startTime);
const endQ = this.timeToQuarters(note.endTime);
return {
start: startQ,
length: endQ - startQ,
pitch: note.pitch,
intensity: note.velocity
};
}
getScoreInfo(sequence) {
const notesInfo = [];
sequence.notes.forEach((note) => {
if (this.isNoteInInstruments(note)) {
notesInfo.push(this.getNoteInfo(note));
}
});
return {
notes: notesInfo,
tempos: sequence.tempos ?
sequence.tempos.map((t) => {
return { start: this.timeToQuarters(t.time), qpm: t.qpm };
}) :
[],
keySignatures: sequence.keySignatures ?
sequence.keySignatures.map((ks) => {
return { start: this.timeToQuarters(ks.time), key: ks.key };
}) :
[],
timeSignatures: sequence.timeSignatures ?
sequence.timeSignatures.map((ts) => {
return {
start: this.timeToQuarters(ts.time),
numerator: ts.numerator,
denominator: ts.denominator
};
}) :
[]
};
}
clearActiveNotes() {
this.redraw();
}
}
//# sourceMappingURL=visualizer.js.map