refactor(tracker): put all recording data into struct Recording

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2023-10-19 23:02:21 +03:00
parent 453f45c48a
commit d342fb860b
5 changed files with 61 additions and 63 deletions

View File

@ -20,8 +20,8 @@ import (
type NullContext struct {
}
func (NullContext) NextEvent() (event tracker.PlayerProcessEvent, ok bool) {
return tracker.PlayerProcessEvent{}, false
func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
return tracker.MIDINoteEvent{}, false
}
func (NullContext) BPM() (bpm float64, ok bool) {

View File

@ -20,7 +20,7 @@ type VSTIProcessContext struct {
host vst2.Host
}
func (c *VSTIProcessContext) NextEvent() (event tracker.PlayerProcessEvent, ok bool) {
func (c *VSTIProcessContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
var ev vst2.MIDIEvent
for len(c.events) > 0 {
ev, c.events = c.events[0], c.events[1:]
@ -28,16 +28,16 @@ func (c *VSTIProcessContext) NextEvent() (event tracker.PlayerProcessEvent, ok b
case ev.Data[0] >= 0x80 && ev.Data[0] < 0x90:
channel := ev.Data[0] - 0x80
note := ev.Data[1]
return tracker.PlayerProcessEvent{Frame: int(ev.DeltaFrames), On: false, Channel: int(channel), Note: note}, true
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: false, Channel: int(channel), Note: note}, true
case ev.Data[0] >= 0x90 && ev.Data[0] < 0xA0:
channel := ev.Data[0] - 0x90
note := ev.Data[1]
return tracker.PlayerProcessEvent{Frame: int(ev.DeltaFrames), On: true, Channel: int(channel), Note: note}, true
return tracker.MIDINoteEvent{Frame: int(ev.DeltaFrames), On: true, Channel: int(channel), Note: note}, true
default:
// ignore all other MIDI messages
}
}
return tracker.PlayerProcessEvent{}, false
return tracker.MIDINoteEvent{}, false
}
func (c *VSTIProcessContext) BPM() (bpm float64, ok bool) {

View File

@ -240,11 +240,14 @@ func (m *Model) ProcessPlayerMessage(msg PlayerMessage) {
switch e := msg.Inner.(type) {
case PlayerCrashMessage:
m.d.Panic = true
case PlayerRecordedMessage:
case Recording:
if e.BPM == 0 {
e.BPM = float64(m.d.Song.BPM)
}
song := RecordingToSong(m.d.Song.Patch, m.d.Song.RowsPerBeat, m.d.Song.Score.RowsPerPattern, e)
song, err := e.Song(m.d.Song.Patch, m.d.Song.RowsPerBeat, m.d.Song.Score.RowsPerPattern)
if err != nil {
break
}
m.SetSong(song)
m.d.InstrEnlarged = false
default:

View File

@ -25,10 +25,8 @@ type (
peakVolumeMeter VolumeAnalyzer
voiceStates [vm.MAX_VOICES]float32
recording bool
recordingNoteArrived bool
recordingFrames int
recordingEvents []PlayerProcessEvent
recState recState
recording Recording
synther sointu.Synther
playerMessages chan<- PlayerMessage
@ -36,11 +34,11 @@ type (
}
PlayerProcessContext interface {
NextEvent() (event PlayerProcessEvent, ok bool)
NextEvent() (event MIDINoteEvent, ok bool)
BPM() (bpm float64, ok bool)
}
PlayerProcessEvent struct {
MIDINoteEvent struct {
Frame int
On bool
Channel int
@ -51,12 +49,6 @@ type (
bool
}
PlayerRecordedMessage struct {
BPM float64 // vsts allow bpms as floats so for accurate reconstruction, keep it as float for recording
Events []PlayerProcessEvent
TotalFrames int
}
// Volume and SongRow are transmitted so frequently that they are treated specially, to avoid boxing. All the
// rest messages can be boxed to interface{}
PlayerMessage struct {
@ -74,6 +66,10 @@ type (
PlayerVolumeErrorMessage struct {
error
}
)
type (
recState int
voiceNote struct {
voice int
@ -85,6 +81,12 @@ type (
}
)
const (
recStateNone recState = iota
recStateWaitingForNote
recStateRecording
)
const NUM_RENDER_TRIES = 10000
func NewPlayer(synther sointu.Synther, playerMessages chan<- PlayerMessage, modelMessages <-chan interface{}) *Player {
@ -103,22 +105,22 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
midi, midiOk := context.NextEvent()
frame := 0
if p.recording && p.recordingNoteArrived {
p.recordingFrames += len(buffer)
if p.recState == recStateRecording {
p.recording.TotalFrames += len(buffer)
}
oldBuffer := buffer
for i := 0; i < NUM_RENDER_TRIES; i++ {
for midiOk && frame >= midi.Frame {
if p.recording {
if !p.recordingNoteArrived {
p.recordingFrames = len(buffer)
p.recordingNoteArrived = true
}
if p.recState == recStateWaitingForNote {
p.recording.TotalFrames = len(buffer)
p.recState = recStateRecording
}
if p.recState == recStateRecording {
midiTotalFrame := midi
midiTotalFrame.Frame = p.recordingFrames - len(buffer)
p.recordingEvents = append(p.recordingEvents, midiTotalFrame)
midiTotalFrame.Frame = p.recording.TotalFrames - len(buffer)
p.recording.Events = append(p.recording.Events, midiTotalFrame)
}
if midi.On {
p.triggerInstrument(midi.Channel, midi.Note)
@ -278,20 +280,14 @@ loop:
}
case ModelRecordingMessage:
if m.bool {
p.recording = true
p.recordingEvents = make([]PlayerProcessEvent, 0)
p.recordingFrames = 0
p.recordingNoteArrived = false
p.recState = recStateWaitingForNote
p.recording = Recording{}
} else {
if p.recording && len(p.recordingEvents) > 0 {
bpm, _ := context.BPM()
p.trySend(PlayerRecordedMessage{
BPM: bpm,
Events: p.recordingEvents,
TotalFrames: p.recordingFrames,
})
if p.recState == recStateRecording && len(p.recording.Events) > 0 {
p.recording.BPM, _ = context.BPM()
p.trySend(p.recording)
}
p.recording = false
p.recState = recStateNone
}
default:
// ignore unknown messages

View File

@ -1,20 +1,31 @@
package tracker
import (
"bytes"
"errors"
"math"
"github.com/vsariola/sointu"
)
type (
recordingNote struct {
note byte
startRow int
endRow int
}
)
type Recording struct {
BPM float64 // vsts allow bpms as floats so for accurate reconstruction, keep it as float for recording
Events []MIDINoteEvent
TotalFrames int
}
func RecordingToSong(patch sointu.Patch, rowsPerBeat, rowsPerPattern int, recording PlayerRecordedMessage) sointu.Song {
type recordingNote struct {
note byte
startRow int
endRow int
}
var ErrInvalidRows = errors.New("rows per beat and rows per pattern must be greater than 1")
func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Song, error) {
if rowsPerBeat <= 1 || rowsPerPattern <= 1 {
return sointu.Song{}, ErrInvalidRows
}
channelNotes := make([][]recordingNote, 0)
// find the length of each note and assign it to its respective channel
for i, m := range recording.Events {
@ -109,7 +120,7 @@ func RecordingToSong(patch sointu.Patch, rowsPerBeat, rowsPerPattern int, record
continue L
}
for l, p2 := range patterns {
if testEq(p, p2) {
if bytes.Equal(p, p2) {
order[k] = l
continue L
}
@ -125,21 +136,9 @@ func RecordingToSong(patch sointu.Patch, rowsPerBeat, rowsPerPattern int, record
}
}
score := sointu.Score{Length: songLengthPatterns, RowsPerPattern: rowsPerPattern, Tracks: songTracks}
return sointu.Song{BPM: int(recording.BPM + 0.5), RowsPerBeat: rowsPerBeat, Score: score, Patch: patch.Copy()}
return sointu.Song{BPM: int(recording.BPM + 0.5), RowsPerBeat: rowsPerBeat, Score: score, Patch: patch.Copy()}, nil
}
func frameToRow(BPM float64, rowsPerBeat, frame int) int {
return int(float64(frame)/44100/60*BPM*float64(rowsPerBeat) + 0.5)
}
func testEq(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}