Skip to content

Commit 3ab47d3

Browse files
committed
Update normalization of different spellings, add a way to add differently spelled presets
1 parent b17ce4b commit 3ab47d3

18 files changed

Lines changed: 676 additions & 135 deletions

docs/custom-tunings.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
{
3939
"version": 2,
4040
"name": "My Custom Tuning",
41+
"spelling": "german",
4142
"system": { "edo": 12 },
4243
"tuning": {
4344
"strings": [
@@ -66,6 +67,7 @@
6667

6768
- `version` (number, optional for parsing, exporter writes `2`): tag for your own bookkeeping.
6869
- `name` (string, required): shown in the UI.
70+
- `spelling` (string, optional): spelling hint for note parsing. If set to a Germanic/Czech marker (`"german"`, `"germanic"`, `"czech"`, `"cz"`, `"de"`, `"de h/b"`, `"de-h/b"`, `"de_h/b"`, `"h/b"`), note tokens are translated to international spellings internally (for example `H``B`, `B``Bb`, `Fis``F#`, `Hih``B↑`, `Aeh``A↓`).
6971
- `system.edo` (number, required): equal divisions of the octave (12 for 12‑TET, 19 for 19‑TET, etc.).
7072

7173
**Strings (array, required, ≥1)**
@@ -121,9 +123,24 @@ This example models the short 5th string starting at fret 5 and showing a grey s
121123
## Tips & gotchas
122124

123125
- **Octaves are ignored.** Use `E`, not `E2`. Use `F#` or `Gb` — whichever you prefer; import does not change your accidental spelling.
126+
- **German/Czech preset spelling.** If your JSON uses `H/B/Fis/Es/...`, set top-level `spelling` to a Germanic/Czech value (for example `"german"`, `"czech"`, or `"cz"`). Internally the app stores/uses international spellings and only localizes at display time.
127+
- **Microtonal German suffixes work too.** With `spelling: "german"` or `spelling: "czech"`, both `ih/eh` suffixes and arrow forms are accepted (`Hih` or `B↑`, `Aeh` or `A↓`).
124128
- **Order matters.** Strings are read top‑to‑bottom in the JSON array the same way they’re drawn in the UI.
125129
- **Short strings (banjo, etc.).** Put `startFret` and `greyBefore: true` on that string (and optionally mirror in `meta.stringMeta`).
126130

131+
Microtonal example:
132+
133+
```json
134+
{
135+
"name": "24-TET Germanic micro example",
136+
"spelling": "german",
137+
"system": { "edo": 24 },
138+
"tuning": {
139+
"strings": [{ "note": "Hih" }, { "note": "Aeh" }, { "note": "Fis" }]
140+
}
141+
}
142+
```
143+
127144
---
128145

129146
## Multiple tunings in one file

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"tsx": "^4.21.0",
5050
"typescript": "^5.9.3",
5151
"typescript-eslint": "^8.58.0",
52-
"vite": "^8.0.3",
52+
"vite": "^8.0.5",
5353
"vite-plugin-html-minifier-terser": "^3.8.0",
5454
"vite-plugin-image-optimizer": "^2.0.3"
5555
},

pnpm-lock.yaml

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default function App() {
9090
defaultTunings: DEFAULT_TUNINGS,
9191
presetTunings: PRESET_TUNINGS,
9292
confirm,
93+
noteNaming: displayPrefs.noteNaming,
9394
});
9495
const { instrumentState, instrumentDerived, capo } = instrumentDomain;
9596
const { strings, tuning, stringMeta, boardMeta } = instrumentState;

src/app/adapters/controls.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function buildInstrumentControlModel({
2929
meta: {
3030
systems: instrument.tunings,
3131
sysNames: instrument.sysNames,
32+
noteNaming: instrument.noteNaming,
3233
presetNames: presets.mergedPresetNames,
3334
customPresetNames: presets.customPresetNames,
3435
presetMetaMap: presets.mergedPresetMetaMap,

src/app/hooks/useInstrumentDomain.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function useInstrumentDomain({
2424
defaultTunings,
2525
presetTunings,
2626
confirm,
27+
noteNaming = "english",
2728
}) {
2829
const instrument = useInstrumentConfig({
2930
system,
@@ -101,6 +102,7 @@ export function useInstrumentDomain({
101102
systemDivisions,
102103
sysNames,
103104
tunings,
105+
noteNaming,
104106
},
105107
presets: {
106108
mergedPresetNames,
@@ -127,6 +129,7 @@ export function useInstrumentDomain({
127129
systemDivisions,
128130
sysNames,
129131
tunings,
132+
noteNaming,
130133
mergedPresetNames,
131134
customPresetNames,
132135
mergedPresetMetaMap,

src/components/UI/controls/InstrumentControls.jsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { withToastPromise } from "@/utils/toast";
1111
import { memoWithShallowPick } from "@/utils/memo";
1212
import NumberField from "@/components/UI/NumberField";
13+
import { renderNoteName } from "@/lib/theory/noteNaming";
14+
import { normalizeIntlNoteName } from "@/lib/theory/notation";
1315

1416
function InstrumentControls({ state, actions, meta }) {
1517
const { strings, frets, tuning, systemId, selectedPreset } = state;
@@ -24,12 +26,30 @@ function InstrumentControls({ state, actions, meta }) {
2426
onCreateCustomPack,
2527
onEditCustomPack,
2628
} = actions;
27-
const { systems, sysNames, presetNames, customPresetNames, presetMetaMap } =
29+
const {
30+
systems,
31+
sysNames,
32+
noteNaming,
33+
presetNames,
34+
customPresetNames,
35+
presetMetaMap,
36+
} =
2837
meta;
2938
const safeSystems = systems ?? {};
3039
const safeSysNames = Array.isArray(sysNames) ? sysNames : [];
3140
const safeTuning = Array.isArray(tuning) ? tuning : [];
3241

42+
const optionEntries = Array.from(
43+
new Map(
44+
safeSysNames.map((displayName) => [
45+
normalizeIntlNoteName(displayName, {
46+
translateGerman: noteNaming === "german",
47+
}),
48+
displayName,
49+
]),
50+
),
51+
).map(([value, label]) => ({ value, label }));
52+
3353
const onSaveDefault = () =>
3454
withToastPromise(
3555
() => handleSaveDefault?.(),
@@ -100,7 +120,10 @@ function InstrumentControls({ state, actions, meta }) {
100120
<div className="tv-controls__strings-grid">
101121
{safeTuning.map((note, i) => {
102122
const stringNum = strings - i;
103-
const hasOption = safeSysNames.includes(note);
123+
const noteValue = normalizeIntlNoteName(note, {
124+
translateGerman: noteNaming === "german",
125+
});
126+
const hasOption = optionEntries.some((entry) => entry.value === noteValue);
104127
return (
105128
<div key={i} className="tv-field">
106129
<label htmlFor={`string-${stringNum}`}>
@@ -109,18 +132,22 @@ function InstrumentControls({ state, actions, meta }) {
109132
<select
110133
id={`string-${stringNum}`}
111134
name={`string-${stringNum}`}
112-
value={note}
135+
value={noteValue}
113136
onChange={(e) => {
114137
const value = e.target.value;
115138
setTuning((d) => {
116139
d[i] = value;
117140
});
118141
}}
119142
>
120-
{!hasOption && <option value={note}>{note}</option>}
121-
{safeSysNames.map((n) => (
122-
<option key={n} value={n}>
123-
{n}
143+
{!hasOption && (
144+
<option value={noteValue}>
145+
{renderNoteName(noteValue, noteNaming)}
146+
</option>
147+
)}
148+
{optionEntries.map((entry) => (
149+
<option key={`${entry.value}:${entry.label}`} value={entry.value}>
150+
{entry.label}
124151
</option>
125152
))}
126153
</select>
@@ -190,6 +217,7 @@ function pickInstrumentMemoProps(p) {
190217
selectedPreset: s.selectedPreset,
191218
systems: m.systems,
192219
sysNames: m.sysNames,
220+
noteNaming: m.noteNaming,
193221
presetNames: m.presetNames,
194222
customPresetNames: m.customPresetNames,
195223
presetMetaMap: m.presetMetaMap,

0 commit comments

Comments
 (0)