mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-27 19:00:25 -04:00
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package tracker
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"math"
|
|
|
|
"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 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) Score(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Score, error) {
|
|
if rowsPerBeat <= 1 || rowsPerPattern <= 1 {
|
|
return sointu.Score{}, ErrInvalidRows
|
|
}
|
|
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) {
|
|
continue
|
|
}
|
|
endFrame := math.MaxInt
|
|
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
|
|
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)
|
|
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
|
|
// if none is released, assign it to new track if there's any. otherwise, assign it to the left most track
|
|
tracks := make([][][]recordingNote, len(channelNotes))
|
|
for i, c := range channelNotes {
|
|
tracks[i] = make([][]recordingNote, 0)
|
|
noteloop:
|
|
for _, n := range c {
|
|
// if a track is release, assign the note to left-most released track
|
|
for k, t := range tracks[i] {
|
|
if len(t) == 0 || t[len(t)-1].endRow <= n.startRow {
|
|
tracks[i][k] = append(t, n)
|
|
continue noteloop
|
|
}
|
|
}
|
|
// if there's space for more tracks, create one
|
|
if len(tracks[i]) < patch[i].NumVoices {
|
|
tracks[i] = append(tracks[i], []recordingNote{n})
|
|
continue noteloop
|
|
}
|
|
// otherwise, put the note to the track that was triggered longest time ago
|
|
oldestIndex := -1
|
|
oldestRow := math.MaxInt
|
|
for k, t := range tracks[i] {
|
|
if r := t[len(t)-1].startRow; r < oldestRow {
|
|
oldestRow = r
|
|
oldestIndex = k
|
|
}
|
|
}
|
|
tracks[i][oldestIndex] = append(tracks[i][oldestIndex], n)
|
|
}
|
|
}
|
|
// if there was tracks that had no notes, create empty tracks for them
|
|
for i := range channelNotes {
|
|
if l := len(tracks[i]); l == 0 && l < patch[i].NumVoices {
|
|
tracks[i] = append(tracks[i], []recordingNote{})
|
|
}
|
|
}
|
|
songLengthPatterns := (frameToRow(recording.BPM, rowsPerBeat, recording.TotalFrames) + rowsPerPattern - 1) / rowsPerPattern
|
|
songLengthRows := songLengthPatterns * rowsPerPattern
|
|
songTracks := make([]sointu.Track, 0)
|
|
for i, tg := range tracks {
|
|
for j, t := range tg {
|
|
// construct flat linear note arrays for tracks
|
|
flatPattern := make(sointu.Pattern, songLengthRows)
|
|
for k := range flatPattern {
|
|
flatPattern[k] = 1 // set all notes as holds at first
|
|
}
|
|
for _, n := range t {
|
|
if n.startRow >= songLengthRows {
|
|
continue
|
|
}
|
|
flatPattern[n.startRow] = n.note
|
|
if n.endRow < songLengthRows {
|
|
for l := n.startRow + 1; l < n.endRow; l++ {
|
|
flatPattern[l] = 1
|
|
}
|
|
flatPattern[n.endRow] = 0
|
|
} else {
|
|
for l := n.startRow + 1; l < songLengthRows; l++ {
|
|
flatPattern[l] = 1
|
|
}
|
|
}
|
|
}
|
|
// calculate number of voices, distributing the total number of voices to the different tracks
|
|
numVoices := (patch[i].NumVoices + len(tg) - j - 1) / len(tg)
|
|
// construct patterns
|
|
order := make(sointu.Order, songLengthPatterns)
|
|
patterns := make([]sointu.Pattern, 0)
|
|
L:
|
|
for k := range order {
|
|
p := flatPattern[k*rowsPerPattern : (k+1)*rowsPerPattern]
|
|
allHolds := true
|
|
for _, n := range p {
|
|
if n != 1 {
|
|
allHolds = false
|
|
break
|
|
}
|
|
}
|
|
if allHolds {
|
|
order[k] = -1
|
|
continue L
|
|
}
|
|
for l, p2 := range patterns {
|
|
if bytes.Equal(p, p2) {
|
|
order[k] = l
|
|
continue L
|
|
}
|
|
}
|
|
// make a copy of the slice so they are all independent and don't accidentally expand to same memory
|
|
newPat := make(sointu.Pattern, len(p))
|
|
copy(newPat, p)
|
|
order[k] = len(patterns)
|
|
patterns = append(patterns, newPat)
|
|
}
|
|
track := sointu.Track{NumVoices: numVoices, Effect: false, Order: order, Patterns: patterns}
|
|
songTracks = append(songTracks, track)
|
|
}
|
|
}
|
|
score := sointu.Score{Length: songLengthPatterns, RowsPerPattern: rowsPerPattern, Tracks: songTracks}
|
|
return score, nil
|
|
}
|
|
|
|
func frameToRow(BPM float64, rowsPerBeat, frame int) int {
|
|
return int(float64(frame)/44100/60*BPM*float64(rowsPerBeat) + 0.5)
|
|
}
|