feat(tracker): rework the MIDI input and note event handling

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-03 20:03:22 +03:00
parent 7ef868a434
commit 283fbc1171
19 changed files with 428 additions and 500 deletions

View File

@ -8,42 +8,77 @@ import (
"github.com/vsariola/sointu"
)
type Recording struct {
BPM float64 // vsts allow bpms as floats so for accurate reconstruction, keep it as float for recording
Events []MIDINoteEvent
TotalFrames int
}
type (
Recording struct {
BPM float64 // vsts allow bpms as floats so for accurate reconstruction, keep it as float for recording
Events NoteEventList
StartFrame, EndFrame int64
State RecordingState
}
type recordingNote struct {
note byte
startRow int
endRow int
}
RecordingState int
)
const (
RecordingNone RecordingState = iota
RecordingWaitingForNote
RecordingStarted // StartFrame is set, but EndFrame is not
RecordingFinished // StartFrame and EndFrame are both set, recording is finished
)
var ErrInvalidRows = errors.New("rows per beat and rows per pattern must be greater than 1")
var ErrNotFinished = errors.New("the recording was not finished")
func (r *Recording) Record(ev NoteEvent, frame int64) {
if r.State == RecordingNone || r.State == RecordingFinished {
return
}
if r.State == RecordingWaitingForNote {
r.StartFrame = frame
r.State = RecordingStarted
}
r.Events = append(r.Events, ev)
}
func (r *Recording) Finish(frame int64, frameDeltas map[any]int64) {
if r.State != RecordingStarted {
return
}
r.State = RecordingFinished
r.EndFrame = frame
r.Events.adjustTimes(frameDeltas, r.StartFrame, r.EndFrame)
}
func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Score, error) {
if rowsPerBeat <= 1 || rowsPerPattern <= 1 {
return sointu.Score{}, ErrInvalidRows
}
if recording.State != RecordingFinished {
return sointu.Score{}, ErrNotFinished
}
type recordingNote struct {
note byte
startRow int
endRow int
}
channelNotes := make([][]recordingNote, 0)
// find the length of each note and assign it to its respective channel
for i, m := range recording.Events {
if !m.On || m.Channel >= len(patch) {
if !m.On || m.Channel >= len(patch) || m.IsTrack {
continue
}
endFrame := math.MaxInt
var endFrame int64 = math.MaxInt64
for j := i + 1; j < len(recording.Events); j++ {
if recording.Events[j].Channel == m.Channel && recording.Events[j].Note == m.Note {
endFrame = recording.Events[j].Frame
endFrame = recording.Events[j].playerTimestamp
break
}
}
for len(channelNotes) <= m.Channel {
channelNotes = append(channelNotes, make([]recordingNote, 0))
}
startRow := frameToRow(recording.BPM, rowsPerBeat, m.Frame)
endRow := frameToRow(recording.BPM, rowsPerBeat, endFrame)
startRow := frameToRow(recording.BPM, rowsPerBeat, m.playerTimestamp-recording.StartFrame)
endRow := frameToRow(recording.BPM, rowsPerBeat, endFrame-recording.StartFrame)
channelNotes[m.Channel] = append(channelNotes[m.Channel], recordingNote{m.Note, startRow, endRow})
}
//assign notes to tracks, assigning it to left most track that is released
@ -83,7 +118,7 @@ func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPatter
tracks[i] = append(tracks[i], []recordingNote{})
}
}
songLengthPatterns := (frameToRow(recording.BPM, rowsPerBeat, recording.TotalFrames) + rowsPerPattern - 1) / rowsPerPattern
songLengthPatterns := (frameToRow(recording.BPM, rowsPerBeat, recording.EndFrame-recording.StartFrame) + rowsPerPattern - 1) / rowsPerPattern
songLengthRows := songLengthPatterns * rowsPerPattern
songTracks := make([]sointu.Track, 0)
for i, tg := range tracks {
@ -148,6 +183,6 @@ func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPatter
return score, nil
}
func frameToRow(BPM float64, rowsPerBeat, frame int) int {
func frameToRow(BPM float64, rowsPerBeat int, frame int64) int {
return int(float64(frame)/44100/60*BPM*float64(rowsPerBeat) + 0.5)
}