diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index 6583b1b..36fd7a0 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -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) { diff --git a/cmd/sointu-vsti/main.go b/cmd/sointu-vsti/main.go index 1842c71..32bcf62 100644 --- a/cmd/sointu-vsti/main.go +++ b/cmd/sointu-vsti/main.go @@ -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) { diff --git a/tracker/model.go b/tracker/model.go index 8de03e2..23fdf1a 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -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: diff --git a/tracker/player.go b/tracker/player.go index fa323e7..19862eb 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -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 diff --git a/tracker/recording.go b/tracker/recording.go index 8355be3..9bdca1f 100644 --- a/tracker/recording.go +++ b/tracker/recording.go @@ -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 -}