Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ development artifact builds use `vMAJOR.MINOR.PATCH-dev.N`.

## Unreleased

- Added Backspace and Delete support for clearing selected Notation measures in the selected part while preserving harmony symbols and leaving default whole-measure rests.
- Added fully offline per-stem Audio-to-MIDI transcription with a bundled Basic Pitch model, native C++ inference, cancellable track progress, polyphonic Notation/MIDI notes, and project persistence. Basic Pitch is unavailable for Drum stems, and re-transcription now warns before replacing existing stem notes and rests.
- Added inline flat, natural, and sharp signs to Notation with Leland glyphs, one-shot note entry, selected tied-note editing, compact duration and accidental track menus, keyboard shortcuts, persistence, and MusicXML export.
- Added automatic rhythmic beaming for eighth and sixteenth notes in supported simple and compound Notation meters, including shared stem direction, sloped beams, secondary beam breaks, and beamlets.
Expand Down
22 changes: 22 additions & 0 deletions JammLab/ViewModels/AudioPlayerViewModel+Notation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,28 @@ extension AudioPlayerViewModel {
return changeSelectedNotationNotePitch(to: pitch)
}

@discardableResult
func deleteSelectedNotationMeasureContents() -> Bool {
guard let measures = validatedSelectedNotationMeasures() else { return false }
let partID = selectedNotationMeasures.first?.partID ?? .main
let belongsToSelection: (NotationMeasureItem) -> Bool = { item in
item.partID == partID
&& measures.contains { measure in
item.measureNumber == measure.number
&& abs(item.measureStartTime - measure.startTime)
< NotationMeasureTiming.timelineTolerance
}
}
guard notationItems.contains(where: belongsToSelection) else { return false }

performUndoableEdit("Delete Measure Contents") {
notationItems.removeAll(where: belongsToSelection)
sanitizeNotationTieRelationships()
}

return true
}

@discardableResult
func deleteSelectedNotationNote() -> Bool {
replaceSelectedNotationNoteWithRest(
Expand Down
1 change: 1 addition & 0 deletions JammLab/Views/MainWorkspacePanels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ extension ContentView {
changeNotationClef: { viewModel.setNotationClef($1, for: $0) },
selectDrumInstrument: { viewModel.selectDrumInstrument(midiNoteNumber: $0) },
auditionNotePitch: { viewModel.auditionNotationNotePitch($0, clef: $1) },
deleteSelectedNotationMeasureContents: { viewModel.deleteSelectedNotationMeasureContents() },
deleteSelectedNotationNote: { viewModel.deleteSelectedNotationNote() },
showNotationWindow: { openWindow(id: AppWindowID.notation) },
beginNotationNoteEdit: { viewModel.beginNotationNoteEdit(partID: $0) },
Expand Down
14 changes: 12 additions & 2 deletions JammLab/Views/NotationTrackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct NotationTrackActions {
var changeSelectedNotePitch: (NotationPitch, Bool) -> Bool
var changeClef: (NotationPartID, Clef) -> Void
var auditionNotePitch: (NotationPitch, Clef) -> Void
var deleteSelectedNotationMeasureContents: () -> Bool
var deleteSelectedNotationNote: () -> Bool
var locatePlaybackMarkerExactly: (TimeInterval) -> Void
var saveHarmony: (HarmonySymbol) -> Void
Expand Down Expand Up @@ -157,7 +158,7 @@ struct NotationTrackView: View {
.focused($isTrackFocused)
.focusEffectDisabled(true)
.onDeleteCommand {
deleteSelectedNotationItemOrHarmony()
deleteSelectedNotationSelectionOrHarmony()
}
.onChange(of: pendingEditorRequest?.id) { _, _ in
if partID.isMain {
Expand Down Expand Up @@ -1956,7 +1957,15 @@ struct NotationTrackView: View {
editingDraft = nil
}

private func deleteSelectedNotationItemOrHarmony() {
private func deleteSelectedNotationSelectionOrHarmony() {
if selectedMeasures.contains(where: { $0.partID == partID }) {
if actions.deleteSelectedNotationMeasureContents() {
editingDraft = nil
draggedNotePitchPreview = nil
}
return
}

if actions.deleteSelectedNotationNote() {
editingDraft = nil
draggedNotePitchPreview = nil
Expand Down Expand Up @@ -2295,6 +2304,7 @@ private extension NotationTrackActions {
changeSelectedNotePitch: { _, _ in false },
changeClef: { _, _ in },
auditionNotePitch: { _, _ in },
deleteSelectedNotationMeasureContents: { false },
deleteSelectedNotationNote: { false },
locatePlaybackMarkerExactly: { _ in },
saveHarmony: { _ in },
Expand Down
1 change: 1 addition & 0 deletions JammLab/Views/NotationWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ struct NotationWindowView: View {
changeSelectedNotePitch: { viewModel.changeSelectedNotationNotePitch(to: $0, shouldAudition: $1) },
changeClef: { viewModel.setNotationClef($1, for: $0) },
auditionNotePitch: { viewModel.auditionNotationNotePitch($0, clef: $1) },
deleteSelectedNotationMeasureContents: { viewModel.deleteSelectedNotationMeasureContents() },
deleteSelectedNotationNote: { viewModel.deleteSelectedNotationNote() },
locatePlaybackMarkerExactly: { viewModel.locatePlaybackMarkerExactly(to: $0) },
saveHarmony: { viewModel.saveHarmonySymbol($0) },
Expand Down
2 changes: 2 additions & 0 deletions JammLab/Views/WaveformTimelineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct TimelineViewActions {
var changeNotationClef: (NotationPartID, Clef) -> Void
var selectDrumInstrument: (Int) -> Void = { _ in }
var auditionNotePitch: (NotationPitch, Clef) -> Void
var deleteSelectedNotationMeasureContents: () -> Bool = { false }
var deleteSelectedNotationNote: () -> Bool
var showNotationWindow: () -> Void
var beginNotationNoteEdit: (NotationPartID) -> Void = { _ in }
Expand Down Expand Up @@ -133,6 +134,7 @@ extension TimelineViewActions {
changeSelectedNotePitch: changeSelectedNotePitch,
changeClef: changeNotationClef,
auditionNotePitch: auditionNotePitch,
deleteSelectedNotationMeasureContents: deleteSelectedNotationMeasureContents,
deleteSelectedNotationNote: deleteSelectedNotationNote,
locatePlaybackMarkerExactly: locatePlaybackMarkerExactly,
saveHarmony: saveHarmonyAction,
Expand Down
8 changes: 8 additions & 0 deletions JammLabTests/NotationTrackLayoutItemsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ final class NotationTrackLayoutItemsTests: XCTestCase {
XCTAssertTrue(stemActions.insertNotationNote(notePlacement))
XCTAssertTrue(mainActions.insertNotationRest(restPlacement))
XCTAssertTrue(stemActions.insertNotationRest(restPlacement))
XCTAssertTrue(mainActions.deleteSelectedNotationMeasureContents())
XCTAssertTrue(stemActions.deleteSelectedNotationMeasureContents())
XCTAssertTrue(mainActions.deleteSelectedNotationNote())
XCTAssertTrue(stemActions.deleteSelectedNotationNote())
mainActions.changeClef(.main, .bass)
Expand All @@ -145,6 +147,7 @@ final class NotationTrackLayoutItemsTests: XCTestCase {
XCTAssertEqual(recorder.itemSelectionCount, 2)
XCTAssertEqual(recorder.playbackLocationCount, 2)
XCTAssertEqual(recorder.notationInsertionCount, 4)
XCTAssertEqual(recorder.deleteMeasureContentsCount, 2)
XCTAssertEqual(recorder.deleteNotationCount, 2)
XCTAssertEqual(recorder.clefChanges.map(\.0), [.main, .stem(.bass)])
XCTAssertEqual(recorder.clefChanges.map(\.1), [.bass, .bass])
Expand Down Expand Up @@ -767,6 +770,7 @@ private final class TimelineNotationActionRecorder {
var itemSelectionCount = 0
var playbackLocationCount = 0
var notationInsertionCount = 0
var deleteMeasureContentsCount = 0
var deleteNotationCount = 0
var clefChanges: [(NotationPartID, Clef)] = []
}
Expand Down Expand Up @@ -823,6 +827,10 @@ private func timelineViewActions(
changeSelectedNotePitch: { _, _ in true },
changeNotationClef: { recorder.clefChanges.append(($0, $1)) },
auditionNotePitch: { _, _ in },
deleteSelectedNotationMeasureContents: {
recorder.deleteMeasureContentsCount += 1
return true
},
deleteSelectedNotationNote: {
recorder.deleteNotationCount += 1
return true
Expand Down
Loading