mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
241 lines
6.3 KiB
Go
241 lines
6.3 KiB
Go
package sointu
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// Patch is simply a list of instruments used in a song
|
|
type Patch []Instrument
|
|
|
|
// Copy makes a deep copy of a Patch.
|
|
func (p Patch) Copy() Patch {
|
|
instruments := make([]Instrument, len(p))
|
|
for i, instr := range p {
|
|
instruments[i] = instr.Copy()
|
|
}
|
|
return instruments
|
|
}
|
|
|
|
// NumVoices returns the total number of voices used in the patch; summing the
|
|
// voices of every instrument
|
|
func (p Patch) NumVoices() int {
|
|
ret := 0
|
|
for _, i := range p {
|
|
ret += i.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// NumDelayLines return the total number of delay lines used in the patch;
|
|
// summing the number of delay lines of every delay unit in every instrument
|
|
func (p Patch) NumDelayLines() int {
|
|
total := 0
|
|
for _, instr := range p {
|
|
for _, unit := range instr.Units {
|
|
if unit.Type == "delay" {
|
|
total += len(unit.VarArgs) * instr.NumVoices
|
|
}
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
// NumSyns return the total number of sync outputs used in the patch; summing
|
|
// the number of sync outputs of every sync unit in every instrument
|
|
func (p Patch) NumSyncs() int {
|
|
total := 0
|
|
for _, instr := range p {
|
|
for _, unit := range instr.Units {
|
|
if unit.Type == "sync" {
|
|
total += instr.NumVoices
|
|
}
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
// FirstVoiceForInstrument returns the index of the first voice of given
|
|
// instrument. For example, if the Patch has three instruments (0, 1 and 2),
|
|
// with 1, 3, 2 voices, respectively, then FirstVoiceForInstrument(0) returns 0,
|
|
// FirstVoiceForInstrument(1) returns 1 and FirstVoiceForInstrument(2) returns
|
|
// 4. Essentially computes just the cumulative sum.
|
|
func (p Patch) FirstVoiceForInstrument(instrIndex int) int {
|
|
ret := 0
|
|
for _, t := range p[:instrIndex] {
|
|
ret += t.NumVoices
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// InstrumentForVoice returns the instrument number for the given voice index.
|
|
// For example, if the Patch has three instruments (0, 1 and 2), with 1, 3, 2
|
|
// voices, respectively, then InstrumentForVoice(0) returns 0,
|
|
// InstrumentForVoice(1) returns 1 and InstrumentForVoice(3) returns 1.
|
|
func (p Patch) InstrumentForVoice(voice int) (int, error) {
|
|
if voice < 0 {
|
|
return 0, errors.New("voice cannot be negative")
|
|
}
|
|
for i, instr := range p {
|
|
if voice < instr.NumVoices {
|
|
return i, nil
|
|
}
|
|
voice -= instr.NumVoices
|
|
}
|
|
return 0, errors.New("voice number is beyond the total voices of an instrument")
|
|
}
|
|
|
|
// FindSendTarget searches the instrument number and unit index for a unit with
|
|
// the given id. Two units should never have the same id, but if they do, then
|
|
// the first match is returned. Id 0 is interpreted as "no id", thus searching
|
|
// for id 0 returns an error. Error is also returned if the searched id is not
|
|
// found.
|
|
func (p Patch) FindSendTarget(id int) (int, int, error) {
|
|
if id == 0 {
|
|
return 0, 0, errors.New("send targets unit id 0")
|
|
}
|
|
for i, instr := range p {
|
|
for u, unit := range instr.Units {
|
|
if unit.ID == id {
|
|
return i, u, nil
|
|
}
|
|
}
|
|
}
|
|
return 0, 0, fmt.Errorf("send targets an unit with id %v, could not find a unit with such an ID in the patch", id)
|
|
}
|
|
|
|
// ParamHintString returns a human readable string representing the current
|
|
// value of a given unit parameter.
|
|
func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
|
|
if instrIndex < 0 || instrIndex >= len(p) {
|
|
return ""
|
|
}
|
|
instr := p[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":
|
|
switch param {
|
|
case "voice":
|
|
if value == 0 {
|
|
return "auto"
|
|
}
|
|
return fmt.Sprintf("%v", value)
|
|
case "target":
|
|
instrIndex, unitIndex, err := p.FindSendTarget(unit.Parameters["target"])
|
|
if err != nil {
|
|
return "invalid target"
|
|
}
|
|
instr := p[instrIndex]
|
|
unit := instr.Units[unitIndex]
|
|
return fmt.Sprintf("%v / %v%v", instr.Name, unit.Type, unitIndex)
|
|
case "port":
|
|
instrIndex, unitIndex, err := p.FindSendTarget(unit.Parameters["target"])
|
|
if err != nil {
|
|
return fmt.Sprintf("%v ???", value)
|
|
}
|
|
portList := Ports[p[instrIndex].Units[unitIndex].Type]
|
|
if value < 0 || value >= len(portList) {
|
|
return fmt.Sprintf("%v ???", value)
|
|
}
|
|
return fmt.Sprintf(portList[value])
|
|
}
|
|
case "delay":
|
|
switch param {
|
|
case "notetracking":
|
|
switch value {
|
|
case 0:
|
|
return "fixed"
|
|
case 1:
|
|
return "tracks pitch"
|
|
case 2:
|
|
return "tracks BPM"
|
|
}
|
|
}
|
|
case "in", "aux":
|
|
switch param {
|
|
case "channel":
|
|
switch value {
|
|
case 0:
|
|
return "left"
|
|
case 1:
|
|
return "right"
|
|
case 2:
|
|
return "aux1 left"
|
|
case 3:
|
|
return "aux1 right"
|
|
case 4:
|
|
return "aux2 left"
|
|
case 5:
|
|
return "aux2 right"
|
|
case 6:
|
|
return "aux3 left"
|
|
case 7:
|
|
return "aux3 right"
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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)
|
|
}
|