diff --git a/src/editor/editor.js b/src/editor/editor.js index c2f1419..5b22198 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -1,11 +1,11 @@ -import { Song, Note, Chord, MeterChange, KeyChange } from "../song/song.js" +import { Song, Note, SongChord, MeterChange, KeyChange } from "../song/song.js" import { EditorMarkers } from "./editorMarkers.js" import { EditorNotes } from "./editorNotes.js" import { EditorChords } from "./editorChords.js" import { Rational } from "../util/rational.js" import { Range } from "../util/range.js" import { Rect } from "../util/rect.js" -import { Key, scales } from "../util/theory.js" +import { Key, scales, Chord } from "../util/theory.js" export class Editor @@ -35,16 +35,16 @@ export class Editor .upsertNote(new Note(new Range(new Rational(6, 4), new Rational(7, 4)), 71)) .upsertNote(new Note(new Range(new Rational(7, 4), new Rational(8, 4)), 72)) - .upsertChord(new Chord(new Range(new Rational(0, 4), new Rational(3, 4)), 0, 0, 0)) - .upsertChord(new Chord(new Range(new Rational(4, 4), new Rational(7, 4)), 9, 0, 2)) - .upsertChord(new Chord(new Range(new Rational(8, 4), new Rational(13, 4)), 2, 0, 3)) + .upsertChord(new SongChord(new Range(new Rational(0, 4), new Rational(3, 4)), new Chord(0, 0, 0))) + .upsertChord(new SongChord(new Range(new Rational(4, 4), new Rational(7, 4)), new Chord(9, 0, 2))) + .upsertChord(new SongChord(new Range(new Rational(8, 4), new Rational(13, 4)), new Chord(2, 0, 3))) .upsertMeterChange(new MeterChange(new Rational(0, 4), 4, 4)) .upsertMeterChange(new MeterChange(new Rational(11, 4), 5, 4)) .upsertKeyChange(new KeyChange(new Rational(0, 4), new Key(0, 0, scales.major.pitches))) .upsertKeyChange(new KeyChange(new Rational(7, 4), new Key(5, 1, scales.minor.pitches))) - .upsertKeyChange(new KeyChange(new Rational(9, 4), new Key(7, -1, scales.dorian.pitches))) + .upsertKeyChange(new KeyChange(new Rational(9, 4), new Key(7, -1, scales.doubleHarmonic.pitches))) this.timeScale = 200 this.timeScroll = 0 @@ -60,6 +60,7 @@ export class Editor this.cursorTime = new Range(new Rational(0), new Rational(0)) this.cursorTrack = { start: 0, end: 0 } this.cursorShow = true + this.insertionDuration = new Rational(1, 4) this.mouseDown = false this.mouseDownData = { pos: { x: -1, y: -1 }, time: new Rational(0) } @@ -93,6 +94,46 @@ export class Editor } + insertNoteAtCursor(pitch) + { + const time = this.cursorTime.start.min(this.cursorTime.end) + const duration = this.insertionDuration + + const id = this.song.nextId + this.song = this.song.upsertNote(new Note(Range.fromStartDuration(time, duration), 60 + pitch)) + + this.cursorTime = Range.fromPoint(time.add(duration)) + this.cursorTrack = { start: 1, end: 1 } + this.cursorShow = false + + this.selectionClear() + this.selection.add(id) + + this.toolboxRefreshFn() + this.draw() + } + + + insertChordAtCursor(chord) + { + const time = this.cursorTime.start.min(this.cursorTime.end) + const duration = this.insertionDuration + + const id = this.song.nextId + this.song = this.song.upsertChord(new SongChord(Range.fromStartDuration(time, duration), chord)) + + this.cursorTime = Range.fromPoint(time.add(duration)) + this.cursorTrack = { start: 2, end: 2 } + this.cursorShow = false + + this.selectionClear() + this.selection.add(id) + + this.toolboxRefreshFn() + this.draw() + } + + selectionClear() { this.selection.clear() diff --git a/src/editor/editorChords.js b/src/editor/editorChords.js index fc0c911..f190067 100644 --- a/src/editor/editorChords.js +++ b/src/editor/editorChords.js @@ -2,7 +2,7 @@ import { KeyChange } from "../song/song.js" import { Editor } from "./editor.js" import { Rect } from "../util/rect.js" import { Range } from "../util/range.js" -import { Key, scales, chordList, getRomanNumeralScaleDegreeStr, getScaleDegreeForPitch, getColorRotationForScale, getColorForScaleDegree } from "../util/theory.js" +import { Key, scales, chords, getRomanNumeralScaleDegreeStr, getScaleDegreeForPitch, getColorRotationForScale, getColorForScaleDegree } from "../util/theory.js" export class EditorChords @@ -172,7 +172,7 @@ export class EditorChords { const rect = this.getChordRect(chord, xStart, xEnd) - const scaleDegree = getScaleDegreeForPitch(key, chord.pitch + chord.accidental) + const scaleDegree = getScaleDegreeForPitch(key, chord.chord.rootPitch + chord.chord.rootAccidental) const scaleDegreeRotation = getColorRotationForScale(key.scalePitches) const color = getColorForScaleDegree(scaleDegree + scaleDegreeRotation) @@ -188,9 +188,9 @@ export class EditorChords this.owner.ctx.textAlign = "center" this.owner.ctx.textBaseline = "middle" - const chordData = chordList[chord.chordKind] + const chordData = chords[chord.chord.kind] - let mainStr = getRomanNumeralScaleDegreeStr(scaleDegree, chord.accidental) + let mainStr = getRomanNumeralScaleDegreeStr(scaleDegree, chord.chord.rootAccidental) if (chordData.symbol[0]) mainStr = mainStr.toLowerCase() diff --git a/src/song/song.js b/src/song/song.js index 8928a42..e5f716f 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -89,21 +89,19 @@ export class Note } -export class Chord +export class SongChord { - constructor(range, pitch, accidental, chordKind) + constructor(range, chord) { this.id = -1 this.range = range - this.pitch = pitch - this.accidental = accidental - this.chordKind = chordKind + this.chord = chord } withChanges(obj) { - return Object.assign(new Chord(this.range, this.pitch, this.accidental, this.chordKind), { id: this.id }, obj) + return Object.assign(new SongChord(this.range, this.chord), { id: this.id }, obj) } } diff --git a/src/toolbox/toolbox.js b/src/toolbox/toolbox.js index 1944c47..f8df2e0 100644 --- a/src/toolbox/toolbox.js +++ b/src/toolbox/toolbox.js @@ -1,23 +1,45 @@ import React from "react" import { KeyChange } from "../song/song.js" -import { Key, scales, getScaleDegreeForPitch, getPitchForScaleDegree, getNameForPitch, getColorForScaleDegree, getColorRotationForScale, getChordRecordFromPitches, getRomanNumeralScaleDegreeStr } from "../util/theory.js" +import { Key, scales, Chord, chords, getScaleDegreeForPitch, getPitchForScaleDegree, getNameForPitch, getColorForScaleDegree, getColorRotationForScale, getChordKindFromPitches, getRomanNumeralScaleDegreeStr } from "../util/theory.js" import { Rational } from "../util/rational.js" +function LengthToolbox(props) +{ + const LengthButton = (props) => + { + return + + + } + + return null + + return
+ Length: + + + +
+} + + function NoteToolbox(props) { return
- Key: { props.songKey.getName() } + + { props.songKey.getName() }
{ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(pitch => { - const degree = getScaleDegreeForPitch(props.songKey, pitch + props.songKey.tonicPitch + props.songKey.tonicAccidental) + const finalPitch = pitch + props.songKey.tonicPitch + props.songKey.tonicAccidental + const degree = getScaleDegreeForPitch(props.songKey, finalPitch) const inKey = Math.floor(degree) == degree - const noteName = getNameForPitch(props.songKey, pitch + props.songKey.tonicPitch + props.songKey.tonicAccidental) + const noteName = getNameForPitch(props.songKey, finalPitch) const colorRotation = getColorRotationForScale(props.songKey.scalePitches) const color = getColorForScaleDegree(colorRotation + degree) - return } @@ -29,31 +51,185 @@ function NoteToolbox(props) function ChordToolbox(props) { - return
- Key: { props.songKey.getName() } -
+ const [allChordsKind, setAllChordsKind] = React.useState(-1) + const [allChordsAccidental, setAllChordsAccidental] = React.useState(0) + const [inKeyChordType, setInKeyChordType] = React.useState(0) + const [sus2, setSus2] = React.useState(false) + const [sus4, setSus4] = React.useState(false) + const [add9, setAdd9] = React.useState(false) + const [add11, setAdd11] = React.useState(false) + const [add13, setAdd13] = React.useState(false) + const [no3, setNo3] = React.useState(false) + const [no5, setNo5] = React.useState(false) + + const ChordButton = (props2) => + { + const color = props2.chord.getColor(props.songKey) + const baseStr = props2.chord.getNameBase(props.songKey) + const supStr = props2.chord.getNameSup(props.songKey) + const subStr = props2.chord.getNameSub(props.songKey) + + return + } - { [0, 1, 2, 3, 4, 5, 6].map(degree => + const TabButton = (props2) => + { + return props2.onClick(props2.value) } style={{ minWidth:"2em", display:"inline-block", fontFamily:"Verdana", fontSize:"15px", backgroundColor:(props2.value == props2.variable ? "#ddd" : "#fff"), padding:"0.25em", borderRadius:"0.25em", marginRight:"0.5em", cursor:"pointer" }}> + { props2.children } + + } + + const AllChordsMenu = () => + { + return + } + + const AllChordsMatrix = () => + { + let groupIndex = 0 + + return
+ { + chords.map((chord, index) => + { + if (chord.startGroup) + groupIndex = 1 - let baseStr = getRomanNumeralScaleDegreeStr(degree, 0) - if (chordKind.symbol[0]) - baseStr = baseStr.toLowerCase() + return + { (groupIndex++) % 9 == 0 || chord.startGroup ?
: null } + + { (chord.symbol[0] ? "i" : "I") + chord.symbol[1] }{ chord.symbol[2] } + +
+ }) + } +
+ } - baseStr += chordKind.symbol[1] + let modifiers = {} + if (sus2) modifiers.sus2 = true + if (sus4) modifiers.sus4 = true + if (add9) modifiers.add9 = true + if (add11) modifiers.add11 = true + if (add13) modifiers.add13 = true + if (no3) modifiers.no3 = true + if (no5) modifiers.no5 = true + + let chordButtons = null + + switch (allChordsKind) + { + case -1: + chordButtons = [0, 1, 2, 3, 4, 5, 6].map(degree => + { + const root = getPitchForScaleDegree(props.songKey, degree) - return - } - )} + let pitches = [0] + pitches.push(getPitchForScaleDegree(props.songKey, degree + 2) - root) + pitches.push(getPitchForScaleDegree(props.songKey, degree + 4) - root) + + if (inKeyChordType >= 1) + pitches.push(getPitchForScaleDegree(props.songKey, degree + 6) - root) + + if (inKeyChordType >= 2) + pitches.push(getPitchForScaleDegree(props.songKey, degree + 8) - root) + + if (inKeyChordType >= 3) + pitches.push(getPitchForScaleDegree(props.songKey, degree + 10) - root) + + if (inKeyChordType >= 4) + pitches.push(getPitchForScaleDegree(props.songKey, degree + 12) - root) + + const kind = getChordKindFromPitches(pitches) + + return + }) + break + + default: + chordButtons = + + + +
+ { [0, 1, 2, 3, 4, 5, 6].map(degree => { + const kind = getChordKindFromPitches(chords[allChordsKind].pitches) + const root = getPitchForScaleDegree(props.songKey, degree) + + return + })} +
+ break + } + const Checkbox = (props2) => + { + return props2.onClick(ev.target.checked) }/> + } + + const RadioButton = (props2) => + { + return props2.onClick(props2.value) }/> + } + + return
+ + + + + + + + + + + + +
+ + + { props.songKey.getName() } +
+ + + + { allChordsKind >= 0 ? null : + + } + +
+ + + + + +
+ + + + + + + +
+
+ { chordButtons } +
} @@ -62,8 +238,17 @@ export default function Toolbox(props) { const keyChange = props.editor.song.keyChanges.findActiveAt(props.editor.cursorTime.start) || new KeyChange(new Rational(0), new Key(0, 0, scales.major.pitches)) + const onSelectNote = (pitch) => props.editor.insertNoteAtCursor(pitch) + const onSelectChord = (chord) => props.editor.insertChordAtCursor(chord) + + const callbacks = + { + onSelectNote, + onSelectChord, + } + return
- { props.editor.cursorTrack.start != 1 ? null : } - { props.editor.cursorTrack.start != 2 ? null : } + { props.editor.cursorTrack.start != 1 ? null : } + { props.editor.cursorTrack.start != 2 ? null : }
} \ No newline at end of file diff --git a/src/util/theory.js b/src/util/theory.js index 8e8ae3e..6543af7 100644 --- a/src/util/theory.js +++ b/src/util/theory.js @@ -13,52 +13,45 @@ export const scales = mixolydian: { pitches: [0, 2, 4, 5, 7, 9, 10], mode: 4, name: "Mixolydian" }, minor: { pitches: [0, 2, 3, 5, 7, 8, 10], mode: 5, name: "Natural Minor" }, locrian: { pitches: [0, 1, 3, 5, 6, 8, 10], mode: 6, name: "Locrian" }, + + doubleHarmonic: { pitches: [0, 1, 4, 5, 7, 8, 11], mode: 0, name: "Double Harmonic Major" }, } export const chords = -{ - // `symbol` = [isLowercase, complement, superscriptComplement] - - major: { pitches: [0, 4, 7], code: "", symbol: [false, "", null], name: "Major", startGroup: "Triads" }, - minor: { pitches: [0, 3, 7], code: "m", symbol: [true, "", null], name: "Minor" }, - augmented: { pitches: [0, 4, 8], code: "+", symbol: [false, "", "+"], name: "Augmented" }, - diminished: { pitches: [0, 3, 6], code: "o", symbol: [true, "", "o"], name: "Diminished" }, - - power: { pitches: [0, 0, 7, 12], code: "5", symbol: [false, "", "5"], name: "Power" }, - - major6: { pitches: [0, 4, 7, 9], code: "6", symbol: [false, "", "6"], name: "Major Sixth", startGroup: "Sixths" }, - minor6: { pitches: [0, 3, 7, 9], code: "m6", symbol: [true, "", "6"], name: "Minor Sixth" }, - - dominant7: { pitches: [0, 4, 7, 10], code: "7", symbol: [false, "", "7"], name: "Dominant Seventh", startGroup: "Sevenths" }, - major7: { pitches: [0, 4, 7, 11], code: "maj7", symbol: [false, "", "M7"], name: "Major Seventh" }, - minor7: { pitches: [0, 3, 7, 10], code: "m7", symbol: [true, "", "7"], name: "Minor Seventh" }, - minorMajor7: { pitches: [0, 3, 7, 11], code: "mmaj7", symbol: [true, "", "M7"], name: "Minor-Major Seventh" }, - augmented7: { pitches: [0, 4, 8, 10], code: "+7", symbol: [false, "+", "7"], name: "Augmented Seventh" }, - augmentedMajor7: { pitches: [0, 4, 8, 11], code: "+maj7", symbol: [false, "+", "M7"], name: "Augmented Major Seventh" }, - diminished7: { pitches: [0, 3, 6, 9], code: "o7", symbol: [true, "", "o7"], name: "Diminished Seventh" }, - halfDiminished7: { pitches: [0, 3, 6, 10], code: "%7", symbol: [true, "", "ø7"], name: "Half-Diminished Seventh" }, - - dominant9: { pitches: [0, 4, 7, 10, 14], code: "9", symbol: [false, "", "9"], name: "Dominant Ninth", startGroup: "Ninths" }, - major9: { pitches: [0, 4, 7, 11, 14], code: "maj9", symbol: [false, "", "M9"], name: "Major Ninth" }, - minor9: { pitches: [0, 3, 7, 10, 14], code: "m9", symbol: [true, "", "9"], name: "Minor Ninth" }, - minorMajor9: { pitches: [0, 3, 7, 11, 14], code: "mmaj9", symbol: [true, "", "M9"], name: "Minor-Major Ninth" }, - augmented9: { pitches: [0, 4, 8, 10, 14], code: "+9", symbol: [false, "+", "9"], name: "Augmented Ninth" }, - augmentedMajor9: { pitches: [0, 4, 8, 11, 14], code: "+maj9", symbol: [false, "+", "M9"], name: "Augmented Major Ninth" }, - diminished9: { pitches: [0, 3, 6, 9, 14], code: "o9", symbol: [true, "", "o9"], name: "Diminished Ninth" }, - diminishedMinor9: { pitches: [0, 3, 6, 9, 13], code: "ob9", symbol: [true, "", "o♭9"], name: "Diminished Minor Ninth" }, - halfDiminished9: { pitches: [0, 3, 6, 10, 14], code: "%9", symbol: [true, "", "ø9"], name: "Half-Diminished Ninth" }, - halfDiminishedMinor9: { pitches: [0, 3, 6, 10, 13], code: "%b9", symbol: [true, "", "ø♭9"], name: "Half-Diminished Minor Ninth" }, -} - - -export const chordList = [ - chords.major, chords.minor, chords.augmented, chords.diminished, chords.power, chords.major6, chords.minor6, - chords.dominant7, chords.major7, chords.minor7, chords.minorMajor7, chords.augmented7, chords.augmentedMajor7, - chords.diminished7, chords.halfDiminished7, - chords.dominant9, chords.major9, chords.minor9, chords.minorMajor9, chords.augmented9, chords.augmentedMajor9, - chords.diminished9, chords.diminishedMinor9, chords.halfDiminished9, chords.halfDiminishedMinor9, + { pitches: [0, 4, 7], code: "", symbol: [false, "", null], name: "Major", startGroup: "Triads" }, + { pitches: [0, 3, 7], code: "m", symbol: [true, "", null], name: "Minor" }, + { pitches: [0, 4, 8], code: "+", symbol: [false, "", "+"], name: "Augmented" }, + { pitches: [0, 3, 6], code: "o", symbol: [true, "", "o"], name: "Diminished" }, + { pitches: [0, 2, 6], code: "oo", symbol: [true, "", "oo"], name: "Doubly-Diminished" }, + { pitches: [0, 4, 6], code: "b5", symbol: [false, "", "(b5)"], name: "Flat-Fifth" }, + + { pitches: [0, 0, 7, 12], code: "5", symbol: [false, "", "5"], name: "Power" }, + + { pitches: [0, 4, 7, 9], code: "6", symbol: [false, "", "6"], name: "Major Sixth", startGroup: "Sixths" }, + { pitches: [0, 3, 7, 9], code: "m6", symbol: [true, "", "6"], name: "Minor Sixth" }, + + { pitches: [0, 4, 7, 10], code: "7", symbol: [false, "", "7"], name: "Dominant Seventh", startGroup: "Sevenths" }, + { pitches: [0, 4, 7, 11], code: "maj7", symbol: [false, "", "M7"], name: "Major Seventh" }, + { pitches: [0, 3, 7, 10], code: "m7", symbol: [true, "", "7"], name: "Minor Seventh" }, + { pitches: [0, 3, 7, 11], code: "mmaj7", symbol: [true, "", "M7"], name: "Minor-Major Seventh" }, + { pitches: [0, 4, 8, 10], code: "+7", symbol: [false, "+", "7"], name: "Augmented Seventh" }, + { pitches: [0, 4, 8, 11], code: "+maj7", symbol: [false, "+", "M7"], name: "Augmented Major Seventh" }, + { pitches: [0, 3, 6, 9], code: "o7", symbol: [true, "", "o7"], name: "Diminished Seventh" }, + { pitches: [0, 3, 6, 10], code: "%7", symbol: [true, "", "ø7"], name: "Half-Diminished Seventh" }, + + { pitches: [0, 4, 7, 10, 14], code: "9", symbol: [false, "", "9"], name: "Dominant Ninth", startGroup: "Ninths" }, + { pitches: [0, 4, 7, 11, 14], code: "maj9", symbol: [false, "", "M9"], name: "Major Ninth" }, + { pitches: [0, 3, 7, 10, 14], code: "m9", symbol: [true, "", "9"], name: "Minor Ninth" }, + { pitches: [0, 3, 7, 11, 14], code: "mmaj9", symbol: [true, "", "M9"], name: "Minor-Major Ninth" }, + { pitches: [0, 3, 7, 10, 13], code: "9?", symbol: [true, "", "9?"], name: "???" }, + { pitches: [0, 4, 8, 10, 14], code: "+9", symbol: [false, "+", "9"], name: "Augmented Ninth" }, + { pitches: [0, 4, 8, 11, 14], code: "+maj9", symbol: [false, "+", "M9"], name: "Augmented Major Ninth" }, + { pitches: [0, 3, 6, 9, 14], code: "o9", symbol: [true, "", "o9"], name: "Diminished Ninth" }, + { pitches: [0, 3, 6, 9, 13], code: "ob9", symbol: [true, "", "o♭9"], name: "Diminished Minor Ninth" }, + { pitches: [0, 3, 6, 10, 14], code: "%9", symbol: [true, "", "ø9"], name: "Half-Diminished Ninth" }, + { pitches: [0, 3, 6, 10, 13], code: "%b9", symbol: [true, "", "ø♭9"], name: "Half-Diminished Minor Ninth" }, ] @@ -82,6 +75,16 @@ export function getChordRecordFromPitches(pitches) } +export function getChordKindFromPitches(pitches) +{ + return Object.values(chords).findIndex(chord => + ( + chord.pitches.length == pitches.length && + chord.pitches.every((p, index) => p == pitches[index]) + )) +} + + export function getAbsolutePitchStr(pitch, accidental) { const baseLabels = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6] @@ -240,4 +243,87 @@ export class Key return getAbsolutePitchStr(this.tonicPitch, this.tonicAccidental) + " " + scaleRecord.name } +} + + +export class Chord +{ + constructor(rootPitch, rootAccidental, kind, modifiers = {}) + { + this.rootPitch = rootPitch + this.rootAccidental = rootAccidental + this.kind = kind + this.modifiers = modifiers + } + + + getNameBase(key) + { + const degree = getScaleDegreeForPitch(key, this.rootPitch) + const chordKind = chords[this.kind] || { symbol: [false, "", "?"] } + + let baseStr = getRomanNumeralScaleDegreeStr(degree, this.rootAccidental) + if (chordKind.symbol[0]) + baseStr = baseStr.toLowerCase() + + return baseStr + chordKind.symbol[1] + } + + + getNameSup(key) + { + const chordKind = chords[this.kind] || { symbol: [false, "", "?"] } + + let supStr = chordKind.symbol[2] || "" + + if (this.modifiers) + { + if (this.modifiers.add9) + supStr += "(add9)" + + if (this.modifiers.add11) + supStr += "(add11)" + + if (this.modifiers.add13) + supStr += "(add13)" + + if (this.modifiers.no3) + supStr += "(no3)" + + if (this.modifiers.no5) + supStr += "(no5)" + } + + return supStr + } + + + getNameSub(key) + { + let subStr = "" + + if (this.modifiers) + { + if (this.modifiers.sus2) + { + if (this.modifiers.sus4) + subStr += "sus24" + else + subStr += "sus2" + } + else if (this.modifiers.sus4) + subStr += "sus4" + } + + return subStr + } + + + getColor(key) + { + const degree = getScaleDegreeForPitch(key, this.rootPitch) + const colorRotation = getColorRotationForScale(key.scalePitches) + const color = getColorForScaleDegree(colorRotation + degree) + return color + } } \ No newline at end of file diff --git a/watch.bat b/watch.bat new file mode 100644 index 0000000..7946708 --- /dev/null +++ b/watch.bat @@ -0,0 +1 @@ +npm run watch \ No newline at end of file