Implement a song struct to hold all the information of a single song (corresponding one .asm file) and Render function for it.

This commit is contained in:
Veikko Sariola 2020-10-24 23:25:23 +03:00
parent be7a4e21f3
commit 6c90ba2067
3 changed files with 215 additions and 2 deletions

View File

@ -28,6 +28,16 @@ type Instrument struct {
Units []Unit
}
type Patch []Instrument
func (p Patch) TotalVoices() int {
ret := 0
for _, i := range p {
ret += i.NumVoices
}
return ret
}
var ( // cannot be const as the rhs are not known at compile-time
Add = Opcode(C.su_add_id)
Addp = Opcode(C.su_addp_id)
@ -100,7 +110,7 @@ func (s *SynthState) Render(buffer []float32, maxRows int, callback func()) (int
return maxSamples - remaining, nil
}
func (s *SynthState) SetPatch(patch []Instrument) error {
func (s *SynthState) SetPatch(patch Patch) error {
totalVoices := 0
commands := make([]Opcode, 0)
values := make([]byte, 0)
@ -138,7 +148,7 @@ func (s *SynthState) SetPatch(patch []Instrument) error {
return nil
}
func (s *SynthState) Trigger(voice int, note int) {
func (s *SynthState) Trigger(voice int, note byte) {
cs := (*C.SynthState)(s)
cs.Synth.Voices[voice] = C.Voice{}
cs.Synth.Voices[voice].Note = C.int(note)
@ -149,6 +159,10 @@ func (s *SynthState) Release(voice int) {
cs.Synth.Voices[voice].Release = 1
}
func (s *SynthState) SetSamplesPerRow(spr int) {
s.SamplesPerRow = C.uint(spr)
}
func NewSynthState() *SynthState {
s := new(SynthState)
s.RandSeed = 1

135
song/song.go Normal file
View File

@ -0,0 +1,135 @@
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.Patterns)-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() ([]float32, error) {
err := s.Validate()
if err != nil {
return nil, err
}
synth := bridge.NewSynthState()
synth.SetPatch(s.Patch)
synth.SetSamplesPerRow(44100 * 60 / (s.BPM * 4))
curVoices := make([]int, len(s.Tracks))
for i := range curVoices {
curVoices[i] = s.FirstTrackVoice(i)
}
processRow := func(row int) {
if row >= s.TotalRows() {
return
}
patternRow := row % s.PatternRows()
pattern := row / s.PatternRows()
for t := range s.Tracks {
note := s.Patterns[pattern][patternRow]
if note == 1 { // anything but hold causes an action.
continue // TODO: can hold be actually something else than 1?
}
synth.Release(curVoices[t])
if note > 1 {
curVoices[t]++
first := s.FirstTrackVoice(t)
if curVoices[t] >= first+s.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
}
samples := s.Samples
if samples < 0 {
samples = s.TotalRows() * s.SamplesPerRow()
}
buffer := make([]float32, samples*2)
row := 0
processRow(0)
synth.Render(buffer, s.TotalRows(), func() {
row++
processRow(row)
})
return buffer, nil
}

64
song/song_test.go Normal file
View File

@ -0,0 +1,64 @@
package song_test
import (
"bytes"
"encoding/binary"
"io/ioutil"
"path"
"runtime"
"testing"
"github.com/vsariola/sointu/bridge"
"github.com/vsariola/sointu/song"
)
const BPM = 100
const SAMPLE_RATE = 44100
const TOTAL_ROWS = 16
const SAMPLES_PER_ROW = SAMPLE_RATE * 4 * 60 / (BPM * 16)
const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
// const bufsize = su_max_samples * 2
func TestSongRender(t *testing.T) {
patch := []bridge.Instrument{
bridge.Instrument{1, []bridge.Unit{
bridge.Unit{bridge.Envelope, []byte{32, 32, 64, 64, 128}},
bridge.Unit{bridge.Oscillat, []byte{64, 64, 0, 96, 64, 128, 0x40}},
bridge.Unit{bridge.Mulp, []byte{}},
bridge.Unit{bridge.Envelope, []byte{32, 32, 64, 64, 128}},
bridge.Unit{bridge.Oscillat, []byte{72, 64, 64, 64, 96, 128, 0x40}},
bridge.Unit{bridge.Mulp, []byte{}},
bridge.Unit{bridge.Out.Stereo(), []byte{128}},
}}}
patterns := [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}
tracks := []song.Track{song.Track{1, []byte{0}}}
song, err := song.NewSong(100, patterns, tracks, patch)
if err != nil {
t.Fatalf("NewSong failed: %v", err)
}
buffer, err := song.Render()
if err != nil {
t.Fatalf("Render failed: %v", err)
}
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", "test_oscillat_sine.raw"))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
var createdbuf bytes.Buffer
err = binary.Write(&createdbuf, binary.LittleEndian, buffer)
if err != nil {
t.Fatalf("error converting buffer: %v", err)
}
createdb := createdbuf.Bytes()
if len(createdb) != len(expectedb) {
t.Fatalf("buffer length mismatch, got %v, expected %v", len(createdb), len(expectedb))
}
for i, v := range createdb {
if expectedb[i] != v {
t.Fatalf("byte mismatch @ %v, got %v, expected %v", i, v, expectedb[i])
}
}
}