mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
BREAKING CHANGE: The problem with crush was that it had very few usable values. This changes the crush to map the value nonlinearly, so the crush resolution is bits. Still the upper portion of the values is not very usable (bits 12-24 i.e. hardly any crushing), but at least the lower portion is usable. But now crush resolution has slightly different meaning.
253 lines
6.6 KiB
Go
253 lines
6.6 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 "loadval":
|
|
switch param {
|
|
case "value":
|
|
return fmt.Sprintf("%.2f", float32(value)/64-1)
|
|
}
|
|
case "send":
|
|
switch param {
|
|
case "amount":
|
|
return fmt.Sprintf("%.2f", float32(value)/64-1)
|
|
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"
|
|
}
|
|
}
|
|
case "crush":
|
|
switch param {
|
|
case "resolution":
|
|
return fmt.Sprintf("%v bits", 24*float32(value)/128)
|
|
}
|
|
}
|
|
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)
|
|
}
|