mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
This should make testing easier, as Synth can be assumed to stay the same during each call. Synth is also the part that we can parse from .asm/.json file and a Patch can be compiled into a synth. Synth can be eventually made quite opaque to the user. The user should not need to worry about opcodes etc.
129 lines
2.9 KiB
Go
129 lines
2.9 KiB
Go
package song
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/vsariola/sointu/bridge"
|
|
)
|
|
|
|
type Track struct {
|
|
NumVoices int
|
|
Sequence []byte
|
|
}
|
|
|
|
type Song struct {
|
|
BPM int
|
|
Patterns [][]byte
|
|
Tracks []Track
|
|
Patch bridge.Patch
|
|
Samples int // -1 means calculate automatically, but you can also set it manually
|
|
}
|
|
|
|
func NewSong(bpm int, patterns [][]byte, tracks []Track, patch bridge.Patch) (*Song, error) {
|
|
s := new(Song)
|
|
s.BPM = bpm
|
|
s.Patterns = patterns
|
|
s.Tracks = tracks
|
|
s.Patch = patch
|
|
err := s.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.Samples = -1
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Song) Validate() error {
|
|
if s.BPM < 1 {
|
|
return errors.New("BPM should be > 0")
|
|
}
|
|
for i := range s.Patterns[:len(s.Patterns)-1] {
|
|
if len(s.Patterns[i]) != len(s.Patterns[i+1]) {
|
|
return errors.New("Every pattern should have the same length")
|
|
}
|
|
}
|
|
for i := range s.Tracks[:len(s.Tracks)-1] {
|
|
if len(s.Tracks[i].Sequence) != len(s.Tracks[i+1].Sequence) {
|
|
return errors.New("Every track should have the same sequence length")
|
|
}
|
|
}
|
|
totalTrackVoices := 0
|
|
for _, track := range s.Tracks {
|
|
totalTrackVoices += track.NumVoices
|
|
for _, p := range track.Sequence {
|
|
if p < 0 || int(p) >= len(s.Patterns) {
|
|
return errors.New("Tracks use a non-existing pattern")
|
|
}
|
|
}
|
|
}
|
|
if totalTrackVoices > s.Patch.TotalVoices() {
|
|
return errors.New("Tracks use too many voices")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Song) PatternRows() int {
|
|
return len(s.Patterns[0])
|
|
}
|
|
|
|
func (s *Song) SequenceLength() int {
|
|
return len(s.Tracks[0].Sequence)
|
|
}
|
|
|
|
func (s *Song) TotalRows() int {
|
|
return s.PatternRows() * s.SequenceLength()
|
|
}
|
|
|
|
func (s *Song) SamplesPerRow() int {
|
|
return 44100 * 60 / (s.BPM * 4)
|
|
}
|
|
|
|
func (s *Song) FirstTrackVoice(track int) int {
|
|
ret := 0
|
|
for _, t := range s.Tracks[:track] {
|
|
ret += t.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (s *Song) Render(synth *bridge.Synth, state *bridge.SynthState) ([]float32, error) {
|
|
err := s.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
curVoices := make([]int, len(s.Tracks))
|
|
for i := range curVoices {
|
|
curVoices[i] = s.FirstTrackVoice(i)
|
|
}
|
|
samples := s.Samples
|
|
if samples < 0 {
|
|
samples = s.TotalRows() * s.SamplesPerRow()
|
|
}
|
|
buffer := make([]float32, samples*2)
|
|
totaln := 0
|
|
rowtime := 44100 * 60 / (s.BPM * 4)
|
|
for row := 0; row < s.TotalRows(); row++ {
|
|
patternRow := row % s.PatternRows()
|
|
pattern := row / s.PatternRows()
|
|
for t := range s.Tracks {
|
|
patternIndex := s.Tracks[t].Sequence[pattern]
|
|
note := s.Patterns[patternIndex][patternRow]
|
|
if note == 1 { // anything but hold causes an action.
|
|
continue // TODO: can hold be actually something else than 1?
|
|
}
|
|
state.Release(curVoices[t])
|
|
if note > 1 {
|
|
curVoices[t]++
|
|
first := s.FirstTrackVoice(t)
|
|
if curVoices[t] >= first+s.Tracks[t].NumVoices {
|
|
curVoices[t] = first
|
|
}
|
|
state.Trigger(curVoices[t], note)
|
|
}
|
|
}
|
|
samples, _, _ := synth.RenderTime(state, buffer[2*totaln:], rowtime)
|
|
totaln += samples
|
|
}
|
|
return buffer, nil
|
|
}
|