diff --git a/.gitignore b/.gitignore index 64023fc..3d55350 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules .vscode .parcel-cache build +.eslintcache diff --git a/shared/midi.ts b/shared/midi.ts index e09d8e1..fef05c5 100644 --- a/shared/midi.ts +++ b/shared/midi.ts @@ -1,4 +1,5 @@ import {MidiInstrumentName} from 'constants/midi_instrument_constants'; + import type easymidi from 'easymidi'; import type {Note} from 'easymidi'; @@ -74,52 +75,6 @@ const isSpammyMidiEvent = (type: string, msg: MidiMessage): boolean => { return false; }; -export const equalControlButton = (button: ControlButtonMapping | undefined, msg: MidiSubjectMessage) => { - if (!button) { - return false; - } - - const noteMsg = msg.msg as easymidi.Note; - return button.channel === noteMsg.channel && button.note === noteMsg.note; -}; - -export const equalKeyboard = (keyboard: KeyboardMapping | undefined, msg: MidiSubjectMessage) => { - if (!keyboard) { - return false; - } - - const noteMsg = msg.msg as easymidi.Note; - return keyboard.channel === noteMsg.channel; -}; - -export const isNoteOnEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { - return msg.type === 'noteon'; -}; - -export const isNoteOffEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { - return msg.type === 'noteoff'; -}; - -export const isControlChangeEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { - return msg.type === 'cc'; -}; - -export const equalChords = (chord1: Note[], chord2: Note[]) => { - const set1 = new Set(chord1); - const set2 = new Set(chord2); - - if (set1.size !== set2.size) { - return false; - } - - for (const c of set1.values()) { - if (!set2.has(c)) { - return false; - } - } - - return true; -}; export type MidiSubjectMessage = { name: MidiInstrumentName; @@ -147,15 +102,15 @@ export const equalKeyboard = (keyboard: KeyboardMapping | undefined, msg: MidiSu export const isNoteOnEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { return msg.type === 'noteon'; -} +}; export const isNoteOffEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { return msg.type === 'noteoff'; -} +}; export const isControlChangeEvent = (msg: MidiSubjectMessage): msg is MidiSubjectMessage => { return msg.type === 'cc'; -} +}; export const equalChords = (chord1: Note[], chord2: Note[]) => { const set1 = new Set(chord1); @@ -172,10 +127,4 @@ export const equalChords = (chord1: Note[], chord2: Note[]) => { } return true; -} - -export type MidiSubjectMessage = { - name: MidiInstrumentName; - type: MidiMessageType - msg: T; -} +}; diff --git a/shared/types/model-interfaces.ts b/shared/types/model-interfaces.ts new file mode 100644 index 0000000..f6e0c27 --- /dev/null +++ b/shared/types/model-interfaces.ts @@ -0,0 +1,28 @@ +export interface Note { + number: number; // i.e. 84 + name: string; // i.e. "C" + octave: number; // i.e. 5 +} + +export interface Chord { + notes: Note[]; +} + +export interface Scale { + root: Note; + quality: string; +} + +export interface Progression { + chords: Chord[]; +} + +export interface NoteCollection { + notes: Note[]; +} + +export type MidiNumber = number + +export const noteToMidiNumber = (note: Note) => { + return note.number; +}; diff --git a/webapp/package-lock.json b/webapp/package-lock.json index cba4599..b1abdfb 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "classnames": "^2.3.2", "easymidi": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2054,6 +2055,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -9453,6 +9459,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index 1db7285..c7a283f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -8,6 +8,9 @@ "dev": "snowpack dev --port 2000", "local": "LOCAL_MODE=true snowpack dev --port 2000", "build": "snowpack build", + "check": "eslint --ext .js,.jsx,.tsx,.ts ./src --quiet --cache && eslint --ext .js,.jsx,.tsx,.ts ./webapp/src --quiet --cache", + "fix": "eslint --ext .js,.jsx,.tsx,.ts ./src --quiet --fix --cache && eslint --ext .js,.jsx,.tsx,.ts ./webapp/src --quiet --fix --cache", + "check-types": "tsc -b", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -29,6 +32,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "classnames": "^2.3.2", "easymidi": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/webapp/src/components/main.tsx b/webapp/src/components/main.tsx index 644618a..e3be46f 100644 --- a/webapp/src/components/main.tsx +++ b/webapp/src/components/main.tsx @@ -6,6 +6,8 @@ import {useGlobalState} from '../hooks/use_global_state'; import ControlPanel from './control_panel/control_panel'; +import ProgressionView from './progression_view/progression_view'; + type Props = { actionHandler: ActionHandler; // localMode: boolean; @@ -35,8 +37,13 @@ export default function Main(props: Props) { // /> // ); + const progressionView = ( + + ); + return (
+ {progressionView}
                     {messages.length}
diff --git a/webapp/src/components/progression_view/chord-name.tsx b/webapp/src/components/progression_view/chord-name.tsx
new file mode 100644
index 0000000..c8c0a05
--- /dev/null
+++ b/webapp/src/components/progression_view/chord-name.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import {Note, Chord} from './model-interfaces';
+
+// import {cycle} from '../../midi_processing/chord-processor';
+
+// import styles from './progression_view.scss';
+
+const cycle = (n: number) => n % 12;
+
+const correctNoteName = (note: Note) => {
+    return note.name;
+};
+
+type Props = {
+    chord: Chord;
+}
+
+export default function ChordName(props: Props) {
+    const {chord} = props;
+
+    let label;
+    const name = correctNoteName(chord.notes[0]);
+    if (chord.notes.length === 1) {
+        label = name;
+    } else {
+        const isMinor = cycle(chord.notes[0].number + 3) === cycle(chord.notes[1].number);
+        label = isMinor ? name + 'm' : name;
+    }
+
+    return (
+        
+ + {label} + +
+ ); +} diff --git a/webapp/src/components/progression_view/chord.tsx b/webapp/src/components/progression_view/chord.tsx new file mode 100644 index 0000000..1f3db99 --- /dev/null +++ b/webapp/src/components/progression_view/chord.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import classnames from 'classnames'; + +import {Chord} from './model-interfaces'; + +// import DumbPiano from '../piano/dumb-piano'; + +import ChordName from './chord-name'; + +type Props = { + chord: Chord; +} + +export default function ChordComponent(props: Props) { + const {chord} = props; + + // const selected = Math.random() > 0.5 + // const selectedClass = selected ? styles.selectedChord : '' + + const selectedClass = ''; + + return ( +
+ {/* console.log('special')} + // noteRange={{ first: firstNote, last: lastNote }} + width={200} + // keyboardShortcuts={keyboardShortcuts} + heldDownNotes={chord.notes} + /> */} + +
+ ); +} diff --git a/webapp/src/components/progression_view/model-interfaces.ts b/webapp/src/components/progression_view/model-interfaces.ts new file mode 100644 index 0000000..f6e0c27 --- /dev/null +++ b/webapp/src/components/progression_view/model-interfaces.ts @@ -0,0 +1,28 @@ +export interface Note { + number: number; // i.e. 84 + name: string; // i.e. "C" + octave: number; // i.e. 5 +} + +export interface Chord { + notes: Note[]; +} + +export interface Scale { + root: Note; + quality: string; +} + +export interface Progression { + chords: Chord[]; +} + +export interface NoteCollection { + notes: Note[]; +} + +export type MidiNumber = number + +export const noteToMidiNumber = (note: Note) => { + return note.number; +}; diff --git a/webapp/src/components/progression_view/progression_view.scss b/webapp/src/components/progression_view/progression_view.scss new file mode 100644 index 0000000..724f0ad --- /dev/null +++ b/webapp/src/components/progression_view/progression_view.scss @@ -0,0 +1,33 @@ +.heading { + font-size: 40px; +} + +.progressionContainer { + border: 5px solid; + width: 1050px; +} + +.chordName { + font-size: 80px; + font-family:Georgia, 'Times New Roman', Times, serif + // margin-right: 30px; +} + +.entireChordContainer { + margin: 10px; + display: inline-block; + padding: 20px; +} + +.selectedChord { + background-color: #FDEFCC; +} + +.noteNameContainer { + background-color: #7EAA54; + text-align: center; + border: 20px solid; + border-color: #4D73BE; + border-radius: 30px; + padding: 10px; +} diff --git a/webapp/src/components/progression_view/progression_view.tsx b/webapp/src/components/progression_view/progression_view.tsx new file mode 100644 index 0000000..a782b30 --- /dev/null +++ b/webapp/src/components/progression_view/progression_view.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +// import {useStore, State} from 'easy-peasy'; + +import {Chord, Progression, Scale} from './model-interfaces'; +// import {IGlobalStore} from '../../store/store-types'; + +import './progression_view.scss'; + +import ChordComponent from './chord'; + +type Props = { + +} + +// const getGroupsOfChords = (chords: Chord[]) => { +// const copy = [...chords]; +// const result = []; + +// while (copy.length) { +// result.push(copy.splice(0, 4)); +// } +// return result; +// }; + +export default function ProgressionView(props: Props) { + // const progressions: Progression[] = useStore((state: State) => state.progressions.progressions); + // const scale: Scale = useStore((state: State) => state.progressions.currentScale); + + const progressions: Progression[] = [ + { + chords: [ + { + notes: [ + { + name: 'C', + number: 48, + octave: 4, + }, + ], + }, + { + notes: [ + { + name: 'D', + number: 48, + octave: 4, + }, + ], + }, + ], + }, + ]; + const scale: Scale = { + root: { + name: 'C', + number: 48, + octave: 4, + }, + quality: 'Major', + }; + + // const groups = getGroupsOfChords(chords) + + return ( + + {scale &&

{scale.root.name} {scale.quality}

} + + {progressions.length === 0 && ( +

No progression

+ )} + + {progressions.map((progression, i) => ( +
+ {progression.chords.map((chord: Chord, i: number) => ( + + ))} + {/*

+
+            {JSON.stringify(chords.map(chord => chord.notes[0].name), null, 2)}
+            
+

*/} +
+ ))} +
+ ); +}