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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The project is an offline-first MVP. Keep the app local-only: no server, no paid
- Temporary loop range is separate from saved Regions. Editing a temporary loop must not mutate a saved Region. Editing a saved Region must not silently move the temporary loop unless the user explicitly activates that Region as loop.
- Waveform, tempo track, region track, notes, loop markers, beat grid, and playhead must share one visible time range. Use `TimelineViewport` for `time <-> pixel`, zoom, pan, bounds, and intersections.
- The Notation track and Notation window must share score generation, harmony editing actions, selection, and the `AudioPlayerViewModel` playback clock. The window may lay out the full score in multiple systems, but it must not create a second notation storage model or a second playback source of truth.
- Future Notation features must export through the shared Notation export mechanism so MusicXML and later formats stay in sync with the app's Notation model.
- Heavy audio work must not run on the main thread. Analysis and peakform generation should stay async/background.
- Do not use system macOS alert/beep sounds for the metronome. Use the app's metronome sound abstraction.
- Keep hotkeys centralized in `AppHotkey`; Help > Keyboard Shortcuts is generated from it.
Expand Down
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 MusicXML export for Notation from the File menu and Notation window.
- Added a collapsible Notation track in the main timeline, collapsed by default and saved per project.
- Changed notation harmony entry so beat slashes are selectable and Cmd+K opens chord input for the selected beat.
- Added notation measure range selection with Shift-click, Cmd+C/Cmd+V measure copy and replace-paste for harmony contents, and Esc to clear the selection.
Expand Down
8 changes: 8 additions & 0 deletions JammLab.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
9FCB01062D80000100112233 /* NotationMeasureLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCB00062D80000100112233 /* NotationMeasureLayout.swift */; };
9FEA01052D70000100112233 /* NotationVisibleMeasureFitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA00052D70000100112233 /* NotationVisibleMeasureFitter.swift */; };
9FEA01062D70000100112233 /* ProjectKeySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEA00062D70000100112233 /* ProjectKeySelection.swift */; };
9FEE01012D90000100112233 /* NotationExportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE00012D90000100112233 /* NotationExportService.swift */; };
9FEE01022D90000100112233 /* AudioPlayerViewModel+NotationExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE00022D90000100112233 /* AudioPlayerViewModel+NotationExport.swift */; };
9FCB01072D80000100112233 /* NotationHarmonyInlineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCB00072D80000100112233 /* NotationHarmonyInlineTextField.swift */; };
9FEC01012D71000100112233 /* NotationWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC00012D71000100112233 /* NotationWindowView.swift */; };
9F8B01062C10000100112233 /* TestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B00072C10000100112233 /* TestSupport.swift */; };
Expand Down Expand Up @@ -212,6 +214,8 @@
9FCB00062D80000100112233 /* NotationMeasureLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationMeasureLayout.swift; sourceTree = "<group>"; };
9FEA00052D70000100112233 /* NotationVisibleMeasureFitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationVisibleMeasureFitter.swift; sourceTree = "<group>"; };
9FEA00062D70000100112233 /* ProjectKeySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectKeySelection.swift; sourceTree = "<group>"; };
9FEE00012D90000100112233 /* NotationExportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationExportService.swift; sourceTree = "<group>"; };
9FEE00022D90000100112233 /* AudioPlayerViewModel+NotationExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioPlayerViewModel+NotationExport.swift"; sourceTree = "<group>"; };
9FCB00072D80000100112233 /* NotationHarmonyInlineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationHarmonyInlineTextField.swift; sourceTree = "<group>"; };
9FEC00012D71000100112233 /* NotationWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationWindowView.swift; sourceTree = "<group>"; };
9F8B00062C10000100112233 /* JammLabTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JammLabTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -394,6 +398,7 @@
9F8A00172C00000100112233 /* BeatGridCalculator.swift */,
9F8A00182C00000100112233 /* MetronomeClickScheduler.swift */,
9FCB00062D80000100112233 /* NotationMeasureLayout.swift */,
9FEE00012D90000100112233 /* NotationExportService.swift */,
9FEA00032D70000100112233 /* NotationViewportFactory.swift */,
9FEA00052D70000100112233 /* NotationVisibleMeasureFitter.swift */,
9F8A00192C00000100112233 /* ProjectArtifactStore.swift */,
Expand Down Expand Up @@ -427,6 +432,7 @@
9FB201032D30000100112233 /* AudioPlayerViewModel+Stems.swift */,
9FB201042D30000100112233 /* AudioPlayerViewModel+Timeline.swift */,
9FB201052D30000100112233 /* AudioPlayerViewModel+Notes.swift */,
9FEE00022D90000100112233 /* AudioPlayerViewModel+NotationExport.swift */,
9FB201062D30000100112233 /* AudioPlayerViewModel+Video.swift */,
9FB201072D30000100112233 /* AudioPlayerViewModel+UndoDirty.swift */,
);
Expand Down Expand Up @@ -753,6 +759,7 @@
9FB200032D30000100112233 /* AudioPlayerViewModel+Stems.swift in Sources */,
9FB200042D30000100112233 /* AudioPlayerViewModel+Timeline.swift in Sources */,
9FB200052D30000100112233 /* AudioPlayerViewModel+Notes.swift in Sources */,
9FEE01022D90000100112233 /* AudioPlayerViewModel+NotationExport.swift in Sources */,
9FB200062D30000100112233 /* AudioPlayerViewModel+Video.swift in Sources */,
9FB200072D30000100112233 /* AudioPlayerViewModel+UndoDirty.swift in Sources */,
9F8C01032C20000100112233 /* AppControls.swift in Sources */,
Expand Down Expand Up @@ -787,6 +794,7 @@
9FEC01012D71000100112233 /* NotationWindowView.swift in Sources */,
9F8A010D2C00000100112233 /* AppHotkey.swift in Sources */,
9FCB01062D80000100112233 /* NotationMeasureLayout.swift in Sources */,
9FEE01012D90000100112233 /* NotationExportService.swift in Sources */,
9FEA01032D70000100112233 /* NotationViewportFactory.swift in Sources */,
9FEA01052D70000100112233 /* NotationVisibleMeasureFitter.swift in Sources */,
9FEA01022D70000100112233 /* NotationViewportState.swift in Sources */,
Expand Down
9 changes: 9 additions & 0 deletions JammLab/JammLabApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ struct JammLabCommands: Commands {
}
}
.keyboardShortcut("s", modifiers: [.command, .shift])

Divider()

Button("Export Notation as MusicXML...") {
Task {
await viewModel.exportNotationAsMusicXML()
}
}
.disabled(!viewModel.canExportNotation)
}

CommandGroup(replacing: .undoRedo) {
Expand Down
Loading