Skip to content

Commit

Permalink
Remove dropdown for Pythagorean enharmonic
Browse files Browse the repository at this point in the history
Use the sharp enharmonic by default (MIDI standard).
Motivate the user to declare an explicit root pitch when using a black base note.
Update sonic-weave dependency.
Hook into the warning system.

SonicWeave changelog:
- Binary prefixes
- "Aug" and "dim" as alternatives for "a" and "d"
- Vulgar fractions of Aug, M, #, etc. in Pythagorean notation
- Make "/" a bona fide operator
- Helpful warning to let the user know that fraction reduction is called "simplify"

ref #640
  • Loading branch information
frostburn committed Apr 10, 2024
1 parent 87571c4 commit 326cc4a
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 32 deletions.
10 changes: 5 additions & 5 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scale-workshop",
"version": "3.0.0-beta.9",
"version": "3.0.0-beta.10",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
Expand All @@ -21,7 +21,7 @@
"moment-of-symmetry": "^0.4.2",
"pinia": "^2.1.7",
"qs": "^6.12.0",
"sonic-weave": "github:xenharmonic-devs/sonic-weave#v0.0.10",
"sonic-weave": "github:xenharmonic-devs/sonic-weave#v0.0.11",
"sw-synth": "^0.1.0",
"temperaments": "^0.5.3",
"vue": "^3.3.4",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
--color-accent-dimmed: rgba(0, 20, 20, 0.7);

--color-error: red;
--color-warning: orangered;
--color-indicator: #c35;

/* Mimic Bootstrap alert with 'danger' variant */
Expand Down Expand Up @@ -59,6 +60,7 @@
--color-accent-dimmed: rgba(0, 20, 20, 0.7);

--color-error: red;
--color-warning: orangered;
--color-indicator: #b25;
}

Expand Down
3 changes: 3 additions & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ button {
.error {
color: var(--color-error);
}
.warning {
color: var(--color-warning);
}
code {
display: inline-block;
background-color: var(--color-background-soft);
Expand Down
9 changes: 2 additions & 7 deletions src/components/ScaleControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,6 @@ defineExpose({ focus, clearPaletteInfo })
@input="updateScale"
/>
</div>
<div class="control">
<label for="enharmonic">Pythagorean enharmonic</label>
<select id="enharmonic" v-model="scale.enharmonic" @input="updateScale">
<option v-for="e of scale.enharmonics" :key="e" :value="e">{{ e }}</option>
</select>
</div>

<div class="control">
<label for="base-frequency">Base frequency</label>
Expand Down Expand Up @@ -133,7 +127,8 @@ defineExpose({ focus, clearPaletteInfo })
></textarea>
</div>
<ScaleRule :scale="scale.scale" />
<p class="error">{{ scale.error }}</p>
<p v-if="scale.error" class="error">{{ scale.error }}</p>
<p v-else-if="scale.warning" class="warning">{{ scale.warning }}</p>
<h3>Character palette</h3>
<div class="control">
<button
Expand Down
57 changes: 39 additions & 18 deletions src/stores/scale.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Scale } from '@/scale'
import { midiNoteNumberToEnharmonics, type AccidentalStyle, syncValues } from '@/utils'
import {
midiNoteNumberToEnharmonics,
type AccidentalStyle,
syncValues,
isBlackMidiNote,
midiNoteNumberToName
} from '@/utils'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { mmod, mtof } from 'xen-dev-utils'
Expand All @@ -15,7 +21,8 @@ import {
type SonicWeaveValue,
StatementVisitor,
ExpressionVisitor,
builtinNode
builtinNode,
repr
} from 'sonic-weave'
import {
DEFAULT_NUMBER_OF_COMPONENTS,
Expand Down Expand Up @@ -57,10 +64,6 @@ export const useScaleStore = defineStore('scale', () => {

const name = ref('')
const baseMidiNote = ref(60)
const enharmonics = computed(() =>
midiNoteNumberToEnharmonics(baseMidiNote.value, accidentalPreference.value)
)
const enharmonic = ref(enharmonics.value[0])
const userBaseFrequency = ref(261.63)
const autoFrequency = ref(true)
const baseFrequency = computed({
Expand All @@ -79,6 +82,7 @@ export const useScaleStore = defineStore('scale', () => {
const colors = ref(defaultColors(baseMidiNote.value))
const labels = ref(defaultLabels(baseMidiNote.value, accidentalPreference.value))
const error = ref('')
const warning = ref('')

// Keyboard mode affects both physical qwerty and virtual keyboards
const keyboardMode = ref<'isomorphic' | 'piano'>('isomorphic')
Expand All @@ -98,10 +102,11 @@ export const useScaleStore = defineStore('scale', () => {
// === Computed state ===
const sourcePrefix = computed(() => {
const base = `numComponents(${DEFAULT_NUMBER_OF_COMPONENTS})\n`
const rootPitch = midiNoteNumberToName(baseMidiNote.value)
if (autoFrequency.value) {
return `${base}${enharmonic.value} = mtof(_) = 1/1`
return `${base}${rootPitch} = mtof(_) = 1/1`
}
return `${base}${enharmonic.value} = baseFrequency = 1/1`
return `${base}${rootPitch} = baseFrequency = 1/1`
})

const frequencies = computed(() => scale.value.getFrequencyRange(0, NUMBER_OF_NOTES))
Expand Down Expand Up @@ -257,11 +262,6 @@ export const useScaleStore = defineStore('scale', () => {
return baseMidiNote.value - baseInfo.whiteNumber
})

// State synchronization
watch([baseMidiNote, accidentalPreference], () => {
enharmonic.value = enharmonics.value[0]
})

// Sanity watchers
watch(baseMidiNote, (newValue) => {
if (isNaN(newValue)) {
Expand All @@ -274,6 +274,7 @@ export const useScaleStore = defineStore('scale', () => {
// Local storage watchers
syncValues({ accidentalPreference, hasLeftOfZ, gas })

// Extra builtins
function latticeView(this: ExpressionVisitor) {
const scale = this.getCurrentScale()
for (let i = 0; i < scale.length; ++i) {
Expand All @@ -287,6 +288,14 @@ export const useScaleStore = defineStore('scale', () => {
latticeView.__doc__ = 'Store the current scale to be displayed in the lattice tab.'
latticeView.__node__ = builtinNode(latticeView)

function warn(this: ExpressionVisitor, ...args: any[]) {
const s = repr.bind(this);
const message = args.map(a => (typeof a === 'string' ? a : s(a))).join(', ');
warning.value = message
}
warn.__doc__ = 'Issue a warning to the user and continue execution.'
warn.__node__ = builtinNode(warn)

// Local helpers
function getGlobalVisitor() {
// Inject global variables
Expand All @@ -297,7 +306,8 @@ export const useScaleStore = defineStore('scale', () => {
_,
baseMidiNote: _,
baseFrequency: baseFreq,
latticeView
latticeView,
warn
}
const visitor = getSourceVisitor(true, extraBuiltins)
visitor.rootContext.gas = gas.value
Expand Down Expand Up @@ -333,11 +343,17 @@ export const useScaleStore = defineStore('scale', () => {

function computeScale() {
try {
error.value = ''
warning.value = ''
latticeIntervals.value = []
const globalVisitor = getGlobalVisitor()
const visitor = new StatementVisitor(globalVisitor.rootContext, globalVisitor)
const ast = parseAST(sourceText.value)
let userDeclaredPitch = false
for (const statement of ast.body) {
if (statement.type === 'PitchDeclaration') {
userDeclaredPitch = true
}
const interupt = visitor.visit(statement)
if (interupt) {
throw new Error('Illegal statement.')
Expand Down Expand Up @@ -374,12 +390,18 @@ export const useScaleStore = defineStore('scale', () => {
)
}
labels.value = intervals.map((interval) => interval.label || name(interval))
error.value = ''
} else {
scale.value = new Scale(TET12, visitorBaseFrequency, baseMidiNote.value)
colors.value = defaultColors(baseMidiNote.value)
labels.value = defaultLabels(baseMidiNote.value, accidentalPreference.value)
error.value = 'Empty scale defaults to 12-tone equal temperament.'
warning.value = 'Empty scale defaults to 12-tone equal temperament.'
}
const noteNumber = baseMidiNote.value
if (!warning.value && isBlackMidiNote(noteNumber) && !userDeclaredPitch) {
const midiName = midiNoteNumberToName(noteNumber)
const enharmonics = midiNoteNumberToEnharmonics(noteNumber)
const rootFrequency = autoFrequency.value ? 'mtof(_)' : 'baseFrequency'
warning.value = `Base MIDI note ${noteNumber} defaults to ${midiName}. Use an explicit ${enharmonics[0]} = ${rootFrequency} or ${enharmonics[1]} = ${rootFrequency} to get rid of this warning.`
}
} catch (e) {
if (e instanceof Error) {
Expand All @@ -403,8 +425,6 @@ export const useScaleStore = defineStore('scale', () => {
// Live state
name,
baseMidiNote,
enharmonics,
enharmonic,
userBaseFrequency,
autoFrequency,
autoColors,
Expand All @@ -421,6 +441,7 @@ export const useScaleStore = defineStore('scale', () => {
latticeColors,
latticeLabels,
error,
warning,
keyboardMode,
equaveShift,
degreeShift,
Expand Down
19 changes: 19 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,25 @@ export function midiNoteNumberToEnharmonics(
return result
}

const IS_BLACK_MIDI_NOTE = [
false, // C
true, // C# / Db
false, // D
true, // D# / Eb
false, // E
false, // F
true, // F# / Gb
false, // G
true, // G# / Ab
false, // A
true, // A# / Bb
false // B
]

export function isBlackMidiNote(noteNumber: number) {
return IS_BLACK_MIDI_NOTE[mmod(noteNumber, 12)]
}

export function annotateColors(sourceLines: string[], keyColors: string[]) {
if (!keyColors.length) {
return
Expand Down

0 comments on commit 326cc4a

Please sign in to comment.