Skip to content

Commit

Permalink
fix dont crash on unrecognized key
Browse files Browse the repository at this point in the history
  • Loading branch information
TeemuKoivisto committed Jun 30, 2024
1 parent b4bf982 commit 1ef76de
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 43 deletions.
12 changes: 4 additions & 8 deletions packages/chords-and-scales/src/createScale.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { getKeySignature } from './getKeySignature'
import { findScale } from './scales'
import { NOTES, getRootNote } from './notes'
import { isValidKey, normalizeKey, NOTES, getRootNote } from './notes'

import type { Interval, Pitch, Result, Scale, ScaleNote } from './types'

const regexKey = /^[a-gA-G][♭b#♯]?$/
const alphabet = 'ABCDEFG'

/**
Expand Down Expand Up @@ -87,22 +86,19 @@ function createScaleNotes(startingOrder: number, letters: string[], intervals: P
/**
* Creates a scale from 2-length key and scale name
*
* @param rawKey Key comprising of [a-gA-G][♭b#♯]?
* @param rawKey Key adhering to [a-gA-G][♭Bb#♯sS] regex
* @param scaleName
* @returns
*/
export function createScale(rawKey: string, scaleName: string): Result<Scale> {
if (!regexKey.test(rawKey)) {
if (!isValidKey(rawKey)) {
return { err: `Unknown key: ${rawKey}`, code: 400 }
}
const scale = findScale(scaleName)
if (!scale) {
return { err: `Unknown scale: ${scaleName}`, code: 404 }
}
const key = `${rawKey.charAt(0).toUpperCase()}${rawKey
.charAt(1)
.replace('b', '♭')
.replace('#', '♯')}`
const key = normalizeKey(rawKey)
const foundRoot = getRootNote(key)
if (!foundRoot) {
return { err: `Unable to find root for note: ${key}`, code: 404 }
Expand Down
30 changes: 18 additions & 12 deletions packages/chords-and-scales/src/notes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { MidiNote, Result, ScaleNote } from './types'

const regexKey = /^[a-gA-G][♭Bb#♯sS]*[0-9]?$/
const flatAccidental = /^[♭Bb]$/
const sharpAccidental = /^[#♯sS]$/
const regexKeyLetter = /^[a-gA-G]$/
const regexPosInt = /^[0-9]$/
const regexKey = /^[a-gA-G][♭Bb#♯sS]*$/
const regexKeyOctave = /^[a-gA-G][♭Bb#♯sS]*[0-9]?$/

export const NOTES = [
{ note: 'C', semitones: 0, sharps: 0, flats: 0 },
Expand All @@ -18,6 +22,16 @@ export const NOTES = [
{ note: 'B', semitones: 11, sharps: 0, flats: 0 }
]

export const isValidKey = (raw: string) => regexKey.test(raw)
export const normalizeKey = (raw: string) =>
[
regexKeyLetter.test(raw[0] || '') ? raw[0].toUpperCase() : '',
...raw
.slice(1)
.split('')
.map(c => (flatAccidental.test(c) ? '♭' : sharpAccidental.test(c) ? '♯' : ''))
].join('')

export function getOctave(note: { midi: number; flats: number; sharps: number }) {
const norm = note.midi + note.flats - note.sharps
return norm === 12 ? 0 : Math.floor((norm - 12) / 12)
Expand Down Expand Up @@ -47,7 +61,7 @@ export function getNote(midi: number): MidiNote {
}

export function parseNote(raw: string, strict = true, requireOctave = false): Result<MidiNote> {
if (strict && !regexKey.test(raw)) {
if (strict && !regexKeyOctave.test(raw)) {
return { err: `Unrecognized note "${raw}"`, code: 400 }
}
const note = raw.trim()
Expand All @@ -59,11 +73,7 @@ export function parseNote(raw: string, strict = true, requireOctave = false): Re
const shifted = note
.slice(1)
.split('')
.reduce(
(acc, c) =>
acc + (c.toLowerCase() === 'b' || c === '♭' ? -1 : c === '#' || c === '♯' ? 1 : 0),
0
)
.reduce((acc, c) => acc + (flatAccidental.test(c) ? -1 : sharpAccidental.test(c) ? 1 : 0), 0)
let octave: number | undefined
if (regexPosInt.test(note.charAt(note.length - 1))) {
try {
Expand Down Expand Up @@ -96,11 +106,7 @@ export function getRootNote(note: string): ScaleNote | undefined {
const shifted = note
.slice(1)
.split('')
.reduce(
(acc, c) =>
acc + (c.toLowerCase() === 'b' || c === '♭' ? -1 : c === '#' || c === '♯' ? 1 : 0),
0
)
.reduce((acc, c) => acc + (flatAccidental.test(c) ? -1 : sharpAccidental.test(c) ? 1 : 0), 0)
const semitones = (rootNote.semitones + shifted) % 12
return {
note,
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/components/play/ScoreOptions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import arrowDown from '@iconify-icons/mdi/arrow-down'
import { writable } from 'svelte/store'
import { getNoteAbsolute, parseNote, scalesFromJSON } from '@/chords-and-scales'
import { getNoteAbsolute, normalizeKey, parseNote, scalesFromJSON } from '@/chords-and-scales'
import { currentGame } from '$stores/game'
import { inputsActions, midiRangeNotes } from '$stores/inputs'
Expand Down Expand Up @@ -69,7 +69,8 @@
function handleKeyChange({
currentTarget: { value }
}: Event & { currentTarget: EventTarget & HTMLInputElement }) {
selectedKey = `${value.charAt(0).toUpperCase()}${value.charAt(1).toLowerCase()}`
const norm = normalizeKey(value)
selectedKey = norm
scoreActions.setKey(selectedKey)
}
function handleSelectScale(key: string | number) {
Expand Down
17 changes: 9 additions & 8 deletions packages/client/src/modals/Introduction.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@
target="_blank"
>
GitHub
</a>
GitHub repository or send me email directly (see About).
</a> repository or send me email directly (see About).
</p>
<p>
The most confusing part has been perhaps how the used accidentals are decided. I suppose in
most cases you use either all flats or sharps but since sometimes a note is specified as
either flat or sharp interval, I ultimately resolve to use its accidental. I'm not sure if
this is always the correct way but it was the most programming-friendly.
The most confusing part has been perhaps how the used accidentals (♭♯) are decided. I suppose
in most cases you use either all flats or sharps for the notes in a specific scale (eg C♯) but
since sometimes a note is specified as either flat or sharp interval (minor third 3♭), I
ultimately resolve to use its accidental. I'm not sure if this is always the correct way but
it was the most programming-friendly.
</p>
<p>
You can input a <b>Key</b> and visualize the notes in every scale. I am also displaying the
Expand Down Expand Up @@ -185,7 +185,8 @@
for flats and
<b>F♯, C♯, G♯, D♯, A♯, E♯, B♯</b> for sharps. So if there were Bb, Eb, Db and Gb flats in the scale,
the key signature would only include Bb, Eb and then use accidentals for the rest. I could add
5 flats to the signature and then naturalize the As but that starts to get convoluted.
5 flats to the signature and then naturalize the As but that starts to get convoluted. Only reason
I see for doing so is when the scale is altered from an existing one (D♭ major natural-3)
</p>
<h3>Play</h3>
<p>
Expand All @@ -201,7 +202,7 @@
</p>
<p>
One big missing feature from the games are the inversions of chords as well as missing notes.
This is after all how majority of chords are played. Maybe one day I'll find time for it. Also
This is after all how majority of chords are played. Maybe I'll one day find time for it. Also
playing the given scale degree trichord would be another. As well as recognizing the played
scales. Oh well. Pull requests are welcome!
</p>
Expand Down
17 changes: 4 additions & 13 deletions packages/client/src/stores/score.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { derived, get, readable, writable } from 'svelte/store'
import { createScale } from '@/chords-and-scales'
import { createScale, createScaleUnsafe } from '@/chords-and-scales'

import { inputs } from './inputs'

Expand All @@ -12,24 +12,15 @@ export interface PlayedNote extends MidiNote {

let timeout: ReturnType<typeof setTimeout> | undefined

const C_MAJOR = createScaleUnsafe('C', 'Major')

export const keyAndScale = writable<[string, string]>(['C', 'Major'])
export const scaleData = derived(keyAndScale, (val): Scale => {
const res = createScale(val[0], val[1])
if ('data' in res) {
return res.data
}
return {
key: val[0],
scale: val[1],
names: ['major'],
flats: 0,
sharps: 0,
majorSignature: 'C',
intervals: [],
scaleNotes: [],
trichords: [],
notesMap: new Map()
} as Scale
return C_MAJOR
})
export const target = writable<MidiNote[]>([])
export const played = writable<PlayedNote[]>([])
Expand Down

0 comments on commit 1ef76de

Please sign in to comment.