Add memoization to expensive pure functions because the library involves a lot of computation under the hood.master
@@ -20,7 +20,9 @@ | |||
"build-storybook": "build-storybook" | |||
}, | |||
"dependencies": { | |||
"styled-components": "^5.1.1" | |||
"mem": "^6.1.0", | |||
"styled-components": "^5.1.1", | |||
"typescript-memoize": "^1.0.0-alpha.3" | |||
}, | |||
"peerDependencies": { | |||
"react": ">=16" | |||
@@ -1,9 +1,10 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import isNaturalKey from '../../services/isNaturalKey' | |||
import getKeyWidth from '../../services/getKeyWidth' | |||
import getKeyLeft from '../../services/getKeyLeft' | |||
import mem from 'mem' | |||
import isNaturalKeyUnmemoized from '../../services/isNaturalKey' | |||
import getKeyWidthUnmemoized from '../../services/getKeyWidth' | |||
import getKeyLeftUnmemoized from '../../services/getKeyLeft' | |||
import generateKeys from '../../services/generateKeys' | |||
import * as DefaultAccidentalKey from '../AccidentalKey/AccidentalKey' | |||
import * as DefaultNaturalKey from '../NaturalKey/NaturalKey' | |||
@@ -19,6 +20,10 @@ const Key = styled('div')({ | |||
top: 0, | |||
}) | |||
const getKeyWidth = mem(getKeyWidthUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const getKeyLeft = mem(getKeyLeftUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const isNaturalKey = mem(isNaturalKeyUnmemoized) | |||
export const propTypes = { | |||
/** | |||
* MIDI note of the first key. | |||
@@ -20,7 +20,7 @@ | |||
*/ | |||
// export default [ | |||
// export const KEY_OFFSETS = [ | |||
// 0, // C | |||
// 3 / 7 / 5, // C# | |||
// 1 / 7, // D | |||
@@ -36,7 +36,7 @@ | |||
// ] | |||
// http://datagenetics.com/blog/may32016/index.html | |||
export default [ | |||
export const KEY_OFFSETS = [ | |||
0, // C | |||
525 / 5880, // C# | |||
1 / 7, // D | |||
@@ -50,3 +50,5 @@ export default [ | |||
(525 + 490 * 7 + 525 + 455) / 5880, // A# | |||
6 / 7, // B | |||
] | |||
export const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 |
@@ -3,8 +3,8 @@ interface GenerateKeys { | |||
} | |||
const generateKeys: GenerateKeys = (startKey, endKey) => | |||
Array(endKey! - startKey! + 1) | |||
Array(endKey - startKey + 1) | |||
.fill(0) | |||
.map((_, i) => startKey! + i) | |||
.map((_, i) => startKey + i) | |||
export default generateKeys |
@@ -0,0 +1,28 @@ | |||
import mem from 'mem' | |||
import generateKeys from './generateKeys' | |||
import groupKeysIntoOctaves from './groupKeysIntoOctaves' | |||
import getOctaveCompletenessUnmemoized from './getOctaveCompleteness' | |||
const getOctaveCompleteness = mem(getOctaveCompletenessUnmemoized) | |||
interface GetFractionalOctaveCount { | |||
(startKey: number, endKey: number): number | |||
} | |||
const getFractionalOctaveCount: GetFractionalOctaveCount = (startKey, endKey) => { | |||
const dummyKeys = generateKeys(startKey, endKey) | |||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | |||
const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) | |||
.reduce<Record<number, number>>( | |||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | |||
...theOctaveCompleteness, | |||
[octave]: getOctaveCompleteness(firstKey, lastKey), | |||
}), | |||
{}, | |||
) | |||
return Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) | |||
} | |||
export default getFractionalOctaveCount |
@@ -1,8 +1,13 @@ | |||
import getKeyXOffset from './getKeyXOffset' | |||
import getOctaveCount from './getOctaveCount' | |||
import generateKeys from './generateKeys' | |||
import groupKeysIntoOctaves from './groupKeysIntoOctaves' | |||
import getOctaveCompleteness from './getOctaveCompleteness' | |||
import mem from 'mem' | |||
import getKeyXOffsetUnmemoized from './getKeyXOffset' | |||
import getOctaveCountUnmemoized from './getOctaveCount' | |||
import getFractionalOctaveCountUnmemoized from './getFractionalOctaveCount' | |||
import getKeyOctaveUnmemoized from './getKeyOctave' | |||
const getKeyXOffset = mem(getKeyXOffsetUnmemoized) | |||
const getOctaveCount = mem(getOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const getFractionalOctaveCount = mem(getFractionalOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const getKeyOctave = mem(getKeyOctaveUnmemoized) | |||
interface GetKeyLeft { | |||
(k: number): number | |||
@@ -13,23 +18,10 @@ interface GetKeyLeftDecorator { | |||
} | |||
const getKeyLeftDecorator: GetKeyLeftDecorator = (startKey, endKey): GetKeyLeft => (k) => { | |||
const dummyKeys = generateKeys(startKey, endKey) | |||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | |||
const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) | |||
.reduce<Record<number, number>>( | |||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | |||
...theOctaveCompleteness, | |||
[octave]: getOctaveCompleteness(firstKey, lastKey), | |||
}), | |||
{}, | |||
) | |||
const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) | |||
const fractionalOctaveCount = getFractionalOctaveCount(startKey, endKey) | |||
const octaveCount = getOctaveCount(startKey, endKey) | |||
const startOctave = Math.floor(startKey! / 12) | |||
const octave = Math.floor(k / 12) | |||
const startOctave = getKeyOctave(startKey) | |||
const octave = getKeyOctave(k) | |||
const octaveOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * (octave - startOctave) | |||
const theKeyOffset = octaveOffset + ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(k) | |||
const firstKeyOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(startKey + 12) | |||
@@ -0,0 +1,7 @@ | |||
interface GetKeyOctave { | |||
(k: number): number | |||
} | |||
const getKeyOctave: GetKeyOctave = (k) => Math.floor(k / 12) | |||
export default getKeyOctave |
@@ -1,8 +1,8 @@ | |||
import isNaturalKey from './isNaturalKey' | |||
import groupKeysIntoOctaves from './groupKeysIntoOctaves' | |||
import getOctaveCompleteness from './getOctaveCompleteness' | |||
import getOctaveCount from './getOctaveCount' | |||
import generateKeys from './generateKeys' | |||
import mem from 'mem' | |||
import isNaturalKeyUnmemoized from './isNaturalKey' | |||
import getOctaveCountUnmemoized from './getOctaveCount' | |||
import getFractionalOctaveCountUnmemoized from './getFractionalOctaveCount' | |||
import { ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO } from './constants' | |||
interface GetKeyWidth { | |||
(k: number): number | |||
@@ -12,25 +12,19 @@ interface GetKeyWidthDecorator { | |||
(startKey: number, endKey: number): GetKeyWidth | |||
} | |||
const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 | |||
const isNaturalKey = mem(isNaturalKeyUnmemoized) | |||
const getFractionalOctaveCount = mem(getFractionalOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const getOctaveCount = mem(getOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) | |||
const getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey): GetKeyWidth => (k) => { | |||
const dummyKeys = generateKeys(startKey, endKey) | |||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | |||
const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) | |||
.reduce<Record<number, number>>( | |||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | |||
...theOctaveCompleteness, | |||
[octave]: getOctaveCompleteness(firstKey, lastKey), | |||
}), | |||
{}, | |||
) | |||
const getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey) => { | |||
const getKeyWidth: GetKeyWidth = (k) => { | |||
const fractionalOctaveCount = getFractionalOctaveCount(startKey, endKey) | |||
const octaveCount = getOctaveCount(startKey, endKey) | |||
const naturalKeyWidth = (100 * (octaveCount / fractionalOctaveCount)) / (octaveCount * 7) | |||
return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO | |||
} | |||
const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) | |||
const octaveCount = getOctaveCount(startKey, endKey) | |||
const naturalKeyWidth = (100 * (octaveCount / fractionalOctaveCount)) / (octaveCount * 7) | |||
return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO | |||
return mem(getKeyWidth) | |||
} | |||
export default getKeyWidthDecorator |
@@ -1,5 +1,11 @@ | |||
import KEY_OFFSETS from './constants/keyOffsets' | |||
import { KEY_OFFSETS } from './constants' | |||
export default (k: number): number => { | |||
interface GetKeyXOffset { | |||
(k: number): number | |||
} | |||
const getKeyXOffset: GetKeyXOffset = (k) => { | |||
return KEY_OFFSETS[k % 12] | |||
} | |||
export default getKeyXOffset |
@@ -1,10 +1,20 @@ | |||
import getKeyXOffset from './getKeyXOffset' | |||
import isNaturalKey from './isNaturalKey' | |||
import mem from 'mem' | |||
import getKeyXOffsetUnmemoized from './getKeyXOffset' | |||
import isNaturalKeyUnmemoized from './isNaturalKey' | |||
const getKeyXOffset = mem(getKeyXOffsetUnmemoized) | |||
const isNaturalKey = mem(isNaturalKeyUnmemoized) | |||
interface GetOctaveCompleteness { | |||
(firstKey: number, lastKey: number): number | |||
} | |||
// expect firstKey and lastKey within the same octave | |||
export default (firstKey: number, lastKey: number) => | |||
const getOctaveCompleteness: GetOctaveCompleteness = (firstKey, lastKey) => | |||
// see if there are missing higher notes | |||
getKeyXOffset(lastKey) + | |||
(isNaturalKey(lastKey) ? 1 / 7 : ((1 / 7) * 18) / 36) - | |||
// see if there are missing lower notes | |||
getKeyXOffset(firstKey) | |||
export default getOctaveCompleteness |
@@ -1,11 +1,12 @@ | |||
import mem from 'mem' | |||
import getKeyOctaveUnmemoized from './getKeyOctave' | |||
const getKeyOctave = mem(getKeyOctaveUnmemoized) | |||
interface GetOctaveCount { | |||
(startKey: number, endKey: number): number | |||
} | |||
const getOctaveCount: GetOctaveCount = (startKey, endKey) => { | |||
const startOctave = Math.floor(startKey / 12) | |||
const endOctave = Math.floor(endKey / 12) | |||
return endOctave - startOctave + 1 | |||
} | |||
const getOctaveCount: GetOctaveCount = (startKey, endKey) => getKeyOctave(endKey) - getKeyOctave(startKey) + 1 | |||
export default getOctaveCount |
@@ -1,4 +1,8 @@ | |||
export default (dummyKeys: number[]): Record<number, number[]> => | |||
interface GroupKeysIntoOctaves { | |||
(dummyKeys: number[]): Record<number, number[]> | |||
} | |||
const groupKeysIntoOctaves: GroupKeysIntoOctaves = (dummyKeys) => | |||
dummyKeys | |||
.map((k) => [k, Math.floor(k / 12)]) | |||
.reduce<Record<number, number[]>>( | |||
@@ -8,3 +12,5 @@ export default (dummyKeys: number[]): Record<number, number[]> => | |||
}), | |||
{}, | |||
) | |||
export default groupKeysIntoOctaves |
@@ -1,6 +1,10 @@ | |||
const NATURAL_KEYS = [0, 2, 4, 5, 7, 9, 11] | |||
export default (k: number): boolean => { | |||
interface IsNaturalKey { | |||
(k: number): boolean | |||
} | |||
const isNaturalKey: IsNaturalKey = (k: number): boolean => { | |||
const type = typeof (k as unknown) | |||
if ((type as string) !== 'number') { | |||
throw TypeError(`Invalid value type passed to isNaturalKey, expected 'number', got ${type}.`) | |||
@@ -13,3 +17,5 @@ export default (k: number): boolean => { | |||
} | |||
return NATURAL_KEYS.includes(Math.floor(k) % 12) | |||
} | |||
export default isNaturalKey |
@@ -4249,6 +4249,11 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: | |||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" | |||
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== | |||
core-js@2.4.1: | |||
version "2.4.1" | |||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" | |||
integrity sha1-TekR5mew6ukSTjQlS1OupvxhjT4= | |||
core-js@^1.0.0: | |||
version "1.2.7" | |||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" | |||
@@ -8027,6 +8032,13 @@ makeerror@1.0.x: | |||
dependencies: | |||
tmpl "1.0.x" | |||
map-age-cleaner@^0.1.3: | |||
version "0.1.3" | |||
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" | |||
integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== | |||
dependencies: | |||
p-defer "^1.0.0" | |||
map-cache@^0.2.2: | |||
version "0.2.2" | |||
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" | |||
@@ -8143,6 +8155,14 @@ media-typer@0.3.0: | |||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" | |||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= | |||
mem@^6.1.0: | |||
version "6.1.0" | |||
resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.0.tgz#846eca0bd4708a8f04b9c3f3cd769e194ae63c5c" | |||
integrity sha512-RlbnLQgRHk5lwqTtpEkBTQ2ll/CG/iB+J4Hy2Wh97PjgZgXgWJWrFF+XXujh3UUVLvR4OOTgZzcWMMwnehlEUg== | |||
dependencies: | |||
map-age-cleaner "^0.1.3" | |||
mimic-fn "^3.0.0" | |||
memoizerific@^1.11.3: | |||
version "1.11.3" | |||
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" | |||
@@ -8267,6 +8287,11 @@ mimic-fn@^2.1.0: | |||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" | |||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== | |||
mimic-fn@^3.0.0: | |||
version "3.1.0" | |||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" | |||
integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== | |||
min-document@^2.19.0: | |||
version "2.19.0" | |||
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" | |||
@@ -8832,6 +8857,11 @@ os-tmpdir@~1.0.2: | |||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" | |||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= | |||
p-defer@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" | |||
integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= | |||
p-each-series@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" | |||
@@ -11740,6 +11770,13 @@ typedarray@^0.0.6: | |||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | |||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= | |||
typescript-memoize@^1.0.0-alpha.3: | |||
version "1.0.0-alpha.3" | |||
resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.0.0-alpha.3.tgz#699a5415f886694a8d6e2e5451bc28a39a6bc2f9" | |||
integrity sha1-aZpUFfiGaUqNbi5UUbwoo5prwvk= | |||
dependencies: | |||
core-js "2.4.1" | |||
typescript@^3.7.3, typescript@^3.9.7: | |||
version "3.9.7" | |||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" | |||