more mugic parser tweaks

This commit is contained in:
Daniel 2020-05-01 01:08:29 -04:00
parent 5979ea429e
commit 1a88ead718
11 changed files with 1289 additions and 243 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

927
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
"build": "npm run check-types && webpack -p",
"check-types": "tsc",
"lint": "tsc --noEmit && eslint '*/**/*.{js,ts,tsx}'",
"lint:fix": "tsc --noEmit && eslint '*/**/*.{js,ts,tsx}' --quiet --fix"
"lint:fix": "tsc --noEmit && eslint '*/**/*.{js,ts,tsx}' --quiet --fix",
"test": "cross-env TS_NODE_COMPILER_OPTIONS='{ \"module\": \"commonjs\" }' mocha -r ts-node/register -r ignore-styles -r jsdom-global/register src/**/*.spec.ts"
},
"repository": {
"type": "git",
@ -56,23 +57,32 @@
"@babel/preset-typescript": "^7.9.0",
"@babel/register": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@types/react-router-dom": "^5.1.5",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3",
"eslint": "^6.8.0",
"eslint-plugin-flowtype": "^4.7.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^3.0.0",
"ignore-styles": "^5.0.1",
"jsdom": "^16.2.2",
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^7.1.2",
"node-sass": "^4.14.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.0",
"terser-webpack-plugin": "^2.3.6",
"ts-node": "^8.9.1",
"typescript": "^3.8.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",

View File

@ -28,7 +28,7 @@ class Attack extends React.Component {
}
}
}
if (this.props.ext == false) return (
<div className="card mugic">
<img className="thumb" style={{ float: 'left' }} src={API.base_image + (card.gsx$thumb||API.thumb_missing)} onClick={() => this.props.setImage(card.gsx$image)} />
@ -36,7 +36,7 @@ class Attack extends React.Component {
<Name name={card.gsx$name} /><br />
<Rarity set={card.gsx$set} rarity={card.gsx$rarity} /> <br />
<Tribe size="icon16" tribe={card.gsx$tribe} /> Mugic - {card.gsx$tribe}<br />
<span>{mugicCounters}</span><MugicPlay notes={card.gsx$notes}/><br />
<span>{mugicCounters}</span><MugicPlay notes={card.gsx$shownotes?.length > 0 ? card.gsx$shownotes : card.gsx$notes}/><br />
</div>
<br />
<div className="right" >

View File

@ -0,0 +1,48 @@
// function debounce(f, t) {
// return function (args) {
// let previousCall = this.lastCall;
// this.lastCall = Date.now();
// if (previousCall && ((this.lastCall - previousCall) <= t)) {
// clearTimeout(this.lastCallTimer);
// }
// this.lastCallTimer = setTimeout(() => f(args), t);
// }
// }
// function debounce2 (func, wait, immediate) {
// let timeout;
// return function() {
// const context = this, args = arguments;
// const later = function() {
// clearTimeout(timeout);
// func.apply(context, args);
// };
// clearTimeout(timeout);
// if (immediate) func.apply(context, args);
// else timeout = setTimeout(later, wait);
// };
// }
/* https://codeburst.io/throttling-and-debouncing-in-javascript-646d076d0a44 */
export function debounced(delay, fn) {
let timerId;
return function (...args) {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
timerId = null;
}, delay);
}
}
export function throttled(delay, fn) {
let lastCall = 0;
return function (...args) {
const now = (new Date).getTime();
if (now - lastCall < delay) return;
lastCall = now;
return fn(...args);
}
}

View File

@ -0,0 +1,24 @@
import 'mocha';
import { expect } from 'chai';
import { parseTune, output } from './mugicparser';
const cases = {
"Canon of Casuality": {
input: "2Eb 2F 2D 2G 2Bb 1A 3D",
output: ["2Eb4", "2F4", "2D4", "2G4", "2Bb5", "1A5", "3D5"]
},
"Fortissimo": {
input: "2G#4 1C#5 2E5 2C#5 2D#5 1G#4 4F#5",
output: ["2G#4", "1C#5", "2E5", "2C#5", "2D#5", "1G#4", "4F#5"]
}
}
Object.entries(cases).forEach(([key, value]) => {
describe(key, () => {
it('should return ' + value.output, () => {
const tune = output(parseTune(value.input));
expect(tune).to.deep.equal(value.output);
});
});
});

View File

@ -0,0 +1,249 @@
export class Note {
pitch: string;
octave: number;
time: number;
duration: number;
velocity: number;
constructor(duration: number, time: number, value: {pitch: string, octave: number}, velocity?: number) {
this.duration = duration;
this.time = time;
this.pitch = value.pitch;
this.octave = value.octave;
if (velocity) this.velocity = velocity;
}
}
export const output = (seq: Note[]) => {
return seq.map(n => n.duration + n.pitch + n.octave.toString())
}
// db notation uses duration (quarter notes) and pitch
// 2Eb => E flat for 2 quarter notes
export const parseTune = (input: string): Note[] => {
let seq: Note[] = [];
let time = 0;
console.log(input.split(" "));
input.split(" ").forEach((note) => {
const splitter = /(?:[1-8]{1})/;
const match = note.match(splitter);
if (match === null) throw new Error("invalid_input");
const dur = parseInt(match[0]);
const pitch = note.split(splitter)[1];
const full_note = /[1-8]{1}[A-Za-z#]{1,2}([1-8]{1})/;
if (full_note.test(note)) {
const sp = note.match(full_note);
if (sp === null) throw new Error("invalid_input");
seq.push(new Note(dur, time, { pitch, octave: parseInt(sp[1]) }));
}
else {
seq.push(new Note(dur, time, parseNote(pitch, seq)));
}
time += dur;
});
// If a note is repeated at the same octave, look at trend of last two notes
// for (let i = 2; i < seq.length; i++) {
// const note = seq[i];
// const comp = seq[i-2];
// if (note.pitch === comp.pitch && note.octave === comp.octave) {
// const pitch = letter_to_number(note.pitch);
// seq[i].octave = trend(pitch, i, seq);
// }
// }
console.log(output(seq));
return seq;
}
/*
We have an array of previous notes; for the first to cases the octave is middle (4).
Now for the third and further note, we'll need to look at the previous notes.
First check the "closeness" to the last note (if its within 2.5 notes).
Go with the appropriate octave (an A above a G4 would be an A5)
But if its not within that closeness, look at the trend.
*/
/**
* Tries to find the closer note to match of the previous notes octave
* @note The note's
*/
const parseNote = (pitch: string, seq: Note[]): {pitch: string, octave: number} => {
let octave: number = (() => {
// If its the first note its "middle octave"
if (seq.length === 0) return 4;
const l = seq.length - 1;
const octave = seq[l].octave;
const current = pitchValue(pitch, octave);
const previous = pitchValue(seq[l]);
const distance = compare(previous, current);
// If its less than 3 pitches of the previous note, use the closest pitch
if (distance < 3) {
if (distance === 0) return octave;
if (previous > pitchValue(5, octave)) {
if (current < pitchValue(3, octave)) {
return octave + 1;
}
else {
return octave;
}
}
else if (previous < pitchValue(3, octave)) {
if (current > pitchValue(5, octave)) {
return octave - 1;
}
else {
return octave;
}
}
return octave;
} else if (l === 0) {
if (distance === 3) {
if (current > previous) {
return octave;
}
else {
return octave + 1;
}
}
else if (current > previous) {
return octave;
}
else if (current < previous) {
return octave - 1;
}
}
// If its further away, look at the previous notes for a trend
return trend(current, l, seq);
})();
return {pitch: pitch, octave};
}
/*
* Is the last note a step down from the note before?
* If so consider that to be a higher weight.
* If the pattern is going up, go up, if going down go down with repeated notes
* Example, a note 4 away but on a downward trend would be prioritized over a 3 away in the other direction.
* If there is no change. Pick the octave the previous notes used.
*/
/**
* Searches for a trend in previous notes to calculate the current octave to be used
* For each iteration, the length is reduced so that an older set of notes is compared
* @param current The note being calculated
* @param l The index of the array to be compared
*/
const trend = (current: number, l: number, seq: Note[]): number => {
if (l < 1) return seq[l].octave;
let prev = pitchValue(seq[l]);
let prev2 = pitchValue(seq[l-1]);
console.log(prev2, prev, current);
// downward trend
if (prev2 > prev) {
if (prev < current) {
return seq[l].octave;
}
return seq[l].octave - 1;
}
// upward trend
else if (prev2 < prev) {
if (prev < current) {
return seq[l].octave;
}
return seq[l].octave + 1;
}
// same notes
else {
return trend(current, l-1, seq);
}
}
/**
* Takes two pitches and returns the distance between them
*/
const compare = (one: number, two: number): number => {
let res = Math.abs(one - two);
if (res < 4) {
return res;
}
else if (res > 3.5) {
return res - 1;
}
else if (res > 4.5) {
return res - 2;
}
else if (res > 5.5) {
return res - 3;
}
else if (res > 6.5) {
return res - 4;
}
return res;
}
function pitchValue(note: Note): number;
function pitchValue(letter: number, octave: number): number;
function pitchValue(pitch: string, octave: number): number;
function pitchValue(arg1: number | string | Note, arg2?: number): number {
let pitch: number;
let octave: number;
if (arg1 instanceof Note) {
pitch = letter_to_number(arg1.pitch);
octave = arg1.octave;
} else {
pitch = (typeof arg1 === 'number') ? arg1 : letter_to_number(arg1);
octave = arg2 as number;
}
return pitch + (octave - 1) * 8;
}
/**
* Converts a pitch to numerical value for calculations
*/
const letter_to_number = (pitch: string): number => {
let num: number;
switch (pitch.charAt(0).toUpperCase()) {
case "A":
num = 1;
break;
case "B":
num = 2;
break;
case "C":
num = 3;
break;
case "D":
num = 4;
break;
case "E":
num = 5;
break;
case "F":
num = 6;
break;
case "G":
num = 7;
break;
// In the case of incorrect input, coerce note to a C
default:
num = 3;
}
if (pitch.length > 1) {
if (pitch.charAt(1).toLowerCase() === "b") num -= .5;
else if (pitch.charAt(1) === "#") num += .5;
}
return num;
}

View File

@ -1,4 +1,7 @@
import {Transport, Synth, Part, Time, PolySynth, EnvelopeCurve} from 'tone';
import {Time, Transport, Synth, Part, PolySynth, EnvelopeCurve} from 'tone';
import { Note, parseTune } from './mugicparser';
type BasicEnvelopeCurve = "linear" | "exponential";
// https://github.com/Tonejs/Tone.js/wiki/Time
@ -9,19 +12,11 @@ interface note_value {
duration: number,
velocity?: number
}
export class Note {
pitch: string;
octave: number;
time: number;
duration: number;
velocity: number;
constructor(duration: number, time: number, value: {pitch: string, octave: number}, velocity?: number) {
this.duration = duration;
this.time = time;
this.pitch = value.pitch;
this.octave = value.octave;
if (velocity) this.velocity = velocity;
class Note_Value extends Note {
constructor(note: Note) {
const { duration, time, pitch, octave, velocity } = note;
super(duration, time, { pitch, octave }, velocity);
}
get value(): note_value {
@ -36,7 +31,7 @@ export class Note {
export class MugicPlayer {
private static instance: MugicPlayer;
private synth: PolySynth;
private synth: Synth;
private part: Part;
// Singleton
@ -60,10 +55,9 @@ export class MugicPlayer {
releaseCurve: "exponential" as EnvelopeCurve,
decayCurve: "exponential" as BasicEnvelopeCurve
},
pitchDecay: 0.05,
maxPolyphony: 1
pitchDecay: 0.05
};
this.synth = new PolySynth(Synth, options).toDestination();
this.synth = new Synth(options).toDestination();
Transport.bpm.value = 140;
}
@ -79,10 +73,9 @@ export class MugicPlayer {
if (this.part) this.part.dispose();
try {
const tune = parseTune(input);
console.log(tune.map(n => n.value.pitch));
const tune = parseTune(input).map(note => new Note_Value(note));
this.part = new Part(
(time, val: note_value) => {
(time, val) => {
this.synth.triggerAttackRelease(val.pitch, val.duration, time, val.velocity);
},
tune.map((n) => n.value)
@ -99,212 +92,3 @@ export class MugicPlayer {
}
}
// db notation uses duration (quarter notes) and pitch
// 2Eb => E flat for 2 quarter notes
const parseTune = (input: string): Note[] => {
let seq: Note[] = [];
let time = 0;
input.split(" ").forEach((note) => {
let match = note.match(/(?:[1-8]{1})/);
if (match === null) throw new Error("invalid_input");
let dur = parseInt(match[0]);
let pitch = note.split(/(?:[1-8]{1})/)[1];
seq.push(new Note(dur, time, parseNote(pitch, seq)));
time += dur;
});
// If a note is repeated at the same octave, look at trend of last two notes
// for (let i = 2; i < seq.length; i++) {
// const note = seq[i];
// const comp = seq[i-2];
// if (note.pitch === comp.pitch && note.octave === comp.octave) {
// const pitch = letter_to_number(note.pitch);
// seq[i].octave = trend(pitch, i, seq);
// }
// }
return seq;
}
/*
We have an array of previous notes; for the first to cases the octave is middle (4).
Now for the third and further note, we'll need to look at the previous notes.
First check the "closeness" to the last note (if its within 2.5 notes).
Go with the appropriate octave (an A above a G4 would be an A5)
But if its not within that closeness, look at the trend.
*/
/**
* Tries to find the closer note to match of the previous notes octave
* @note The note's
*/
const parseNote = (pitch: string, seq: Note[]): {pitch: string, octave: number} => {
let octave: number = (() => {
// If its the first note its "middle octave"
if (seq.length === 0) return 4;
const l = seq.length - 1;
const current = pitchValue(pitch, seq[l].octave);
const previous = pitchValue(seq[l]);
const distance = compare(previous, current);
// If its less than 3 pitches of the previous note, use the closest pitch
if (distance < 3) {
if (distance === 0) return seq[l].octave;
if (previous > 5) {
if (current < 3) {
return seq[l].octave + 1;
}
else {
return seq[l].octave;
}
}
else if (previous < 3) {
if (current > 5) {
return seq[l].octave - 1;
}
else {
return seq[l].octave;
}
}
return seq[l].octave;
} else if (l === 0) {
if (distance === 3) {
return seq[l].octave + 1;
}
else if (current > previous) {
return seq[l].octave;
}
else if (current < previous) return seq[l].octave - 1;
}
// If its further away, look at the previous notes for a trend
return trend(current, l, seq);
})();
return {pitch: pitch, octave};
}
/*
* Is the last note a step down from the note before?
* If so consider that to be a higher weight.
* If the pattern is going up, go up, if going down go down with repeated notes
* Example, a note 4 away but on a downward trend would be prioritized over a 3 away in the other direction.
* If there is no change. Pick the octave the previous notes used.
*/
/**
* Searches for a trend in previous notes to calculate the current octave to be used
* For each iteration, the length is reduced so that an older set of notes is compared
* @param current The note being calculated
* @param l The index of the array to be compared
*/
const trend = (current: number, l: number, seq: Note[]): number => {
if (l < 1) return seq[l].octave;
let prev = pitchValue(seq[l].pitch, seq[l].octave);
let prev2 = pitchValue(seq[l-1].pitch, seq[l-1].octave);
// downward trend
if (prev2 > prev) {
if (prev < current) {
return seq[l].octave;
}
return seq[l].octave - 1;
}
// upward trend
else if (prev2 < prev) {
if (prev < current) {
return seq[l].octave;
}
return seq[l].octave + 1;
}
// same notes
else {
return trend(current, l, seq);
}
}
/**
* Takes two pitches and returns the distance between them
*/
const compare = (one: number, two: number): number => {
let res = Math.abs(one - two);
if (res < 4) {
return res;
}
else if (res > 3.5) {
return res - 1;
}
else if (res > 4.5) {
return res - 2;
}
else if (res > 5.5) {
return res - 3;
}
else if (res > 6.5) {
return res - 4;
}
return res;
}
function pitchValue(note: Note): number;
function pitchValue(current: number, octave: number): number;
function pitchValue(pitch: string, octave: number): number;
function pitchValue(arg1: number | string | Note, octave?: number): number {
let pitch: number;
if (arg1 instanceof Note) {
pitch = letter_to_number(arg1.pitch);
octave = arg1.octave;
} else {
pitch = (typeof arg1 === 'number') ? arg1 : letter_to_number(arg1);
octave = octave as number;
}
return pitch + (octave - 1) * 8;
}
/**
* Converts a pitch to numerical value for calculations
*/
const letter_to_number = (pitch: string): number => {
let num: number;
switch (pitch.charAt(0).toUpperCase()) {
case "A":
num = 1;
break;
case "B":
num = 2;
break;
case "C":
num = 3;
break;
case "D":
num = 4;
break;
case "E":
num = 5;
break;
case "F":
num = 6;
break;
case "G":
num = 7;
break;
// In the case of incorrect input, coerce note to a C
default:
num = 3;
}
if (pitch.length > 1) {
if (pitch.charAt(1).toLowerCase() === "b") num -= .5;
else if (pitch.charAt(1) === "#") num += .5;
}
return num;
}
export default MugicPlayer.getInstance();

View File

@ -1,8 +1,11 @@
import React from 'react';
import MugicPlayer from './mugicplayer';
import {debounced} from '../debounce';
import {MugicPlayer} from './mugicplayer';
const player = MugicPlayer.getInstance();
export default (props: any) => (
<React.Fragment >
<input type="button" value="Play" onClick={() => {MugicPlayer.play(props.notes)}} />
</React.Fragment>
);
export default (props: any) => {
const play = debounced(200, () => { player.play(props.notes); });
return (
<input type="button" value="Play" onClick={() => { play() }} />
);
};

View File

@ -41,10 +41,11 @@
]
},
"include": [
"src"
"./src"
],
"exclude": [
"./build/*",
"./node_modules/*"
"./build",
"./node_modules/*",
"./source/**/*.spec.ts"
]
}