Skip to content

Commit de2f7d1

Browse files
authored
Merge pull request #95 from Sportinger/Midi
Add browser MIDI mapping controls and stop markers
2 parents 3dfdade + eefcc19 commit de2f7d1

43 files changed

Lines changed: 2874 additions & 202 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/Features/Project-Persistence.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ interface ProjectFile {
292292

293293
### Per Composition
294294
- All tracks and clips
295+
- Timeline markers and per-marker MIDI bindings
295296
- Clip positions and durations
296297
- Trim points (inPoint/outPoint)
297298
- Transform properties (position, scale, rotation, anchor, opacity, blend mode)
@@ -323,6 +324,7 @@ interface ProjectFile {
323324
- Composition view state per composition (playhead, zoom, scroll, in/out points)
324325
- Media panel column order and name width
325326
- Transcript language preference
327+
- Global MIDI state: enabled flag and transport bindings (`Play / Pause`, `Stop`)
326328
- View toggles: thumbnails, waveforms, proxy, transcript markers
327329
- Changelog preferences (`showChangelogOnStartup`, `lastSeenChangelogVersion`)
328330
- Export panel state: live export settings, named export presets, and the selected preset

docs/Features/Timeline.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ The toolbar and wheel gestures still drive playback and navigation:
192192
- `I` and `O` set in/out points.
193193
- `X` clears in/out.
194194
- `M` adds a marker at the playhead.
195+
- Right-clicking a marker opens marker transport and MIDI actions.
196+
- Markers can be turned into `Stop Marker`s that automatically pause playback when crossed.
197+
- Marker MIDI bindings support `Jump To Marker`, `Play From Marker`, and `Jump To Marker And Stop`.
195198
- Left/Right arrows step frame by frame.
196199
- `Alt+Scroll` or `Ctrl+Scroll` zooms the timeline around the playhead.
197200
- `Shift+Scroll` pans horizontally.

docs/Features/UI-Panels.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ Dockable desktop panel system with an After Effects-style menu bar, unified clip
3333
| **Edit** | Copy, Paste, Settings |
3434
| **View** | Panels submenu, Layouts submenu |
3535
| **Output** | New Output Window, Open Output Manager, Active Outputs |
36-
| **Window** | MIDI Control |
3736
| **Info** | Where are you coming from?, Tutorials, Quick Tour, Timeline Tour, Changelog, About, Imprint, Privacy Policy, Contact |
3837

3938
### Keyboard Shortcuts
@@ -106,7 +105,7 @@ All docked panels can be:
106105

107106
## Available Panels
108107

109-
MasterSelects currently exposes 16 dockable panel types, plus the Slot Grid overlay that sits on top of the Timeline.
108+
MasterSelects currently exposes 17 dockable panel types, plus the Slot Grid overlay that sits on top of the Timeline.
110109

111110
| Panel | Type ID | Surface |
112111
|-------|---------|---------|
@@ -116,6 +115,7 @@ MasterSelects currently exposes 16 dockable panel types, plus the Slot Grid over
116115
| **Media** | `media` | Media browser, folders, and project items |
117116
| **Properties** | `clip-properties` | Unified clip inspector |
118117
| **Export** | `export` | Render and export controls |
118+
| **MIDI Mapping** | `midi-mapping` | Overview and editor for assigned MIDI notes |
119119
| **AI Chat** | `ai-chat` | Editing assistant chat |
120120
| **AI Video** | `ai-video` | Classic generator plus FlashBoard workspace |
121121
| **AI Segment** | `ai-segment` | Local SAM2 segmentation tools |
@@ -175,6 +175,17 @@ MasterSelects currently exposes 16 dockable panel types, plus the Slot Grid over
175175
- Stacked alpha export
176176
- Progress with phase display
177177

178+
### MIDI Mapping Panel
179+
180+
- Open from `View -> Panels -> MIDI Mapping`
181+
- Shows all currently assigned MIDI notes in one list
182+
- Includes both global transport bindings and per-marker bindings
183+
- Each row shows the note, target, and resulting command behavior
184+
- Existing entries can be edited directly:
185+
- Change channel and note manually
186+
- Relearn the binding from incoming MIDI
187+
- Move a marker binding to another available marker from a dropdown list
188+
178189
### AI Chat Panel
179190

180191
- GPT-backed editing assistant
@@ -384,7 +395,7 @@ Multi Preview is available from the View menu and can be floated or docked, but
384395

385396
### Enabling MIDI
386397

387-
Window menu -> MIDI Control
398+
Edit menu -> Settings -> MIDI
388399

389400
### Requirements
390401

@@ -398,6 +409,12 @@ Window menu -> MIDI Control
398409
MIDI Control (N devices)
399410
```
400411

412+
### Mapping Overview
413+
414+
- `View -> Panels -> MIDI Mapping` opens the dedicated mapping panel
415+
- The panel lists all assigned transport and marker bindings
416+
- Empty-state guidance points back to Settings and the marker right-click menu
417+
401418
---
402419

403420
## Resolution Settings
@@ -441,6 +458,7 @@ Edit menu -> Settings
441458
|----------|----------|
442459
| **Appearance** | Theme selection and custom theme controls |
443460
| **General** | Save mode, autosave interval/enable state, import copy behavior, and mobile/desktop view mode |
461+
| **MIDI** | Browser MIDI permission state, transport learning, and device list |
444462
| **Previews** | Preview resolution quality and transparency grid info |
445463
| **Import** | Copy media to project folder toggle |
446464
| **Transcription** | Provider selection and pricing |

src/App.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7623,6 +7623,12 @@ input[type="checkbox"] {
76237623
box-shadow: 0 0 8px var(--marker-color);
76247624
}
76257625

7626+
.timeline-marker.is-stop-marker .timeline-marker-head {
7627+
box-shadow:
7628+
0 0 0 1px color-mix(in srgb, var(--warning-light) 45%, transparent),
7629+
0 0 10px color-mix(in srgb, var(--warning-light) 20%, transparent);
7630+
}
7631+
76267632
.timeline-marker .timeline-marker-line {
76277633
width: 2px;
76287634
min-height: calc(100vh - 50px);
@@ -7632,6 +7638,15 @@ input[type="checkbox"] {
76327638
opacity: 0.7;
76337639
}
76347640

7641+
.timeline-marker.is-stop-marker .timeline-marker-line {
7642+
background:
7643+
repeating-linear-gradient(
7644+
to bottom,
7645+
var(--marker-color) 0 10px,
7646+
color-mix(in srgb, var(--warning-light) 42%, var(--marker-color)) 10px 16px
7647+
);
7648+
}
7649+
76357650
.timeline-marker:hover .timeline-marker-line,
76367651
.timeline-marker.dragging .timeline-marker-line {
76377652
opacity: 1;

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { useTheme } from './hooks/useTheme';
2929
import { useGlobalHistory } from './hooks/useGlobalHistory';
3030
import { useClipPanelSync } from './hooks/useClipPanelSync';
3131
import { useIsMobile, useForceMobile } from './hooks/useIsMobile';
32+
import { useMIDIRuntime } from './hooks/useMIDIRuntime';
3233
import { useAccountStore } from './stores/accountStore';
3334
import { useSettingsStore } from './stores/settingsStore';
3435
import { projectDB } from './services/projectDB';
@@ -63,6 +64,9 @@ function App() {
6364
// Auto-switch panels based on clip selection
6465
useClipPanelSync();
6566

67+
// Browser MIDI runtime
68+
useMIDIRuntime();
69+
6670
// Check if there's a stored project in IndexedDB (the only allowed browser storage)
6771
const [isChecking, setIsChecking] = useState(true);
6872
const [hasStoredProject, setHasStoredProject] = useState(false);

src/changelog-data.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
11
[
2+
{
3+
"date": "2026-04-23",
4+
"type": "new",
5+
"title": "Browser MIDI Control Added with Learn Mode and a Dedicated Mapping Panel",
6+
"description": "MasterSelects now supports direct browser MIDI input over Web MIDI with a dedicated Settings tab, transport bindings, a dockable MIDI Mapping panel, note learn mode, manual reassignment, and project-persistent marker plus transport mappings.",
7+
"section": "MIDI / Workflow",
8+
"commits": [
9+
"8e2701c3"
10+
]
11+
},
12+
{
13+
"date": "2026-04-23",
14+
"type": "improve",
15+
"title": "Timeline Markers Can Now Trigger Playback and Auto-Stop the Playhead",
16+
"description": "Markers now expose right-click MIDI actions for Jump, Play From Marker, and Jump To Marker And Stop, and any marker can be turned into a Stop Marker so playback halts automatically when the playhead crosses it.",
17+
"section": "Timeline / Markers",
18+
"commits": [
19+
"8e2701c3"
20+
]
21+
},
222
{
323
"date": "2026-04-18",
424
"type": "new",

src/components/common/SettingsDialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSettingsStore } from '../../stores/settingsStore';
55
import { useDraggableDialog } from './settings/useDraggableDialog';
66
import { AppearanceSettings } from './settings/AppearanceSettings';
77
import { GeneralSettings } from './settings/GeneralSettings';
8+
import { MidiSettings } from './settings/MidiSettings';
89
import { TranscriptionSettings } from './settings/TranscriptionSettings';
910
import { ApiKeysSettings } from './settings/ApiKeysSettings';
1011
import { NativeHelperSettings } from './settings/NativeHelperSettings';
@@ -17,6 +18,7 @@ interface SettingsDialogProps {
1718

1819
type SettingsCategory =
1920
| 'general'
21+
| 'midi'
2022
| 'shortcuts'
2123
| 'appearance'
2224
| 'transcription'
@@ -31,6 +33,7 @@ interface CategoryConfig {
3133

3234
const categories: CategoryConfig[] = [
3335
{ id: 'general', label: 'General', icon: '\u2699' },
36+
{ id: 'midi', label: 'MIDI', icon: '\u266B' },
3437
{ id: 'shortcuts', label: 'Shortcuts', icon: '\u2328' },
3538
{ id: 'appearance', label: 'Appearance', icon: '\uD83C\uDFA8' },
3639
{ id: 'transcription', label: 'Transcription', icon: '\uD83C\uDFA4' },
@@ -62,6 +65,7 @@ export function SettingsDialog({ onClose }: SettingsDialogProps) {
6265
const renderCategoryContent = () => {
6366
switch (activeCategory) {
6467
case 'general': return <GeneralSettings />;
68+
case 'midi': return <MidiSettings />;
6569
case 'shortcuts': return <ShortcutsSettings />;
6670
case 'appearance': return <AppearanceSettings />;
6771
case 'transcription': return <TranscriptionSettings localKeys={localKeys} />;

src/components/common/settings/GeneralSettings.tsx

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useCallback } from 'react';
22
import { useSettingsStore, type AutosaveInterval, type SaveMode, type PreviewQuality, type GPUPowerPreference } from '../../../stores/settingsStore';
33
// AutosaveInterval used in interval select onChange cast
44
import { useIsMobile } from '../../../hooks/useIsMobile';
5-
import { useMIDI } from '../../../hooks/useMIDI';
65
import { OutputSettings } from './OutputSettings';
76
import { AIFeaturesSettings } from './AIFeaturesSettings';
87

@@ -23,8 +22,6 @@ export function GeneralSettings() {
2322
} = useSettingsStore();
2423

2524
const isMobileDevice = useIsMobile();
26-
const { isSupported, isEnabled, devices, lastMessage, enableMIDI, disableMIDI } = useMIDI();
27-
2825
const handleSwitchToMobile = useCallback(() => {
2926
setForceDesktopMode(false);
3027
window.location.reload();
@@ -150,67 +147,6 @@ export function GeneralSettings() {
150147
</p>
151148
</div>
152149

153-
{/* MIDI */}
154-
<div className="settings-group">
155-
<div className="settings-group-title">MIDI Control</div>
156-
157-
{isSupported ? (
158-
<>
159-
<label className="settings-row">
160-
<span className="settings-label">Enable MIDI</span>
161-
<input
162-
type="checkbox"
163-
checked={isEnabled}
164-
onChange={(e) => { if (e.target.checked) enableMIDI(); else disableMIDI(); }}
165-
className="settings-checkbox"
166-
/>
167-
</label>
168-
169-
{isEnabled && (
170-
<>
171-
<div className="settings-status">
172-
<span className={`status-indicator ${devices.length > 0 ? 'connected' : 'disconnected'}`} />
173-
<span className="status-text">
174-
{devices.length > 0
175-
? `${devices.length} device${devices.length > 1 ? 's' : ''} connected`
176-
: 'No devices detected'}
177-
</span>
178-
</div>
179-
180-
{devices.length > 0 && (
181-
<div className="settings-group" style={{ marginTop: 8 }}>
182-
<div className="settings-group-title">Devices</div>
183-
{devices.map((device) => (
184-
<div key={device.id} className="settings-row">
185-
<span className="settings-label">{device.name}</span>
186-
<span className="settings-hint" style={{ margin: 0 }}>
187-
{device.manufacturer !== 'Unknown' ? device.manufacturer : ''}
188-
</span>
189-
</div>
190-
))}
191-
</div>
192-
)}
193-
194-
{lastMessage && (
195-
<div className="settings-group" style={{ marginTop: 8 }}>
196-
<div className="settings-group-title">Last Message</div>
197-
<div className="settings-row">
198-
<span className="settings-label">
199-
CH {lastMessage.channel} / CC {lastMessage.control} / Val {lastMessage.value}
200-
</span>
201-
</div>
202-
</div>
203-
)}
204-
</>
205-
)}
206-
</>
207-
) : (
208-
<p className="settings-hint">
209-
MIDI is not supported in this browser. Use Chrome or Edge for Web MIDI API support.
210-
</p>
211-
)}
212-
</div>
213-
214150
{/* AI Features */}
215151
<AIFeaturesSettings embedded />
216152
</div>

0 commit comments

Comments
 (0)