🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

midi-segmentizer

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

midi-segmentizer - npm Package Compare versions

Comparing version
2.1.0
to
2.1.1-1
+10
dist/midi.d.ts
import { Midi as ToneJsMidi } from "@tonejs/midi";
import { Note } from ".";
declare class Midi extends ToneJsMidi {
readonly simpleBpm: number;
private constructor();
static fromBuffer(buffer: ArrayBuffer): Midi | null;
static fromBase64(str: string): Midi | null;
getAllNotes(): Note[];
}
export default Midi;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const midi_1 = require("@tonejs/midi");
class Midi extends midi_1.Midi {
constructor(buffer, bpm) {
super(buffer);
this.simpleBpm = bpm;
}
static fromBuffer(buffer) {
try {
const midi = new midi_1.Midi(buffer);
const bpm = midi.header.tempos[0].bpm;
return new Midi(buffer, bpm);
}
catch (e) {
console.info("Buffer isn't valid Midi or SimpleMidi");
console.info(e);
return null;
}
}
static fromBase64(str) {
return Midi.fromBuffer(Buffer.from(str, "base64"));
}
getAllNotes() {
const res = [];
for (const track of this.tracks) {
for (const note of track.notes) {
res.push({
time: note.time,
midi: note.midi,
duration: note.duration,
velocity: note.velocity,
});
}
}
return res;
}
}
exports.default = Midi;
import { Midi as ToneJsMidi } from "@tonejs/midi";
import { Note } from ".";
class Midi extends ToneJsMidi {
readonly simpleBpm: number;
private constructor(buffer: ArrayBuffer, bpm: number) {
super(buffer);
this.simpleBpm = bpm;
}
public static fromBuffer(buffer: ArrayBuffer): Midi | null {
try {
const midi = new ToneJsMidi(buffer);
const bpm = midi.header.tempos[0].bpm;
return new Midi(buffer, bpm);
} catch (e) {
console.info("Buffer isn't valid Midi or SimpleMidi");
console.info(e);
return null;
}
}
public static fromBase64(str: string) {
return Midi.fromBuffer(Buffer.from(str, "base64"));
}
public getAllNotes(): Note[] {
const res = [] as Note[];
for (const track of this.tracks) {
for (const note of track.notes) {
res.push({
time: note.time,
midi: note.midi,
duration: note.duration,
velocity: note.velocity,
});
}
}
return res;
}
}
export default Midi;
+1
-0

@@ -14,1 +14,2 @@ import { Midi, Track } from "@tonejs/midi";

export declare const processMidiFile: (midi: SimpleMidi) => Segment[];
export declare function getSegmentOffset(firstNoteStart: number, beatLength: number, shift: number): number;
+12
-5
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processMidiFile = exports.base64ToBinary = exports.getDurationOfNotes = exports.getBeatLength = exports.sliceAndDice = exports.getPartitioningArrayWithMax = exports.getSimplePartitioningArray = exports.shiftMidi = exports.getValidTracks = exports.MAX_BREATH_SECONDS = void 0;
const notes_processor_1 = require("./notes-processor");
exports.getSegmentOffset = exports.processMidiFile = exports.base64ToBinary = exports.getDurationOfNotes = exports.getBeatLength = exports.sliceAndDice = exports.getPartitioningArrayWithMax = exports.getSimplePartitioningArray = exports.shiftMidi = exports.getValidTracks = exports.MAX_BREATH_SECONDS = void 0;
const notes_processor_1 = __importDefault(require("./notes-processor"));
exports.MAX_BREATH_SECONDS = 7;

@@ -71,5 +74,3 @@ const getValidTracks = (midi) => {

finalDivisions.forEach((notes) => {
const firstNoteStartTime = notes[0].time;
const minimumOffset = firstNoteStartTime - (firstNoteStartTime % beatLength);
const offset = minimumOffset - shift;
const offset = getSegmentOffset(notes[0].time, beatLength, shift);
let lowestNote = Infinity;

@@ -99,1 +100,7 @@ let highestNote = -Infinity;

exports.processMidiFile = processMidiFile;
function getSegmentOffset(firstNoteStart, beatLength, shift) {
const minimumOffset = firstNoteStart - (firstNoteStart % beatLength);
const offset = minimumOffset - shift;
return offset;
}
exports.getSegmentOffset = getSegmentOffset;
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.segmentizeMidi = void 0;
const helpers_1 = require("./helpers");
const simple_midi_1 = require("./simple-midi");
const simple_midi_1 = __importDefault(require("./simple-midi"));
function segmentizeMidi(rawBase64Midi) {

@@ -7,0 +10,0 @@ const simpleMidi = simple_midi_1.default.fromBase64(rawBase64Midi);

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const midi_1 = require("@tonejs/midi");
const helpers_1 = require("./helpers");
const notes_processor_1 = require("./notes-processor");
const notes_processor_1 = __importDefault(require("./notes-processor"));
function midiHasMultipleTempos(midi) {

@@ -7,0 +10,0 @@ return midi.header.tempos.length !== 1;

{
"name": "midi-segmentizer",
"version": "2.1.0",
"version": "2.1.1-1",
"description": "break a simple midi file into monophonic segments",

@@ -48,3 +48,4 @@ "main": "dist/index.js",

"ts-node": "^10.5.0",
"typescript": "^4.5.5"
"typescript": "^4.5.5",
"uuid": "^8.3.2"
},

@@ -51,0 +52,0 @@ "dependencies": {

@@ -1,11 +0,79 @@

import * as React from "react";
import React, { useEffect, useRef, useState } from "react";
import * as ReactDOM from "react-dom";
import { v4 as uuidv4 } from "uuid";
import { Note, Segment, segmentizeMidi } from ".";
import { Note as RawNote } from ".";
import { sample } from "./sample";
import Midi from "./midi";
import { getBeatLength, getSegmentOffset, shiftMidi } from "./helpers";
const X_EXPAND = 100;
type Coord = { x: number; y: number };
interface Note extends RawNote, Coord {
width: number;
height: number;
topLeft: Coord;
bottomRight: Coord;
selected: boolean;
offset: number;
}
function buildNote(
note: RawNote & { selected?: boolean; offset?: number }
): Note {
// NOTE: offset is a unit of time
const height = 1000 / 127;
const noteStart = note.time;
const width = note.duration * X_EXPAND;
const offset = note.offset ?? 0;
const x = noteStart * X_EXPAND + offset * X_EXPAND;
const y = (127 - note.midi) * height;
const topLeft = { x, y };
const bottomRight = { x: x + width, y: y + height };
return {
...note,
x,
y,
width,
height,
topLeft,
bottomRight,
offset,
selected: note.selected ?? false,
};
}
interface Segment {
id: string;
notes: Note[];
color: string;
offset: number;
}
function getBars(beatLength: number, width: number): number[] {
const res = [];
let i = 0;
while (i <= width) {
res.push(i);
i += beatLength;
}
return res;
}
function renderBars(ctx: Canvas, bars: number[], height: number) {
for (const bar of bars) {
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
const x = bar * X_EXPAND;
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
}
type Canvas = CanvasRenderingContext2D;
const X_EXPAND = 100; // NOTE: ideally this is based on width
function getRandomColor() {

@@ -16,27 +84,12 @@ const rand = () => Math.floor(Math.random() * 255);

function renderNote(
ctx: Canvas,
note: Note,
offset: number,
w: number,
h: number,
col: string
) {
const noteHeight = h / 127;
const noteStart = offset + note.time;
const length = note.duration * X_EXPAND;
const x = noteStart * X_EXPAND;
const y = (127 - note.midi) * noteHeight;
function renderNote(ctx: Canvas, note: Note, color?: string) {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = col;
ctx.fillStyle = col;
ctx.rect(x, y, length, noteHeight);
ctx.strokeStyle = color ?? "#000000";
ctx.rect(note.x, note.y, note.width, note.height);
ctx.stroke();
ctx.fill();
return {
topLeft: { x, y },
bottomRight: { x: x + length, y: y + noteHeight },
};
if (note.selected) {
ctx.fillStyle = color ?? "#000000";
ctx.fill();
}
}

@@ -59,29 +112,54 @@

function renderSegment(ctx: Canvas, segment: Segment, w: number, h: number) {
const { notes, offset } = segment;
const col = getRandomColor();
function renderSegments(ctx: Canvas, segments: Segment[]) {
for (const segment of segments) {
renderSegment(ctx, segment);
}
}
function renderSegment(ctx: Canvas, segment: Segment) {
let hi = Infinity;
let lo = -Infinity;
let left = Infinity;
let right = -Infinity;
for (const note of segment.notes) {
renderNote(ctx, note, segment.color);
hi = Math.min(hi, note.topLeft.y);
lo = Math.max(lo, note.bottomRight.y);
right = Math.max(right, note.bottomRight.x);
}
const left = segment.offset * X_EXPAND;
renderBox(ctx, hi, lo, left, right, segment.color);
}
function renderNotes(ctx: Canvas, notes: Note[]) {
for (const note of notes) {
const { topLeft, bottomRight } = renderNote(ctx, note, offset, w, h, col);
hi = Math.min(hi, topLeft.y);
lo = Math.max(lo, bottomRight.y);
left = Math.min(left, topLeft.x);
right = Math.max(right, bottomRight.x);
renderNote(ctx, note);
}
renderBox(ctx, hi, lo, offset * X_EXPAND, right, col);
}
function renderSegments(
ctx: Canvas,
segments: Segment[],
w: number,
h: number
) {
for (const segment of segments) {
renderSegment(ctx, segment, w, h);
function useKeyPress(targetKey: string): boolean {
// State for keeping track of whether key is pressed
const [keyPressed, setKeyPressed] = useState(false);
// If pressed key is our target key then set to true
function downHandler({ key }: any): void {
if (key === targetKey) {
setKeyPressed(true);
}
}
// If released key is our target key then set to false
const upHandler = ({ key }: any): void => {
if (key === targetKey) {
setKeyPressed(false);
}
};
// Add event listeners
useEffect(() => {
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, []); // Empty array ensures that effect is only run on mount and unmount
return keyPressed;
}

@@ -95,15 +173,40 @@

function App() {
const ref = React.useRef(null);
const [data, setData] = React.useState<string>(sample);
const [height, setHeight] = React.useState(500);
const [width, setWidth] = React.useState(2000);
const ref = useRef(null);
const [data, setData] = useState<string>(sample);
const [height, setHeight] = useState(800);
const [width, setWidth] = useState(2000);
const [notes, setNotes] = useState<Note[]>([]);
const [bars, setBars] = useState<number[]>([]);
const [segments, setSegments] = useState<Segment[]>([]);
const [mouseDown, setMouseDown] = useState<Coord | null>(null);
const [beatLength, setBeatLength] = useState<number | null>(null);
const [shift, setShift] = useState<number | null>(null);
const escapePressed = useKeyPress("Escape");
React.useEffect(() => {
useEffect(() => {
if (escapePressed) {
handleClearSelection();
}
}, [escapePressed]);
useEffect(() => {
if (data) {
const segments = segmentizeMidi(data);
if (ref.current && segments) {
const current = ref.current as any;
const ctx: CanvasRenderingContext2D = current.getContext("2d");
clear(ctx, width, height);
renderSegments(ctx, segments, width, height);
const midi = Midi.fromBase64(data);
if (midi) {
const _beatLength = getBeatLength(midi.simpleBpm);
const _shift = _beatLength * 2;
shiftMidi(midi, _shift);
const notes = midi
.getAllNotes()
.map(buildNote)
.sort((a, b) => (b.time > a.time ? -1 : 1));
const maxWidth = Math.max(...notes.map((n) => n.x)) + 300;
setBars(getBars(_beatLength, maxWidth));
setWidth(maxWidth);
setBeatLength(_beatLength);
setShift(_shift);
setTimeout(() => {
// dumb hack to get rid of weird canvas dimension change display issue
setNotes(notes);
}, 100);
}

@@ -113,2 +216,87 @@ }

function handleMouseDown(evt: any) {
if (ref.current) {
const rect = (ref.current as any).getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
setMouseDown({ x, y });
}
}
function handleClearSelection() {
setNotes(
notes.map((note) => ({
...note,
selected: false,
}))
);
}
function handleSaveSegment() {
const remaining: Note[] = [];
const segmentNotes: Note[] = [];
notes.forEach((note) => {
if (note.selected) {
segmentNotes.push(note);
} else {
remaining.push(note);
}
});
setNotes(remaining);
if (segmentNotes.length && beatLength && shift) {
// const id =
const color = getRandomColor();
const offset = getSegmentOffset(segmentNotes[0].time, beatLength, shift);
const shiftedNotes = segmentNotes.map((n) =>
// TODO: it's not the most intuitive thing to pass down offset at note level
buildNote({ ...n, time: n.time - offset, offset })
);
setSegments([
...segments,
{ id: uuidv4(), color, notes: shiftedNotes, offset },
]);
}
}
function handleMouseUp(evt: any) {
if (ref.current && mouseDown) {
const rect = (ref.current as any).getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
const left = Math.min(x, mouseDown.x);
const right = Math.max(x, mouseDown.x);
const top = Math.min(y, mouseDown.y);
const bottom = Math.max(y, mouseDown.y);
setMouseDown(null);
setNotes(
notes.map((note) => {
const { x: noteX, y: noteY, width, height } = note;
let clicked = false;
if (
noteX + width >= left &&
noteX <= right &&
noteY + height >= top &&
noteY <= bottom
) {
clicked = true;
}
return {
...note,
selected: clicked ? !note.selected : note.selected,
};
})
);
}
}
if (ref.current && notes) {
const current = ref.current as any;
const ctx: CanvasRenderingContext2D = current.getContext("2d");
clear(ctx, width, height);
renderNotes(ctx, notes);
renderBars(ctx, bars, height);
renderSegments(ctx, segments);
}
async function handleFileUpload(file: File) {

@@ -120,10 +308,62 @@ const buffer = await file.arrayBuffer();

function deleteSegment(segment: Segment) {
if (segments.find((s) => s.id === segment.id)) {
setSegments(segments.filter((s) => s.id !== segment.id));
setNotes([
...notes,
...segment.notes.map((note) => ({
...note,
time: note.offset + note.time,
selected: false,
offset: 0,
})),
]);
}
}
function renderButtons() {
return (
<div style={{ position: "fixed", maxWidth: "800px" }}>
{segments.map((segment) => (
<button
key={segment.id}
onClick={() => deleteSegment(segment)}
style={{
backgroundColor: segment.color,
color: "#FFFFFF",
}}
>
delete
</button>
))}
</div>
);
}
function handleDownload() {
var data = { a: 1, b: 2, c: 3 };
var json = JSON.stringify(data);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.download = "backup.json";
a.href = url;
a.textContent = "Download backup.json";
}
return (
<>
<input
id="fileupload"
type="file"
onChange={(e) => handleFileUpload(e.target.files![0])}
/>
<div>
<div style={{ position: "fixed" }}>
<input
id="fileupload"
type="file"
onChange={(e) => handleFileUpload(e.target.files![0])}
/>
<button onClick={handleSaveSegment}>save segment</button>
<button onClick={handleDownload}>download</button>
</div>
<canvas
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
id="react-midiVisualizer-canvas"

@@ -134,3 +374,4 @@ ref={ref}

/>
</>
{renderButtons()}
</div>
);

@@ -137,0 +378,0 @@ }

@@ -94,6 +94,3 @@ import { Midi, Track } from "@tonejs/midi";

finalDivisions.forEach((notes: LibNote[]) => {
const firstNoteStartTime = notes[0].time;
const minimumOffset =
firstNoteStartTime - (firstNoteStartTime % beatLength);
const offset = minimumOffset - shift;
const offset = getSegmentOffset(notes[0].time, beatLength, shift);

@@ -125,1 +122,11 @@ let lowestNote = Infinity;

};
export function getSegmentOffset(
firstNoteStart: number,
beatLength: number,
shift: number
) {
const minimumOffset = firstNoteStart - (firstNoteStart % beatLength);
const offset = minimumOffset - shift;
return offset;
}

@@ -13,2 +13,3 @@ {

"outDir": "dist",
"esModuleInterop": true,
"jsx": "react"

@@ -15,0 +16,0 @@ },