Route MIDI notes by channel; punch-in from MIDI on the Tracker tab (v0.20.0)
Incoming USB/BLE MIDI notes were dropping their channel and always playing the generic default-synth pool, ignoring the mixer routing. Now the parser keeps the channel nibble (exposed 1-based, 1..16) on InputAction.NoteOn/NoteOff, and the ViewModel routes a channelled note through engine.busNoteOn/busNoteOff — sounding the mixer track(s) whose midiChannel matches, through their instrument and full insert chain, exactly like the on-screen keyboard. Channel-less sources (physical keyboard) keep the previous audition path. On the Tracker tab with punch-in armed, a MIDI note now also punches into the focused cell, recording its own velocity and channel and advancing by the skip step (punchNote gained velocity/channel parameters). Off the tracker tab or with punch-in off, MIDI only auditions. Note: routing is strict, so a note is audible only on a track whose MIDI channel matches and that has an instrument assigned (new projects start with buses on the empty channel 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,8 @@ android {
|
||||
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 45
|
||||
versionName = "0.19.0"
|
||||
versionCode = 46
|
||||
versionName = "0.20.0"
|
||||
|
||||
// We provide our own instrumentation runner if/when tests are added.
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -33,8 +33,12 @@ sealed interface InputAction {
|
||||
data object NoteOffCell : InputAction
|
||||
|
||||
// ----- Live note entry (also used to audition instruments) -----
|
||||
data class NoteOn(val pitch: Int, val velocity: Int) : InputAction
|
||||
data class NoteOff(val pitch: Int) : InputAction
|
||||
// [channel] is the 1-based MIDI channel (1..16) for MIDI input, so notes can be
|
||||
// routed to the mixer track(s) listening on it; it is -1 when the source has no
|
||||
// channel of its own (physical / on-screen keyboard), which falls back to the
|
||||
// on-screen keyboard's channel.
|
||||
data class NoteOn(val pitch: Int, val velocity: Int, val channel: Int = -1) : InputAction
|
||||
data class NoteOff(val pitch: Int, val channel: Int = -1) : InputAction
|
||||
|
||||
// ----- Transport -----
|
||||
data object PlayPause : InputAction
|
||||
|
||||
@@ -74,16 +74,19 @@ class MidiInput(
|
||||
// anywhere; forward them (clock / start / stop) and move on one byte.
|
||||
if (status >= 0xF8) { onRealtime?.invoke(status); i++; continue }
|
||||
val type = status and 0xF0
|
||||
// Low nibble is the MIDI channel (0..15); expose it 1-based (1..16) so
|
||||
// it matches the mixer channels' `midiChannel` for routing.
|
||||
val channel = (status and 0x0F) + 1
|
||||
when (type) {
|
||||
0x90 -> { // Note On
|
||||
val note = data.getOrZero(i + 1)
|
||||
val vel = data.getOrZero(i + 2)
|
||||
if (vel > 0) router.dispatch(InputAction.NoteOn(note, vel))
|
||||
else router.dispatch(InputAction.NoteOff(note)) // vel 0 == note off
|
||||
if (vel > 0) router.dispatch(InputAction.NoteOn(note, vel, channel))
|
||||
else router.dispatch(InputAction.NoteOff(note, channel)) // vel 0 == note off
|
||||
i += 3
|
||||
}
|
||||
0x80 -> { // Note Off
|
||||
router.dispatch(InputAction.NoteOff(data.getOrZero(i + 1))); i += 3
|
||||
router.dispatch(InputAction.NoteOff(data.getOrZero(i + 1), channel)); i += 3
|
||||
}
|
||||
0xB0 -> { // Control Change
|
||||
val cc = data.getOrZero(i + 1)
|
||||
|
||||
@@ -38,6 +38,9 @@ import kotlinx.coroutines.launch
|
||||
* performance, so after we mutate the grid we bump [revision]. Compose reads that
|
||||
* value while drawing the grid, so an increment forces a redraw. Simple and fast.
|
||||
*/
|
||||
/** Tab index of the Tracker tab (see [App]'s tab order). */
|
||||
private const val TAB_TRACKER = 0
|
||||
|
||||
class AppViewModel(
|
||||
val project: Project,
|
||||
private val engine: AudioEngine,
|
||||
@@ -207,15 +210,16 @@ class AppViewModel(
|
||||
cursorColumn = CellColumn.entries[flat % cols]
|
||||
}
|
||||
|
||||
/** Punch a note into the focused cell (velocity/channel from the keyboard) and
|
||||
* advance the cursor by [skipStep] rows, cycling past the pattern ends — the
|
||||
* touch-keyboard entry path. */
|
||||
fun punchNote(midi: Int) {
|
||||
/** Punch a note into the focused cell and advance the cursor by [skipStep] rows,
|
||||
* cycling past the pattern ends. Velocity/channel default to the on-screen
|
||||
* keyboard's (the touch-keyboard entry path); MIDI punch-in passes the incoming
|
||||
* note's own velocity and channel so the recorded cell plays back correctly. */
|
||||
fun punchNote(midi: Int, velocity: Int = kbdVelocity, channel: Int = kbdChannel) {
|
||||
val cell = focusedCell()
|
||||
cell.note = midi.coerceIn(Pitch.LOWEST, Pitch.HIGHEST)
|
||||
cell.velocity = kbdVelocity.coerceIn(0, Cell.MAX_VELOCITY)
|
||||
cell.channel = kbdChannel
|
||||
lastNote = cell.note; lastChannel = kbdChannel
|
||||
cell.velocity = velocity.coerceIn(0, Cell.MAX_VELOCITY)
|
||||
cell.channel = channel
|
||||
lastNote = cell.note; lastChannel = channel
|
||||
moveCursorVerticalWrap(skipStep)
|
||||
touched()
|
||||
}
|
||||
@@ -528,13 +532,22 @@ class AppViewModel(
|
||||
InputAction.NoteOffCell -> noteOffFocused()
|
||||
is InputAction.NoteOn -> {
|
||||
markNoteOn(action.pitch)
|
||||
engine.liveNoteOn(action.pitch, action.velocity)
|
||||
// Live entry also writes the note into the focused NOTE cell.
|
||||
if (cursorColumn == CellColumn.NOTE) {
|
||||
focusedCell().note = action.pitch; lastNote = action.pitch; touched()
|
||||
// MIDI notes route to the mixer track(s) listening on their channel;
|
||||
// a channel-less source (physical keyboard) plays the default synth.
|
||||
if (action.channel >= 1) engine.busNoteOn(action.channel, action.pitch, action.velocity)
|
||||
else engine.liveNoteOn(action.pitch, action.velocity)
|
||||
// On the Tracker tab with punch-in armed, also enter the note into the
|
||||
// grid — recording its own velocity/channel so it plays back correctly.
|
||||
if (selectedTab == TAB_TRACKER && punchInMode) {
|
||||
val ch = if (action.channel >= 1) action.channel else kbdChannel
|
||||
punchNote(action.pitch, action.velocity, ch)
|
||||
}
|
||||
}
|
||||
is InputAction.NoteOff -> { markNoteOff(action.pitch); engine.liveNoteOff(action.pitch) }
|
||||
is InputAction.NoteOff -> {
|
||||
markNoteOff(action.pitch)
|
||||
if (action.channel >= 1) engine.busNoteOff(action.channel, action.pitch)
|
||||
else engine.liveNoteOff(action.pitch)
|
||||
}
|
||||
InputAction.PlayPause -> playPause()
|
||||
InputAction.Stop -> stop()
|
||||
InputAction.ToggleLoop -> { project.arrangement.loopEnabled = !project.arrangement.loopEnabled; touched() }
|
||||
|
||||
Reference in New Issue
Block a user