Skip to content

Commit

Permalink
feat(Dictionary): added basic module
Browse files Browse the repository at this point in the history
  • Loading branch information
ArTiSTiX committed Dec 19, 2023
1 parent 1f2a8be commit bbdbbe9
Show file tree
Hide file tree
Showing 27 changed files with 810 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"start:preload": "cross-env NODE_ENV=development DEBUG=app:* DEBUG_COLORS=true TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development DEBUG=app:* DEBUG_COLORS=true TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest",
"types": "npm exec tsc",
"schema": "concurrently \"npm run schema:settings\" \"npm run schema:sessions\" \"npm run schema:windowstate\"",
"schema:settings": "typescript-json-schema --strictNullChecks --refs false \"src/main/types/Settings.ts\" Settings -o \"src/main/types/Settings.schema.json\"",
"schema:sessions": "typescript-json-schema --strictNullChecks --refs false \"src/main/types/Midi.ts\" Midi -o \"src/main/types/Midi.schema.json\"",
Expand Down
1 change: 1 addition & 0 deletions src/main/types/Settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
},
"label": {
"enum": [
"chordNote",
"interval",
"none",
"note",
Expand Down
2 changes: 1 addition & 1 deletion src/main/types/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export type KeyboardSettings = {
skin: 'classic' | 'flat';
from: string;
to: string;
label: 'none' | 'pitchClass' | 'note' | 'interval';
label: 'none' | 'pitchClass' | 'note' | 'chordNote' | 'interval';
keyName: 'none' | 'octave' | 'pitchClass' | 'note';
keyInfo: 'none' | 'tonic' | 'interval' | 'tonicAndInterval';
fadeOutDuration: number;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.base {
display: flex;
align-items: center;
justify-content: center;
height: 6em;
}

Expand Down Expand Up @@ -35,7 +36,7 @@
}

.interval--active {
background-color: $color-success-normal;
background-color: $color-primary-normal;
}

.interval--played {
Expand Down
29 changes: 22 additions & 7 deletions src/renderer/components/ChordName/ChordName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ enum ALIAS_NOTATION {
symbol = 2,
}

function getChordSymbol(chord: ChordNameProps['chord'], notation: ChordNameProps['notation']) {
if (!chord) {
return '';
}

if (typeof notation === 'string') {
if (chord.aliases[ALIAS_NOTATION[notation]] !== undefined) {
return chord.tonic + chord.aliases[ALIAS_NOTATION[notation]];
}
} else if (typeof notation === 'number' && chord.aliases[notation] !== undefined) {
return chord.tonic + chord.aliases[notation];
}

if (chord.aliases[ALIAS_NOTATION.short] !== undefined) {
return chord.tonic + chord.aliases[ALIAS_NOTATION.short];
}
return chord.symbol;
}

export const ChordName: React.FC<ChordNameProps> = ({
className,
chord,
Expand All @@ -26,13 +45,9 @@ export const ChordName: React.FC<ChordNameProps> = ({
}) => {
if (!chord) return null;

const symbol =
// eslint-disable-next-line no-nested-ternary
chord.aliases[ALIAS_NOTATION[notation]] !== undefined
? chord.tonic + chord.aliases[ALIAS_NOTATION[notation]]
: chord.aliases[ALIAS_NOTATION.short] !== undefined
? chord.tonic + chord.aliases[ALIAS_NOTATION.short]
: chord.symbol;
const symbol = getChordSymbol(chord, notation);

if (!symbol) return null;

const [tonic, type] = tokenizeChord(symbol);
const tokens = tokenizeChordType(type);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/ChordName/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Chord } from '@tonaljs/chord';
export type ChordNameProps = {
className?: string;
chord?: Chord | null;
notation?: 'long' | 'short' | 'symbol';
notation?: 'long' | 'short' | 'symbol' | number;
hideRoot?: boolean;
highlightAlterations?: boolean;
latinSharpsFlats?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/Icon/icons/dictionary.react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/renderer/components/Icon/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const ICON_NAMES = [
'clock',
'controller',
'cross',
'dictionary',
'exclamation',
'github',
'heart',
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/components/PianoKeyboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
KeySignatureConfig,
formatSharpsFlats,
getChordDegrees,
getChordNotes,
getNoteInKeySignature,
} from 'renderer/helpers';
import { Note } from 'tonal';
Expand Down Expand Up @@ -160,6 +161,17 @@ export const highlightLabels = (
highlightLabel(containerEl, midi[i], intervals[i]);
}
}
} else if (keyboard.label === 'chordNote') {
if (chord) {
const notes = getChordNotes(
chord,
midi.map((midiNote) => Note.pitchClass(Note.fromMidi(midiNote)))
);

for (let i = 0; i < midi.length; i += 1) {
highlightLabel(containerEl, midi[i], formatSharpsFlats(notes[i]));
}
}
} else {
for (let i = 0; i < midi.length; i += 1) {
const note = Note.get(Note.fromMidi(midi[i]));
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/helpers/chords-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
const CHORDS: string[][] = [
// ==Intervals==
['1P 5P', 'fifth', '5'],
['1P 5P', 'fifth', '5 5 5'],

// ==Major==
// '''Normal'''
Expand All @@ -36,7 +36,7 @@ const CHORDS: string[][] = [
['1P 3M 5P* 6M 9M 11A', 'sixth ninth added sharp eleventh', 'maj6/9add#11 6/9add#11 69#11'],

['1P 3M 5P* 7M 11P', 'major seventh added eleventh', 'maj7add11 M7add11 Δ7add11 Δadd11'],
['1P 3M 5P* 7M 11A', 'major seventh added sharp eleventh', 'maj7add#11 M7add#11 Δadd#11 majadd#4 Δadd#4'],
['1P 3M 5P* 7M 11A', 'major seventh added sharp eleventh', 'maj7add#11 M7add#11 Δadd#11 maj7add#4 M7add#4 Δadd#4'],
['1P 3M 5P* 7M 13M', 'major seventh added thirteenth', 'maj7add13 M7add13 Δ7add13'], // removed 9M
['1P 3M 5P* 7M 13m', 'major seventh added flat thirteenth', 'maj7addb13 M7addb13 Δ7addb13'],

Expand Down
20 changes: 19 additions & 1 deletion src/renderer/helpers/chords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const CHORD_NAME_REGEX = /^(([A-G])([b]+|[#]+)?)(.*?)(\/([A-G]([b]+|[#]+)

export const CHORD_TYPE_SPECIALCASE_TOKEN = '6/9|6/11|6/13|no[0-9]{1,2}|quartal';
export const CHORD_TYPE_QUALITY_TOKEN =
'(min|maj|Maj|m/maj?|mM|M|m|-|\\+|aug|dim|dom|sus|o|Δ|^|°|ø|q)(6/9|6/11|6/13|[0-9]{1,2})?';
'(min|maj|Maj|m/maj?|M|m|-|\\+|aug|dim|dom|sus|o|Δ|^|°|ø|q)(6/9|6/11|6/13|[0-9]{1,2})?';

export const CHORD_TYPE_ALTERATIONS_TOKEN = '(add)?(b|#)?[0-9]{1,2}';

Expand Down Expand Up @@ -68,6 +68,20 @@ export function getChordDegrees(chord: TChord, pitchClasses: string[]) {
return chord.intervals[i].replace('*', '');
});
}
/**
* Maps a list of pitch classes to the note name in a chord.
* @param chord - the chord
* @param pitchClasses - the notes played
* @returns string[]
*/
export function getChordNotes(chord: TChord, pitchClasses: string[]) {
return pitchClasses.map((pc: string) => {
const i = chord.notes.findIndex((note) => Note.chroma(note) === Note.chroma(pc));
if (i < 0) return '';

return chord.notes[i];
});
}

export function removeIntervalWildcards(intervals: string[]) {
return intervals.map((interval) => interval.replace('*', ''));
Expand All @@ -80,4 +94,8 @@ export function overrideDictionary() {
);
}

export function getChordTypes() {
return ChordType.all();
}

overrideDictionary();
2 changes: 1 addition & 1 deletion src/renderer/helpers/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type KeySignatureConfig = {
scale: string[];
};

export const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
export const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];

const REGEX_FLAT = /b/g;
const REGEX_SHARP = /#/g;
Expand Down
27 changes: 27 additions & 0 deletions src/renderer/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import Licenses from './views/Settings/Licenses';
import packageJSON from '../../package.json';
import icon from '../../assets/icon.svg';
import ChordDisplayNamespaceSettings from './views/Settings/ChordDisplaySettings/ChordDisplayModuleSettings';
import ChordDictionary from './views/ChordDictionary';
import ChordDictionaryDetail from './views/ChordDictionary/Detail';

const router = createHashRouter(
createRoutesFromElements(
Expand Down Expand Up @@ -108,6 +110,31 @@ const router = createHashRouter(
}
/>
</Route>
<Route
path="dictionary"
handle={{
title: 'Chord Dictionary',
icon: <Icon name="dictionary" />,
hasSettings: false,
}}
element={
<MidiMessageManagerProvider namespace="chord-dictionary" source="internal">
<ChordDictionary />
</MidiMessageManagerProvider>
}
>
<Route index element={<ChordDictionaryDetail />} />
<Route
path=":chordName"
handle={{
title: (params: Params) => (
<span style={{ textTransform: 'none' }}>{params.chordName}</span>
),
icon: <Icon name="music" />,
}}
element={<ChordDictionaryDetail />}
/>
</Route>
<Route
path="settings"
handle={{ title: 'Settings', icon: <Icon name="settings" /> }}
Expand Down
26 changes: 26 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionary.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import 'tokens';

.container {
position: relative;
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: flex-end;
align-items: center;
overflow: hidden;
}

.pitchbar {
--Sidebar_minSize: 48px;
--Button_textTransform: none;
}

.content {
height: 100%;
}

.chordbar {
--Sidebar_minSize: 200px;
--Button_textTransform: none;
}
87 changes: 87 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import classnames from 'classnames/bind';
import { Chord, Note } from 'tonal';
import { SidebarContainer } from '@la-jarre-a-son/ui';

import { useSettings } from 'renderer/contexts/Settings';
import { NOTE_NAMES, getKeySignature, getNoteInKeySignature } from 'renderer/helpers';

import ChordDictionaryChromaMenu from './ChordDictionaryChromaMenu';
import ChordDictionaryChordMenu from './ChordDictionaryChordMenu';

import styles from './ChordDictionary.module.scss';

const cx = classnames.bind(styles);

const ChordDictionary: React.FC = () => {
const { settings } = useSettings();
const { chordName } = useParams();

const navigate = useNavigate();
const { key, accidentals } = settings.notation;
const keySignature = useMemo(
() => getKeySignature(key, accidentals === 'sharp'),
[key, accidentals]
);

const [chroma, setChroma] = useState<number | null>(null);
const [chordType, setChordType] = useState<string | null>(null);

useEffect(() => {
if (chroma !== null && chordType !== null) {
const name = encodeURIComponent(
`${getNoteInKeySignature(NOTE_NAMES[chroma], keySignature.notes)}${chordType}`
);

navigate(`./${name}`);
}
return () => {};
}, [navigate, chroma, chordType, keySignature]);

useEffect(() => {
const chord = chordName ? Chord.get(chordName) : null;

if (chord && chord.tonic) {
setChroma(Note.chroma(chord.tonic) ?? null);
}

if (chord && chord.aliases[0]) {
setChordType(chord.aliases[0]);
}
}, [chordName]);

return (
<SidebarContainer
className={cx('container')}
sidebar={
<ChordDictionaryChromaMenu
keySignature={keySignature}
selected={chroma}
onSelect={setChroma}
/>
}
sidebarProps={{ className: cx('pitchbar') }}
contentProps={{ className: cx('content') }}
size="xs"
open
inset
>
<SidebarContainer
className={cx('container')}
sidebar={<ChordDictionaryChordMenu selected={chordType} onSelect={setChordType} />}
sidebarProps={{ className: cx('chordbar') }}
contentProps={{ className: cx('content') }}
size="sm"
open
inset
>
<Outlet />
</SidebarContainer>
</SidebarContainer>
);
};

ChordDictionary.defaultProps = {};

export default ChordDictionary;
Loading

0 comments on commit bbdbbe9

Please sign in to comment.