Skip to content
Open
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 app/src/actions/song.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const selectTrack =
({ pianoRollStore }: RootStore) =>
(trackId: TrackId) => {
pianoRollStore.selectedTrackId = trackId
pianoRollStore.previewingNoteNumbers.clear()
}

export const insertTrack =
Expand Down
24 changes: 9 additions & 15 deletions app/src/services/MIDIInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import RootStore from "../stores/RootStore"

export class MIDIInput {
private devices: WebMidi.MIDIInput[] = []
onMessage: ((e: WebMidi.MIDIMessageEvent) => void) | undefined
onMessage: ((data: Uint8Array) => void) | undefined

removeAllDevices = () => {
this.devices.forEach(this.removeDevice)
Expand All @@ -22,38 +22,32 @@ export class MIDIInput {
this.devices.push(device)
}

onMidiMessage = (e: WebMidi.MIDIMessageEvent) => {
this.onMessage?.(e)
private onMidiMessage = (e: WebMidi.MIDIMessageEvent) => {
this.onMessage?.(e.data)
}
}

export const previewMidiInput =
(rootStore: RootStore) => (e: WebMidi.MIDIMessageEvent) => {
(rootStore: RootStore) => (dataRaw: Uint8Array) => {
const {
pianoRollStore,
pianoRollStore: { selectedTrack },
player,
} = rootStore
if (selectedTrack === undefined) {
return
}
const { channel } = selectedTrack
if (channel === undefined) {
return
}

const stream = new Stream(e.data)
const stream = new Stream(dataRaw)
const event = deserializeSingleEvent(stream)

if (event.type !== "channel") {
return
}

// modify channel to the selected track channel
event.channel = channel

// TODO: seems like if sending to a channel which is not mapped to a Track, it defaults to playing the default piano (or Drums on CH 10). This should not happen.
player.sendEvent(event)

// optional, only showing notes in piano roll if its the same channel as selected track
if (event.channel !== selectedTrack?.channel) return;

if (event.subtype === "noteOn") {
pianoRollStore.previewingNoteNumbers.add(event.noteNumber)
} else if (event.subtype === "noteOff") {
Expand Down
62 changes: 35 additions & 27 deletions app/src/services/MIDIRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,55 +53,63 @@ export class MIDIRecorder {
})
}

onMessage(e: WebMidi.MIDIMessageEvent) {
onMessage(dataRaw: Uint8Array) {
if (!this.isRecording) {
return
}

const track = this.rootStore.pianoRollStore.selectedTrack
if (track === undefined) {
const stream = new Stream(dataRaw)
const message = deserializeSingleEvent(stream)
if (message.type !== "channel") {
return
}

const stream = new Stream(e.data)
const message = deserializeSingleEvent(stream)
const tracks = this.rootStore.song.tracks.filter(
(t) => t.channel === message.channel
);

if (message.type !== "channel") {
if (tracks.length === 0) {
return
}

const tick = this.player.position

switch (message.subtype) {
case "noteOn": {
const note = track.addEvent<NoteEvent>({
type: "channel",
subtype: "note",
noteNumber: message.noteNumber,
tick,
velocity: message.velocity,
duration: 0,
isRecording: true,
})
this.recordedNotes.push(note)
tracks.forEach((track) => {
const note = track.addEvent<NoteEvent>({
type: "channel",
subtype: "note",
noteNumber: message.noteNumber,
tick,
velocity: message.velocity,
duration: 0,
isRecording: true,
})
this.recordedNotes.push(note)
});
break
}
case "noteOff": {
this.recordedNotes
.filter((n) => n.noteNumber === message.noteNumber)
.forEach((n) => {
track.updateEvent<NoteEvent>(n.id, {
duration: Math.max(0, tick - n.tick),
tracks.forEach((track) => {
this.recordedNotes
.filter((n) => n.noteNumber === message.noteNumber)
.forEach((n) => {
track.updateEvent<NoteEvent>(n.id, {
duration: Math.max(0, tick - n.tick),
})
})
})

this.recordedNotes = this.recordedNotes.filter(
(n) => n.noteNumber !== message.noteNumber,
)

this.recordedNotes = this.recordedNotes.filter(
(n) => n.noteNumber !== message.noteNumber,
)
});
break
}
default: {
track.addEvent({ ...message, tick, isRecording: true })
tracks.forEach((track) => {
track.addEvent({ ...message, tick, isRecording: true })
});
break
}
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/stores/RootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ export default class RootStore {

const preview = previewMidiInput(this)

this.midiInput.onMidiMessage = (e) => {
preview(e)
this.midiRecorder.onMessage(e)
this.midiInput.onMessage = (dataRaw) => {
preview(dataRaw)
this.midiRecorder.onMessage(dataRaw)
}

this.pianoRollStore.setUpAutorun()
Expand Down