Skip to content

Commit

Permalink
Merge pull request #522 from xenharmonic-devs/more-pizza
Browse files Browse the repository at this point in the history
Use pinia to manage app state
  • Loading branch information
frostburn authored Jan 13, 2024
2 parents a172aef + 3a5917e commit 12a3f57
Show file tree
Hide file tree
Showing 14 changed files with 640 additions and 877 deletions.
522 changes: 125 additions & 397 deletions src/App.vue

Large diffs are not rendered by default.

228 changes: 103 additions & 125 deletions src/components/ScaleBuilder.vue

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions src/components/modals/generation/HistoricalScale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { spineLabel as spineLabel_, type AccidentalStyle } from '@/utils'
const props = defineProps<{
centsFractionDigits: number
accidentalStyle: AccidentalStyle
}>()
const emit = defineEmits([
Expand Down Expand Up @@ -53,11 +54,8 @@ const SYNTONIC = new Interval(
'ratio'
)
const ACCIDENTAL_STYLE =
(localStorage.getItem('accidentalPreference') as AccidentalStyle) ?? 'double'
function spineLabel(up: number) {
return spineLabel_(up, ACCIDENTAL_STYLE)
return spineLabel_(up, props.accidentalStyle)
}
const method = ref<'simple' | 'target' | 'well temperament'>('simple')
Expand Down
22 changes: 22 additions & 0 deletions src/stores/midi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { Input, Output } from 'webmidi'

export const useMidiStore = defineStore('midi', () => {
const input = ref<Input | null>(null)
const output = ref<Output | null>(null)
// Channel 10 is reserved for percussion
const outputChannels = ref(new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16]))
const velocityOn = ref(true)

const whiteMode = ref<'off' | 'simple' | 'blackAverage' | 'keyColors'>('off')

return {
// State
input,
output,
outputChannels,
velocityOn,
whiteMode
}
})
236 changes: 236 additions & 0 deletions src/stores/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { computed, reactive, ref, watch } from 'vue'
import { defineStore } from 'pinia'
import {
Interval,
Scale,
parseLine,
type IntervalOptions,
reverseParseScale
} from 'scale-workshop-core'
import { DEFAULT_NUMBER_OF_COMPONENTS, NUMBER_OF_NOTES, UNIX_NEWLINE } from '@/constants'
import { computeWhiteIndices } from '@/midi'
import { mapWhiteAsdfBlackQwerty, mapWhiteQweZxcBlack123Asd } from '@/keyboard-mapping'
import { arraysEqual } from 'xen-dev-utils'
import type { AccidentalStyle } from '@/utils'

export const useStateStore = defineStore('state', () => {
// Nonpersistent state of the application
const scaleName = ref('')
const scaleLines = ref<string[]>([])
const scale = reactive(Scale.fromIntervalArray([parseLine('1/1', DEFAULT_NUMBER_OF_COMPONENTS)]))
const baseMidiNote = ref(69)
const keyColors = ref([
'white',
'black',
'white',
'white',
'black',
'white',
'black',
'white',
'white',
'black',
'white',
'black'
])
const isomorphicVertical = ref(5)
const isomorphicHorizontal = ref(1)
// Keyboard mode affects both physical qwerty and virtual keyboards
const keyboardMode = ref<'isomorphic' | 'piano'>('isomorphic')
// Physical layout mimics a piano layout in one or two layers
const pianoMode = ref<'Asdf' | 'QweZxc0' | 'QweZxc1'>('Asdf')
const equaveShift = ref(0)
const degreeShift = ref(0)
const heldNotes = reactive(new Map<number, number>())
const typingActive = ref(true)

// These user preferences are fetched from local storage.
const storage = window.localStorage
const newline = ref(storage.getItem('newline') ?? UNIX_NEWLINE)

const storedScheme = storage.getItem('colorScheme')
const mediaScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const colorScheme = ref<'light' | 'dark'>(
(storedScheme ?? mediaScheme) === 'dark' ? 'dark' : 'light'
)
const centsFractionDigits = ref(parseInt(storage.getItem('centsFractionDigits') ?? '3', 10))
const decimalFractionDigits = ref(parseInt(storage.getItem('decimalFractionDigits') ?? '5', 10))
const showVirtualQwerty = ref(storage.getItem('showVirtualQwerty') === 'true')
const midiOctaveOffset = ref(parseInt(storage.getItem('midiOctaveOffset') ?? '-1', 10))
const intervalMatrixIndexing = ref(parseInt(storage.getItem('intervalMatrixIndexing') ?? '0', 10))
const accidentalPreference = ref<AccidentalStyle>(
(localStorage.getItem('accidentalPreference') as AccidentalStyle) ?? 'double'
)

// Special keyboard codes also from local storage.
const deactivationCode = ref(storage.getItem('deactivationCode') ?? 'Backquote')
const equaveUpCode = ref(storage.getItem('equaveUpCode') ?? 'NumpadMultiply')
const equaveDownCode = ref(storage.getItem('equaveDownCode') ?? 'NumpadDivide')
const degreeUpCode = ref(storage.getItem('degreeUpCode') ?? 'NumpadAdd')
const degreeDownCode = ref(storage.getItem('degreeDownCode') ?? 'NumpadSubtract')

// === Computed state ===
const frequencies = computed(() =>
scale.getFrequencyRange(-baseMidiNote.value, NUMBER_OF_NOTES - baseMidiNote.value)
)

const baseIndex = computed(
() => baseMidiNote.value + equaveShift.value * scale.size + degreeShift.value
)

// For midi mapping
const whiteIndices = computed(() => computeWhiteIndices(baseMidiNote.value, keyColors.value))

const keyboardMapping = computed<Map<string, number>>(() => {
const size = scale.size
const baseIndex = baseMidiNote.value + equaveShift.value * size + degreeShift.value
if (pianoMode.value === 'Asdf') {
return mapWhiteAsdfBlackQwerty(keyColors.value, baseMidiNote.value, baseIndex)
} else if (pianoMode.value === 'QweZxc0') {
return mapWhiteQweZxcBlack123Asd(keyColors.value, size, baseMidiNote.value, baseIndex, 0)
} else {
return mapWhiteQweZxcBlack123Asd(keyColors.value, size, baseMidiNote.value, baseIndex, 1)
}
})

// === State updates ===
function updateFromScaleLines(lines: string[]) {
if (arraysEqual(lines, scaleLines.value)) {
return
}
scaleLines.value = lines
const intervals: Interval[] = []
const options: IntervalOptions = {
centsFractionDigits: centsFractionDigits.value,
decimalFractionDigits: decimalFractionDigits.value
}
lines.forEach((line) => {
try {
const interval = parseLine(line, DEFAULT_NUMBER_OF_COMPONENTS, options)
intervals.push(interval)
} catch {
/* empty */
}
})
if (!intervals.length) {
intervals.push(parseLine('1/1', DEFAULT_NUMBER_OF_COMPONENTS, options))
}

const surrogate = Scale.fromIntervalArray(intervals)
scale.intervals = surrogate.intervals
scale.equave = surrogate.equave
}

function updateFromScale(surrogate: Scale) {
scale.intervals = surrogate.intervals
scale.equave = surrogate.equave
scaleLines.value = reverseParseScale(scale)
}

// Computed wrappers to avoid triggering a watcher loop.
const scaleWrapper = computed({
get() {
return scale
},
set: updateFromScale
})

const scaleLinesWrapper = computed({
get() {
return scaleLines.value
},
set: updateFromScaleLines
})

// Local storage watchers
watch(newline, (newValue) => window.localStorage.setItem('newline', newValue))
watch(
colorScheme,
(newValue) => {
window.localStorage.setItem('colorScheme', newValue)
document.documentElement.setAttribute('data-theme', newValue)
},
{ immediate: true }
)
watch(centsFractionDigits, (newValue) =>
window.localStorage.setItem('centsFractionDigits', newValue.toString())
)
watch(decimalFractionDigits, (newValue) =>
window.localStorage.setItem('decimalFractionDigits', newValue.toString())
)
watch(showVirtualQwerty, (newValue) =>
window.localStorage.setItem('showVirtualQwerty', newValue.toString())
)
watch(midiOctaveOffset, (newValue) =>
window.localStorage.setItem('midiOctaveOffset', newValue.toString())
)
watch(intervalMatrixIndexing, (newValue) =>
window.localStorage.setItem('intervalMatrixIndexing', newValue.toString())
)
watch(accidentalPreference, (newValue) => localStorage.setItem('accidentalPreference', newValue))
// Store keymaps
watch(deactivationCode, (newValue) => window.localStorage.setItem('deactivationCode', newValue))
watch(equaveUpCode, (newValue) => window.localStorage.setItem('equaveUpCode', newValue))
watch(equaveDownCode, (newValue) => window.localStorage.setItem('equaveDownCode', newValue))
watch(degreeUpCode, (newValue) => window.localStorage.setItem('degreeUpCode', newValue))
watch(degreeDownCode, (newValue) => window.localStorage.setItem('degreeDownCode', newValue))

// Sanity watchers
watch(baseMidiNote, (newValue) => {
if (isNaN(newValue)) {
baseMidiNote.value = 69
} else if (Math.round(newValue) != newValue) {
baseMidiNote.value = Math.round(newValue)
}
})

// Methods
function getFrequency(index: number) {
if (index >= 0 && index < frequencies.value.length) {
return frequencies.value[index]
} else {
// Support more than 128 notes with some additional computational cost
return scale.getFrequency(index - baseMidiNote.value)
}
}

return {
// Live state
scaleName,
scaleLinesRaw: scaleLines,
scaleLines: scaleLinesWrapper,
scaleRaw: scale,
scale: scaleWrapper,
baseMidiNote,
keyColors,
isomorphicVertical,
isomorphicHorizontal,
keyboardMode,
pianoMode,
equaveShift,
degreeShift,
heldNotes,
typingActive,
// Persistent state
newline,
colorScheme,
centsFractionDigits,
decimalFractionDigits,
showVirtualQwerty,
midiOctaveOffset,
intervalMatrixIndexing,
deactivationCode,
equaveUpCode,
equaveDownCode,
degreeUpCode,
degreeDownCode,
accidentalPreference,
// Computed state
frequencies,
baseIndex,
whiteIndices,
keyboardMapping,
// Methods
getFrequency
}
})
35 changes: 12 additions & 23 deletions src/views/AnalysisView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,30 @@
import { intervalMatrix } from '@/analysis'
import ChordWheel from '@/components/ChordWheel.vue'
import { computed, ref } from 'vue'
import {
reverseParseInterval,
type Interval,
type IntervalOptions,
type Scale
} from 'scale-workshop-core'
import { reverseParseInterval, type Interval, type IntervalOptions } from 'scale-workshop-core'
import { useAudioStore } from '@/stores/audio'
import { useStateStore } from '@/stores/state'
const MAX_SCALE_SIZE = 100
const props = defineProps<{
scale: Scale
colorScheme: 'light' | 'dark'
intervalMatrixIndexing: number
}>()
const emit = defineEmits(['update:intervalMatrixIndexing'])
const audio = useAudioStore()
const state = useStateStore()
const cellFormat = ref<'best' | 'cents' | 'decimal'>('best')
const trailLongevity = ref(70)
const maxOtonalRoot = ref(16)
const maxUtonalRoot = ref(23)
const intervalMatrixIndexingRadio = computed({
get: () => props.intervalMatrixIndexing.toString(),
set: (newValue: string) => emit('update:intervalMatrixIndexing', parseInt(newValue, 10))
get: () => state.intervalMatrixIndexing.toString(),
set: (newValue: string) => (state.intervalMatrixIndexing = parseInt(newValue, 10))
})
const fadeAlpha = computed(() => 1 - trailLongevity.value / 100)
const backgroundRBG = computed<[number, number, number]>(() => {
// Add dependency.
props.colorScheme
state.colorScheme
// Fetch from document.
const css = getComputedStyle(document.documentElement)
.getPropertyValue('--color-background')
Expand All @@ -53,7 +42,7 @@ const backgroundRBG = computed<[number, number, number]>(() => {
const strokeStyle = computed(() => {
// Add dependency.
props.colorScheme
state.colorScheme
// Fetch from document.
return getComputedStyle(document.documentElement).getPropertyValue('--color-text').trim()
})
Expand Down Expand Up @@ -91,7 +80,7 @@ function formatMatrixCell(interval: Interval) {
const matrix = computed(() => {
return intervalMatrix(
props.scale.head(MAX_SCALE_SIZE).mergeOptions({
state.scale.head(MAX_SCALE_SIZE).mergeOptions({
centsFractionDigits: 1,
decimalFractionDigits: 3
})
Expand All @@ -106,13 +95,13 @@ const matrix = computed(() => {
<table>
<tr>
<th></th>
<th v-for="i of Math.min(scale.size, MAX_SCALE_SIZE)" :key="i">
{{ i - 1 + intervalMatrixIndexing }}
<th v-for="i of Math.min(state.scale.size, MAX_SCALE_SIZE)" :key="i">
{{ i - 1 + state.intervalMatrixIndexing }}
</th>
<th>({{ scale.size + intervalMatrixIndexing }})</th>
<th>({{ state.scale.size + state.intervalMatrixIndexing }})</th>
</tr>
<tr v-for="(row, i) of matrix" :key="i">
<th>{{ formatMatrixCell(scale.getInterval(i)) }}</th>
<th>{{ formatMatrixCell(state.scale.getInterval(i)) }}</th>
<td v-for="(name, j) of row" :key="j">{{ name }}</td>
</tr>
</table>
Expand Down
Loading

0 comments on commit 12a3f57

Please sign in to comment.