Browse Source

Improve performance

Add memoization to expensive pure functions because the library involves a lot of computation under the hood.
master
TheoryOfNekomata 4 years ago
parent
commit
2daa01d6f6
14 changed files with 159 additions and 63 deletions
  1. +3
    -1
      package.json
  2. +8
    -3
      src/components/Keyboard/Keyboard.tsx
  3. +4
    -2
      src/services/constants.ts
  4. +2
    -2
      src/services/generateKeys.ts
  5. +28
    -0
      src/services/getFractionalOctaveCount.ts
  6. +13
    -21
      src/services/getKeyLeft.ts
  7. +7
    -0
      src/services/getKeyOctave.ts
  8. +16
    -22
      src/services/getKeyWidth.ts
  9. +8
    -2
      src/services/getKeyXOffset.ts
  10. +13
    -3
      src/services/getOctaveCompleteness.ts
  11. +6
    -5
      src/services/getOctaveCount.ts
  12. +7
    -1
      src/services/groupKeysIntoOctaves.ts
  13. +7
    -1
      src/services/isNaturalKey.ts
  14. +37
    -0
      yarn.lock

+ 3
- 1
package.json View File

@@ -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"


+ 8
- 3
src/components/Keyboard/Keyboard.tsx View File

@@ -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.


src/services/constants/keyOffsets.ts → src/services/constants.ts View File

@@ -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

+ 2
- 2
src/services/generateKeys.ts View File

@@ -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

+ 28
- 0
src/services/getFractionalOctaveCount.ts View File

@@ -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

+ 13
- 21
src/services/getKeyLeft.ts View File

@@ -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)


+ 7
- 0
src/services/getKeyOctave.ts View File

@@ -0,0 +1,7 @@
interface GetKeyOctave {
(k: number): number
}

const getKeyOctave: GetKeyOctave = (k) => Math.floor(k / 12)

export default getKeyOctave

+ 16
- 22
src/services/getKeyWidth.ts View File

@@ -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

+ 8
- 2
src/services/getKeyXOffset.ts View File

@@ -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

+ 13
- 3
src/services/getOctaveCompleteness.ts View File

@@ -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

+ 6
- 5
src/services/getOctaveCount.ts View File

@@ -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

+ 7
- 1
src/services/groupKeysIntoOctaves.ts View File

@@ -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

+ 7
- 1
src/services/isNaturalKey.ts View File

@@ -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

+ 37
- 0
yarn.lock View File

@@ -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"


Loading…
Cancel
Save