Skip to content

Commit

Permalink
add context menu; add convert notes to chords command
Browse files Browse the repository at this point in the history
  • Loading branch information
hlorenzi committed Feb 6, 2021
1 parent 5795734 commit cc88c14
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 60 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Several soundfonts are readily available from an online repository.
* Use the middle or right mouse buttons to pan.
* Hold <kbd>A</kbd> to draw elements with the mouse.
* Right-click on an element to change its properties.
* Do a long right-click for a context menu with more commands.
* Double-click on a note block to edit its notes.
* Click on "Project Root" on the breadcrumb bar to exit note editing mode.

Expand Down
9 changes: 5 additions & 4 deletions src/project/_global.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Project from "./index"
import * as Timeline from "../timeline"
import * as GlobalObservable from "../util/globalObservable"


Expand Down Expand Up @@ -71,7 +72,7 @@ export function setNew()

clearUndoStack()
notifyObservers()
window.dispatchEvent(new Event("timelineReset"))
Timeline.sendEventReset()
}


Expand All @@ -82,7 +83,7 @@ export function open(openedProject: Project.Root)

clearUndoStack()
notifyObservers()
window.dispatchEvent(new Event("timelineReset"))
Timeline.sendEventReset()
}


Expand Down Expand Up @@ -142,7 +143,7 @@ export function undo()

saveToLocalStorageWithCooldown(global.project)
notifyObservers()
window.dispatchEvent(new Event("timelineRefresh"))
Timeline.sendEventRefresh()
}


Expand All @@ -157,7 +158,7 @@ export function redo()

saveToLocalStorageWithCooldown(global.project)
notifyObservers()
window.dispatchEvent(new Event("timelineRefresh"))
Timeline.sendEventRefresh()
}


Expand Down
21 changes: 21 additions & 0 deletions src/project/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,27 @@ export function getRelativeRange(project: Root, parentId: Project.ID, range: Ran
}


export function getRangeForElems(project: Root, elemIds: Iterable<Project.ID>): Range | null
{
let range: Range | null = null

for (const id of elemIds)
{
const elem = project.elems.get(id) as Project.Element
if (!elem)
continue

if (elem.type == "track")
continue

const absRange = Project.getAbsoluteRange(project, elem.parentId, elem.range)
range = Range.merge(range, absRange)
}

return range
}


export function getMillisecondsAt(project: Root, time: Rational): number
{
const measuresPerSecond = (project.baseBpm / 4 / 60)
Expand Down
55 changes: 55 additions & 0 deletions src/theory/chord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export const chordKinds: ChordMetadata[] =
]


export interface ChordSuggestion
{
chord: Chord
matches: number
misses: number
}


export default class Chord
{
static kinds: ChordMetadata[] = chordKinds
Expand Down Expand Up @@ -90,6 +98,53 @@ export default class Chord
}


static suggestChordsForPitches(pitches: number[]): ChordSuggestion[]
{
const suggestions: ChordSuggestion[] = []

for (let k = 0; k < chordKinds.length; k++)
{
const kind = chordKinds[k]

for (let root = 0; root < pitches.length; root++)
{
const rootPitch = pitches[root]
const matches = new Set<number>()
let misses = 0

for (let i = 0; i < pitches.length; i++)
{
const pitch = pitches[(root + i) % pitches.length]
const relPitch = MathUtils.mod(pitch - rootPitch, 12)

const kindMatch = kind.pitches.findIndex(p => p === relPitch)
if (kindMatch >= 0)
matches.add(kindMatch)
else
misses++
}

suggestions.push({
chord: new Chord(MathUtils.mod(rootPitch, 12), k, 0, []),
matches: matches.size,
misses,
})
}
}

suggestions.sort((a, b) =>
{
if (a.misses != b.misses)
return a.misses - b.misses

return b.matches - a.matches
})

//console.log("suggestions", pitches, suggestions)
return suggestions.slice(0, 10)
}


get kindId(): string
{
return chordKinds[this.kind].id
Expand Down
12 changes: 12 additions & 0 deletions src/timeline/Element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,16 @@ export function TimelineElement(props: { state?: RefState<Timeline.State> })
editorState.ref.current.tracks,
editorState.ref.current.trackScroll,
])
}


export function sendEventRefresh()
{
window.dispatchEvent(new Event("timelineRefresh"))
}


export function sendEventReset()
{
window.dispatchEvent(new Event("timelineReset"))
}
180 changes: 180 additions & 0 deletions src/timeline/action_convertNotesToChords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as Project from "../project"
import * as Timeline from "./index"
import * as Theory from "../theory"
import * as MathUtils from "../util/mathUtils"
import Rational from "../util/rational"
import Range from "../util/range"


export function convertNotesToChords(data: Timeline.WorkData, elemIds: Iterable<Project.ID>)
{
const elems: Project.Note[] = []
for (const elemId of elemIds)
{
const note = Project.getElem(Project.global.project, elemId, "note")
if (note)
elems.push(note)

const noteBlock = Project.getElem(Project.global.project, elemId, "noteBlock")
if (noteBlock)
{
const list = Project.global.project.lists.get(noteBlock.id)
if (list)
{
for (const elem of list.iterAll())
{
if (elem.type == "note")
elems.push(elem)
}
}
}
}

if (elems.length == 0)
return

const trackId = Project.global.project.chordTrackId

Timeline.selectionClear(data)

const groups = calculateGroups(data, elems)
let chordsToAdd: ChordToAdd[] = []
for (const group of groups)
{
const midiPitches = group.elems.map(elem => elem.midiPitch)
const chordSuggestions = Theory.Chord.suggestChordsForPitches(midiPitches)

if (chordSuggestions.length >= 1)
{
chordsToAdd.push({
chord: chordSuggestions[0].chord,
range: group.range,
})
}
}

chordsToAdd = coallesceNeighboringChordsToAdd(chordsToAdd)

for (const chordToAdd of chordsToAdd)
{
const projChord = Project.makeChord(
trackId,
chordToAdd.range,
chordToAdd.chord)

const id = Project.global.project.nextId
Project.global.project = Project.upsertElement(Project.global.project, projChord)

Timeline.selectionAdd(data, id)
}

Project.global.project = Project.withRefreshedRange(Project.global.project)

data.state.cursor.visible = false
Timeline.selectionRemoveConflictingBehind(data)
}


interface ChordToAdd
{
chord: Theory.Chord
range: Range
}


function coallesceNeighboringChordsToAdd(chordsToAdd: ChordToAdd[]): ChordToAdd[]
{
const newChordsToAdd: ChordToAdd[] = []

let prev = -1
for (let i = 0; i < chordsToAdd.length; i++)
{
if (prev < 0)
{
prev = newChordsToAdd.length
newChordsToAdd.push({ ...chordsToAdd[i] })
}
else
{
const prevChord = newChordsToAdd[prev].chord
const nextChord = chordsToAdd[i].chord

if (prevChord.rootChroma == nextChord.rootChroma &&
prevChord.kind == nextChord.kind)
{
newChordsToAdd[prev].range = newChordsToAdd[prev].range.merge(chordsToAdd[i].range)
}
else
{
// Fill holes?
//newChordsToAdd[prev].range = new Range(newChordsToAdd[prev].range.start, chordsToAdd[i].range.start)

prev = newChordsToAdd.length
newChordsToAdd.push({ ...chordsToAdd[i] })
}
}
}

return newChordsToAdd
}


interface Group
{
elems: Project.Note[]
range: Range
}


function calculateGroups(data: Timeline.WorkData, elems: Project.Note[]): Group[]
{
const minTime = elems.reduce<Rational | null>((accum, elem) =>
{
const start = Project.getAbsoluteTime(Project.global.project, elem.parentId, elem.range.start)
return start.min(accum)
},
null)!

const maxTime = elems.reduce<Rational | null>((accum, elem) =>
{
const end = Project.getAbsoluteTime(Project.global.project, elem.parentId, elem.range.end)
return end.max(accum)
},
null)!

const groups: Group[] = []

let curTime = minTime
while (curTime.compare(maxTime) < 0)
{
const nextTime = elems.reduce<Rational>((accum, elem) =>
{
const range = Project.getAbsoluteRange(Project.global.project, elem.parentId, elem.range)
if (range.start.compare(curTime) > 0)
return range.start.min(accum)

if (range.end.compare(curTime) > 0)
return range.end.min(accum)

return accum
},
null!) || maxTime

const groupRange = new Range(curTime, nextTime)

const groupElems = elems.filter(elem =>
{
const range = Project.getAbsoluteRange(Project.global.project, elem.parentId, elem.range)
return groupRange.overlapsRange(range)
})

groups.push({
elems: groupElems,
range: groupRange,
})

curTime = nextTime
}

return groups
}
3 changes: 2 additions & 1 deletion src/timeline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export * from "./trackChords"
export * from "./trackNotes"
export * from "./trackNoteVolumes"
export * from "./trackNoteVelocities"
export * from "./Element"
export * from "./Element"
export * from "./action_convertNotesToChords"
Loading

0 comments on commit cc88c14

Please sign in to comment.