mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data. This rewrite closes #72, #106 and #120.
148 lines
4.5 KiB
Go
148 lines
4.5 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)
|
|
}
|
|
}
|
|
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)
|
|
}
|