mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
588 lines
20 KiB
Go
588 lines
20 KiB
Go
package sointu
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
|
type Unit struct {
|
|
Type string
|
|
Parameters map[string]int `yaml:",flow"`
|
|
VarArgs []int `yaml:",flow,omitempty"`
|
|
}
|
|
|
|
func (u *Unit) Copy() Unit {
|
|
parameters := make(map[string]int)
|
|
for k, v := range u.Parameters {
|
|
parameters[k] = v
|
|
}
|
|
varArgs := make([]int, len(u.VarArgs))
|
|
copy(varArgs, u.VarArgs)
|
|
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs}
|
|
}
|
|
|
|
const (
|
|
Sine = iota
|
|
Trisaw = iota
|
|
Pulse = iota
|
|
Gate = iota
|
|
Sample = iota
|
|
)
|
|
|
|
// Instrument includes a list of units consisting of the instrument, and the number of polyphonic voices for this instrument
|
|
type Instrument struct {
|
|
Name string
|
|
NumVoices int
|
|
Units []Unit
|
|
}
|
|
|
|
func (instr *Instrument) Copy() Instrument {
|
|
units := make([]Unit, len(instr.Units))
|
|
for i, u := range instr.Units {
|
|
units[i] = u.Copy()
|
|
}
|
|
return Instrument{Name: instr.Name, NumVoices: instr.NumVoices, Units: units}
|
|
}
|
|
|
|
// Patch is simply a list of instruments used in a song
|
|
type Patch struct {
|
|
Instruments []Instrument
|
|
}
|
|
|
|
func (p *Patch) Copy() Patch {
|
|
instruments := make([]Instrument, len(p.Instruments))
|
|
for i, instr := range p.Instruments {
|
|
instruments[i] = instr.Copy()
|
|
}
|
|
return Patch{Instruments: instruments}
|
|
}
|
|
|
|
func (p Patch) TotalVoices() int {
|
|
ret := 0
|
|
for _, i := range p.Instruments {
|
|
ret += i.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (patch Patch) InstrumentForVoice(voice int) (int, error) {
|
|
if voice < 0 {
|
|
return 0, errors.New("voice cannot be negative")
|
|
}
|
|
for i, instr := range patch.Instruments {
|
|
if voice < instr.NumVoices {
|
|
return i, nil
|
|
} else {
|
|
voice -= instr.NumVoices
|
|
}
|
|
}
|
|
return 0, errors.New("voice number is beyond the total voices of an instrument")
|
|
}
|
|
|
|
type Track struct {
|
|
NumVoices int
|
|
Sequence []byte `yaml:",flow"`
|
|
Patterns [][]byte `yaml:",flow"`
|
|
}
|
|
|
|
func (t *Track) Copy() Track {
|
|
sequence := make([]byte, len(t.Sequence))
|
|
copy(sequence, t.Sequence)
|
|
patterns := make([][]byte, len(t.Patterns))
|
|
for i, oldPat := range t.Patterns {
|
|
newPat := make([]byte, len(oldPat))
|
|
copy(newPat, oldPat)
|
|
patterns[i] = newPat
|
|
}
|
|
return Track{
|
|
NumVoices: t.NumVoices,
|
|
Sequence: sequence,
|
|
Patterns: patterns,
|
|
}
|
|
}
|
|
|
|
type Synth interface {
|
|
Render(buffer []float32, maxtime int) (int, int, error)
|
|
Update(patch Patch) error
|
|
Trigger(voice int, note byte)
|
|
Release(voice int)
|
|
}
|
|
|
|
func Render(synth Synth, buffer []float32) error {
|
|
s, _, err := synth.Render(buffer, math.MaxInt32)
|
|
if err != nil {
|
|
return fmt.Errorf("sointu.Render failed: %v", err)
|
|
}
|
|
if s != len(buffer)/2 {
|
|
return errors.New("in sointu.Render, synth.Render should have filled the whole buffer but did not")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type SynthService interface {
|
|
Compile(patch Patch) (Synth, error)
|
|
}
|
|
|
|
type AudioSink interface {
|
|
WriteAudio(buffer []float32) (err error)
|
|
Close() error
|
|
}
|
|
|
|
type AudioSource interface {
|
|
ReadAudio(buffer []float32) (n int, err error)
|
|
Close() error
|
|
}
|
|
|
|
type AudioContext interface {
|
|
Output() AudioSink
|
|
Close() error
|
|
}
|
|
|
|
// UnitParameter documents one parameter that an unit takes
|
|
type UnitParameter struct {
|
|
Name string // thould be found with this name in the Unit.Parameters map
|
|
MinValue int // minimum value of the parameter, inclusive
|
|
MaxValue int // maximum value of the parameter, inclusive
|
|
CanSet bool // if this parameter can be set before hand i.e. through the gui
|
|
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
|
|
}
|
|
|
|
func engineeringTime(sec float64) string {
|
|
if sec < 1e-3 {
|
|
return fmt.Sprintf("%.2f us", sec*1e6)
|
|
} else if sec < 1 {
|
|
return fmt.Sprintf("%.2f ms", sec*1e3)
|
|
}
|
|
return fmt.Sprintf("%.2f s", sec)
|
|
}
|
|
|
|
// UnitTypes documents all the available unit types and if they support stereo variant
|
|
// and what parameters they take.
|
|
var UnitTypes = map[string]([]UnitParameter){
|
|
"add": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"addp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"pop": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"loadnote": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"mul": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"mulp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"push": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"xch": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"distort": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"hold": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"crush": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"gain": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"invgain": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"filter": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"clip": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"pan": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"delay": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
|
|
"compressor": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"speed": []UnitParameter{},
|
|
"out": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"outaux": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"aux": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
|
|
"send": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
|
|
{Name: "unit", MinValue: 0, MaxValue: 63, CanSet: true, CanModulate: false},
|
|
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
|
|
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
|
|
"envelope": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"noise": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"oscillator": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
|
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
|
|
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
|
|
{Name: "samplestart", MinValue: 0, MaxValue: 1720329, CanSet: true, CanModulate: false},
|
|
{Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false},
|
|
{Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
|
|
"loadval": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
|
"receive": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
|
|
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
|
|
"in": []UnitParameter{
|
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
|
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
|
|
}
|
|
|
|
var Ports = make(map[string]([]string))
|
|
|
|
func init() {
|
|
for name, unitType := range UnitTypes {
|
|
unitPorts := make([]string, 0)
|
|
for _, param := range unitType {
|
|
if param.CanModulate {
|
|
unitPorts = append(unitPorts, param.Name)
|
|
}
|
|
}
|
|
Ports[name] = unitPorts
|
|
}
|
|
}
|
|
|
|
type Song struct {
|
|
BPM int
|
|
RowsPerPattern int
|
|
RowsPerBeat int
|
|
Tracks []Track
|
|
Patch Patch
|
|
}
|
|
|
|
func (s *Song) Copy() Song {
|
|
tracks := make([]Track, len(s.Tracks))
|
|
for i, t := range s.Tracks {
|
|
tracks[i] = t.Copy()
|
|
}
|
|
return Song{BPM: s.BPM, RowsPerPattern: s.RowsPerPattern, RowsPerBeat: s.RowsPerBeat, Tracks: tracks, Patch: s.Patch.Copy()}
|
|
}
|
|
|
|
func (s *Song) SequenceLength() int {
|
|
return len(s.Tracks[0].Sequence)
|
|
}
|
|
|
|
func (s *Song) TotalRows() int {
|
|
return s.RowsPerPattern * s.SequenceLength()
|
|
}
|
|
|
|
func (s *Song) SamplesPerRow() int {
|
|
return 44100 * 60 / (s.BPM * s.RowsPerBeat)
|
|
}
|
|
|
|
func (s *Song) FirstTrackVoice(track int) int {
|
|
ret := 0
|
|
for _, t := range s.Tracks[:track] {
|
|
ret += t.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (s *Song) FirstInstrumentVoice(instrument int) int {
|
|
ret := 0
|
|
for _, i := range s.Patch.Instruments[:instrument] {
|
|
ret += i.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (s *Song) TotalTrackVoices() int {
|
|
ret := 0
|
|
for _, t := range s.Tracks {
|
|
ret += t.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// TBD: Where shall we put methods that work on pure domain types and have no dependencies
|
|
// e.g. Validate here
|
|
func (s *Song) Validate() error {
|
|
if s.BPM < 1 {
|
|
return errors.New("BPM should be > 0")
|
|
}
|
|
var patternLen int
|
|
for i, t := range s.Tracks {
|
|
for j, pat := range t.Patterns {
|
|
if i == 0 && j == 0 {
|
|
patternLen = len(pat)
|
|
} else {
|
|
if len(pat) != patternLen {
|
|
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(track.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 (p *Patch) FindSendTarget(i, u int) (int, int, int, error) {
|
|
if i < 0 || i >= len(p.Instruments) {
|
|
return -1, -1, -1, errors.New("instrument index out of range")
|
|
}
|
|
instr := p.Instruments[i]
|
|
if u < 0 || u >= len(instr.Units) {
|
|
return -1, -1, -1, errors.New("unit index out of range")
|
|
}
|
|
unit := instr.Units[u]
|
|
var targetInstrIndex int
|
|
if unit.Parameters["voice"] == 0 {
|
|
targetInstrIndex = i
|
|
} else {
|
|
var err error
|
|
targetInstrIndex, err = p.InstrumentForVoice(unit.Parameters["voice"] - 1)
|
|
if err != nil {
|
|
return -1, -1, -1, errors.New("the target voice was out of range")
|
|
}
|
|
}
|
|
targetInstr := p.Instruments[targetInstrIndex]
|
|
targetUnitIndex := unit.Parameters["unit"]
|
|
if targetUnitIndex < 0 || targetUnitIndex >= len(targetInstr.Units) {
|
|
return targetInstrIndex, -1, -1, errors.New("the target unit was out of range")
|
|
}
|
|
targetUnit := targetInstr.Units[targetUnitIndex]
|
|
port := unit.Parameters["port"]
|
|
if port < 0 {
|
|
return targetInstrIndex, targetUnitIndex, -1, errors.New("the target port was out of range")
|
|
}
|
|
for k, param := range UnitTypes[targetUnit.Type] {
|
|
if param.CanModulate {
|
|
port--
|
|
if port < 0 {
|
|
return targetInstrIndex, targetUnitIndex, k, nil
|
|
}
|
|
}
|
|
}
|
|
return targetInstrIndex, targetUnitIndex, -1, errors.New("the target port was out of range")
|
|
}
|
|
|
|
func (s *Song) ParamHintString(instrIndex, unitIndex int, param string) string {
|
|
if instrIndex < 0 || instrIndex >= len(s.Patch.Instruments) {
|
|
return ""
|
|
}
|
|
instr := s.Patch.Instruments[instrIndex]
|
|
if unitIndex < 0 || unitIndex >= len(instr.Units) {
|
|
return ""
|
|
}
|
|
unit := instr.Units[unitIndex]
|
|
value := unit.Parameters[param]
|
|
switch unit.Type {
|
|
case "envelope":
|
|
switch param {
|
|
case "attack":
|
|
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100)
|
|
case "decay":
|
|
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * (1 - float64(unit.Parameters["sustain"])/128))
|
|
case "release":
|
|
return engineeringTime(math.Pow(2, 24*float64(value)/128) / 44100 * float64(unit.Parameters["sustain"]) / 128)
|
|
}
|
|
case "oscillator":
|
|
switch param {
|
|
case "type":
|
|
switch value {
|
|
case Sine:
|
|
return "Sine"
|
|
case Trisaw:
|
|
return "Trisaw"
|
|
case Pulse:
|
|
return "Pulse"
|
|
case Gate:
|
|
return "Gate"
|
|
case Sample:
|
|
return "Sample"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
case "transpose":
|
|
relvalue := value - 64
|
|
octaves := relvalue / 12
|
|
semitones := relvalue % 12
|
|
if octaves != 0 {
|
|
return fmt.Sprintf("%v oct, %v st", octaves, semitones)
|
|
}
|
|
return fmt.Sprintf("%v st", semitones)
|
|
case "detune":
|
|
return fmt.Sprintf("%v st", float32(value-64)/64.0)
|
|
}
|
|
case "compressor":
|
|
switch param {
|
|
case "attack":
|
|
fallthrough
|
|
case "release":
|
|
alpha := math.Pow(2, -24*float64(value)/128) // alpha is the "smoothing factor" of first order low pass iir
|
|
sec := -1 / (44100 * math.Log(1-alpha)) // from smoothing factor to time constant, https://en.wikipedia.org/wiki/Exponential_smoothing
|
|
return engineeringTime(sec)
|
|
case "ratio":
|
|
return fmt.Sprintf("1 : %.3f", 1-float64(value)/128)
|
|
}
|
|
case "send":
|
|
if param == "voice" || param == "unit" || param == "port" {
|
|
targetVoice := unit.Parameters["voice"]
|
|
if param == "voice" && targetVoice == 0 {
|
|
return "self"
|
|
}
|
|
targetInstrument := instrIndex
|
|
if targetVoice > 0 { // global send, find the instrument
|
|
if targetVoice > s.Patch.TotalVoices() {
|
|
return ""
|
|
}
|
|
targetVoice--
|
|
targetInstrument = 0
|
|
for targetVoice >= s.Patch.Instruments[targetInstrument].NumVoices {
|
|
targetVoice -= s.Patch.Instruments[targetInstrument].NumVoices
|
|
targetInstrument++
|
|
}
|
|
}
|
|
if param == "voice" {
|
|
return fmt.Sprintf("%v (voice %v)", s.Patch.Instruments[targetInstrument].Name, targetVoice)
|
|
}
|
|
targetUnitIndex := unit.Parameters["unit"]
|
|
units := s.Patch.Instruments[targetInstrument].Units
|
|
if targetUnitIndex < 0 || targetUnitIndex >= len(units) {
|
|
return ""
|
|
}
|
|
if param == "unit" {
|
|
return fmt.Sprintf("%v#%v", units[targetUnitIndex].Type, targetUnitIndex)
|
|
}
|
|
port := value
|
|
for _, param := range UnitTypes[units[targetUnitIndex].Type] {
|
|
if param.CanModulate {
|
|
port--
|
|
if port < 0 {
|
|
return param.Name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (u *Unit) StackChange() int {
|
|
switch u.Type {
|
|
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
|
return -1 - u.Parameters["stereo"]
|
|
case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
|
return 1 + u.Parameters["stereo"]
|
|
case "pan":
|
|
return 1 - u.Parameters["stereo"]
|
|
case "speed":
|
|
return -1
|
|
case "send":
|
|
return (-1 - u.Parameters["stereo"]) * u.Parameters["sendpop"]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (u *Unit) StackNeed() int {
|
|
switch u.Type {
|
|
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
|
return 0
|
|
case "mulp", "mul", "add", "addp", "xch":
|
|
return 2 * (1 + u.Parameters["stereo"])
|
|
case "speed":
|
|
return 1
|
|
}
|
|
return 1 + u.Parameters["stereo"]
|
|
}
|
|
|
|
func Play(synth Synth, song Song) ([]float32, error) {
|
|
err := song.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
curVoices := make([]int, len(song.Tracks))
|
|
for i := range curVoices {
|
|
curVoices[i] = song.FirstTrackVoice(i)
|
|
}
|
|
initialCapacity := song.TotalRows() * song.SamplesPerRow() * 2
|
|
buffer := make([]float32, 0, initialCapacity)
|
|
rowbuffer := make([]float32, song.SamplesPerRow()*2)
|
|
for row := 0; row < song.TotalRows(); row++ {
|
|
patternRow := row % song.RowsPerPattern
|
|
pattern := row / song.RowsPerPattern
|
|
for t := range song.Tracks {
|
|
patternIndex := song.Tracks[t].Sequence[pattern]
|
|
note := song.Tracks[t].Patterns[patternIndex][patternRow]
|
|
if note > 0 && note <= 1 { // anything but hold causes an action.
|
|
continue
|
|
}
|
|
synth.Release(curVoices[t])
|
|
if note > 1 {
|
|
curVoices[t]++
|
|
first := song.FirstTrackVoice(t)
|
|
if curVoices[t] >= first+song.Tracks[t].NumVoices {
|
|
curVoices[t] = first
|
|
}
|
|
synth.Trigger(curVoices[t], note)
|
|
}
|
|
}
|
|
tries := 0
|
|
for rowtime := 0; rowtime < song.SamplesPerRow(); {
|
|
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
|
|
rowtime += time
|
|
buffer = append(buffer, rowbuffer[:samples*2]...)
|
|
if tries > 100 {
|
|
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
|
|
}
|
|
}
|
|
}
|
|
return buffer, nil
|
|
}
|