Skip to content

Commit

Permalink
Limit the number of shareable intervals
Browse files Browse the repository at this point in the history
ref #699
  • Loading branch information
frostburn committed May 19, 2024
1 parent ceb253e commit bbc3f72
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 7 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scale-workshop",
"version": "3.0.0-beta.30",
"version": "3.0.0-beta.31",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const MAX_INTERACTIVE_SUBGROUP_SIZE = 6
// Anything larger than this uses O(n²) methods (if available) instead of O(exp(n))
export const MAX_GEO_SUBGROUP_SIZE = 9

// === Sanity limits for scale sharing ===
export const MAX_NUMBER_OF_SHARED_INTERVALS = 255

export const SEMITONE_12TET = 2 ** (1 / 12)

export const TET12 = [...Array(12).keys()].map((i) => SEMITONE_12TET ** (i + 1))
Expand Down
4 changes: 4 additions & 0 deletions src/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class Scale {
return valueToCents(this.equaveRatio)
}

clone() {
return new Scale([...this.intervalRatios], this.baseFrequency, this.baseMidiNote, this.title)
}

/**
* Convert this {@link Scale} instance to JSON.
* @returns The serialized object with property `type` set to `'ScaleWorkshopScale'`.
Expand Down
21 changes: 20 additions & 1 deletion src/stores/__tests__/scale.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useScaleStore } from '../scale'
import { Scale } from '../../scale'
import { Interval } from 'sonic-weave'

const SERIALIZED = String.raw`{"scale":{"type":"ScaleWorkshopScale","intervalRatios":[1.148698354997035,1.3195079107728942,1.515716566510398,1.7411011265922482,2],"baseFrequency":261.6255653005987,"baseMidiNote":60,"title":""},"relativeIntervals":[{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[1,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":1,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[2,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":2,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[3,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":3,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[4,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":4,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[1,1],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":5,"d":5,"p":null,"q":null},"t":[]}],"latticeIntervals":null,"name":"","baseMidiNote":60,"userBaseFrequency":261.63,"autoFrequency":true,"autoColors":"silver","sourceText":"tet(5)","colors":["silver","silver","silver","silver","gray"],"labels":["1\\5","2\\5","3\\5","4\\5","5\\5"],"error":"","warning":"","isomorphicVertical":5,"isomorphicHorizontal":1,"keyboardMode":"isomorphic","equaveShift":0,"degreeShift":0,"pianoMode":"Asdf","accidentalColor":"black","lowAccidentalColor":"maroon","middleAccidentalColor":"navy","highAccidentalColor":"indigo"}`
const SERIALIZED = String.raw`{"scale":{"type":"ScaleWorkshopScale","intervalRatios":[1.148698354997035,1.3195079107728942,1.515716566510398,1.7411011265922482,2],"baseFrequency":261.6255653005987,"baseMidiNote":60,"title":""},"relativeIntervals":[{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[1,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":1,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[2,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":2,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[3,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":3,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[4,5],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":4,"d":5,"p":null,"q":null},"t":[]},{"type":"Interval","v":{"type":"TimeMonzo","t":{"n":0,"d":1},"p":[1,1],"r":{"n":1,"d":1}},"d":1,"s":0,"l":"","n":{"type":"n","n":5,"d":5,"p":null,"q":null},"t":[]}],"colors":["silver","silver","silver","silver","gray"],"labels":["1\\5","2\\5","3\\5","4\\5","5\\5"],"latticeIntervals":null,"name":"","baseMidiNote":60,"userBaseFrequency":261.63,"autoFrequency":true,"autoColors":"silver","sourceText":"tet(5)","error":"","warning":"","isomorphicVertical":5,"isomorphicHorizontal":1,"keyboardMode":"isomorphic","equaveShift":0,"degreeShift":0,"pianoMode":"Asdf","accidentalColor":"black","lowAccidentalColor":"maroon","middleAccidentalColor":"navy","highAccidentalColor":"indigo"}`

describe('Scale store', () => {
beforeEach(() => {
Expand Down Expand Up @@ -40,4 +40,23 @@ describe('Scale store', () => {
expect(centss[5]).toBeCloseTo(1200)
expect(scale.latticeIntervals).toHaveLength(5)
})

it('has a cap on the number of shareable intervals', () => {
const scale = useScaleStore()
scale.sourceText = '500::1000'
scale.computeScale()
const serialized = JSON.stringify(scale)
expect(serialized.length).toBeLessThan(80000)

const data = JSON.parse(serialized, (key: string, value: any) =>
Scale.reviver(key, Interval.reviver(key, value))
)
data.id = 'ABCabc123'
expect(data.latticeIntervals).toBeNull()
scale.fromJSON(data)
expect(scale.scale.size).toBe(255)
expect(scale.scale.equaveRatio).toBeCloseTo(2)
expect(scale.colorForIndex(scale.baseMidiNote)).toBe('gray')
expect(scale.colorForIndex(scale.baseMidiNote + 1)).toBe('silver')
})
})
45 changes: 42 additions & 3 deletions src/stores/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
APP_TITLE,
DEFAULT_NUMBER_OF_COMPONENTS,
INTERVALS_12TET,
MAX_NUMBER_OF_SHARED_INTERVALS,
MIDI_NOTE_COLORS,
MIDI_NOTE_NAMES,
NUMBER_OF_NOTES,
Expand Down Expand Up @@ -487,14 +488,52 @@ export const useScaleStore = defineStore('scale', () => {
* Convert live state to a format suitable for storing on the server.
*/
function toJSON() {
let slicedScale = scale.value
let slicedIntervals = relativeIntervals.value
let slicedColors = colors.value
let slicedLabels = labels.value
if (slicedScale.intervalRatios.length > MAX_NUMBER_OF_SHARED_INTERVALS) {
slicedScale = slicedScale.clone()
slicedIntervals = [...slicedIntervals]
slicedColors = [...slicedColors]
slicedLabels = [...slicedLabels]
const equave = slicedScale.intervalRatios.pop()!
const equaveInterval = slicedIntervals.pop()!
const equaveColor = slicedColors.pop()!
const equaveLabel = slicedLabels.pop()!
slicedScale.intervalRatios = slicedScale.intervalRatios.slice(
0,
MAX_NUMBER_OF_SHARED_INTERVALS - 1
)
slicedScale.intervalRatios.push(equave)
slicedIntervals = slicedIntervals.slice(0, MAX_NUMBER_OF_SHARED_INTERVALS - 1)
slicedIntervals.push(equaveInterval)
slicedColors = slicedColors.slice(0, MAX_NUMBER_OF_SHARED_INTERVALS - 1)
slicedColors.push(equaveColor)
slicedLabels = slicedLabels.slice(0, MAX_NUMBER_OF_SHARED_INTERVALS - 1)
slicedLabels.push(equaveLabel)
}
const result: any = {
scale: scale.value.toJSON(),
relativeIntervals: relativeIntervals.value.map((i) => i.toJSON())
scale: slicedScale.toJSON(),
relativeIntervals: slicedIntervals.map((i) => i.toJSON()),
colors: slicedColors,
labels: slicedLabels
}
if (result.colors.length) {
result.colors[result.colors.length - 1] = colors.value[colors.value.length - 1]
}
if (result.labels.length) {
result.labels[result.labels.length - 1] = labels.value[labels.value.length - 1]
}
if (relativeIntervals.value === latticeIntervals.value) {
result.latticeIntervals = null
} else {
result.latticeIntervals = latticeIntervals.value.map((i) => i.toJSON())
if (latticeIntervals.value.length <= MAX_NUMBER_OF_SHARED_INTERVALS) {
result.latticeIntervals = latticeIntervals.value.map((i) => i.toJSON())
} else {
// Give up. The lattice would be incomprehensible anyway.
result.latticeIntervals = null
}
}
for (const [key, value] of Object.entries(LIVE_STATE)) {
if (key in result) {
Expand Down

0 comments on commit bbc3f72

Please sign in to comment.