mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-03 04:32:55 -04:00
feat: first draft of "EnvelopExp" unit (in ASM)
This commit is contained in:
21
README.md
21
README.md
@ -511,6 +511,27 @@ Prods using Sointu
|
|||||||
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
- [21](https://demozoo.org/music/338597/) by NR4 / Team210
|
||||||
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
- [Tausendeins](https://www.pouet.net/prod.php?which=96192) by epoqe & Team210
|
||||||
|
|
||||||
|
Notes on Adding New OpCodes
|
||||||
|
---------------------------
|
||||||
|
Think of a great new, not already taken name, and extend
|
||||||
|
* ./patch.go
|
||||||
|
* `UnitTypes`
|
||||||
|
* `StackChange()`
|
||||||
|
* `StackNeed()`
|
||||||
|
* ./tracker/presets.go
|
||||||
|
* `defaultUnits`
|
||||||
|
|
||||||
|
Call `./vm/generate/generate.go` (e.g. `generate go` inside the ./vm folder)
|
||||||
|
|
||||||
|
Now, you need a case distinction inside
|
||||||
|
* ./vm/go_synth.go -> `Render()`
|
||||||
|
* for the Go Bytecode VM
|
||||||
|
* this should work with the next `go build` / `go run`
|
||||||
|
* ./vm/compiler/templates/<architecture>/sources.asm
|
||||||
|
* with your given architecture for `-tags=native`, e.g "amd64-386" or "wasm"
|
||||||
|
* this needs a `ninja sointu` inside your build directory, then `go build` / `go run`
|
||||||
|
* it seems not to be necessary to call `go clean -cache`, but in doubt, give it a try.
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
17
patch.go
17
patch.go
@ -157,6 +157,15 @@ var UnitTypes = map[string]([]UnitParameter){
|
|||||||
{Name: "sustain", 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, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
|
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
|
||||||
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
||||||
|
"envelopexp": []UnitParameter{
|
||||||
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||||
|
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
|
||||||
|
{Name: "exp_attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: envelopExpDisplayFunc},
|
||||||
|
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
|
||||||
|
{Name: "exp_decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: envelopExpDisplayFunc},
|
||||||
|
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
||||||
|
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return engineeringTime(math.Pow(2, 24*float64(v)/128) / 44100) }},
|
||||||
|
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
|
||||||
"noise": []UnitParameter{
|
"noise": []UnitParameter{
|
||||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||||
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
|
||||||
@ -317,7 +326,7 @@ func (u *Unit) StackChange() int {
|
|||||||
switch u.Type {
|
switch u.Type {
|
||||||
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
case "addp", "mulp", "pop", "out", "outaux", "aux":
|
||||||
return -1 - u.Parameters["stereo"]
|
return -1 - u.Parameters["stereo"]
|
||||||
case "envelope", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
case "envelope", "envelopexp", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor":
|
||||||
return 1 + u.Parameters["stereo"]
|
return 1 + u.Parameters["stereo"]
|
||||||
case "pan":
|
case "pan":
|
||||||
return 1 - u.Parameters["stereo"]
|
return 1 - u.Parameters["stereo"]
|
||||||
@ -337,7 +346,7 @@ func (u *Unit) StackNeed() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
switch u.Type {
|
switch u.Type {
|
||||||
case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
case "", "envelope", "envelopexp", "oscillator", "noise", "receive", "loadnote", "loadval", "in":
|
||||||
return 0
|
return 0
|
||||||
case "mulp", "mul", "add", "addp", "xch":
|
case "mulp", "mul", "add", "addp", "xch":
|
||||||
return 2 * (1 + u.Parameters["stereo"])
|
return 2 * (1 + u.Parameters["stereo"])
|
||||||
@ -460,3 +469,7 @@ func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) {
|
|||||||
}
|
}
|
||||||
return 0, 0, fmt.Errorf("could not find a unit with id %v", id)
|
return 0, 0, fmt.Errorf("could not find a unit with id %v", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envelopExpDisplayFunc(v int) (string, string) {
|
||||||
|
return fmt.Sprintf("= %.3f", math.Pow(2, float64(v-64)/32)), ""
|
||||||
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ func init() {
|
|||||||
|
|
||||||
var defaultUnits = map[string]sointu.Unit{
|
var defaultUnits = map[string]sointu.Unit{
|
||||||
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||||
|
"envelopexp": {Type: "envelopexp", Parameters: map[string]int{"stereo": 0, "attack": 64, "exp_attack": 64, "decay": 64, "exp_decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||||
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
||||||
"noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}},
|
"noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}},
|
||||||
"mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
"mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
|
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
|
||||||
;-------------------------------------------------------------------------------
|
;-------------------------------------------------------------------------------
|
||||||
; Mono: push the envelope value on stack
|
; Mono: push the envelope value on stack
|
||||||
; Stereo: push the envelope valeu on stack twice
|
; Stereo: push the envelope value on stack twice
|
||||||
;-------------------------------------------------------------------------------
|
;-------------------------------------------------------------------------------
|
||||||
{{.Func "su_op_envelope" "Opcode"}}
|
{{.Func "su_op_envelope" "Opcode"}}
|
||||||
{{- if .StereoAndMono "envelope"}}
|
{{- if .StereoAndMono "envelope"}}
|
||||||
@ -63,6 +63,129 @@ su_op_envelope_leave2:
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
{{if .HasOp "envelopexp" -}}
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
; envelopexp opcode: pushes an ADSR envelopeXPERIMENTAL value on stack [0,1]
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
; Mono: push the envelopexp value on stack
|
||||||
|
; Stereo: push the envelopexp value on stack twice
|
||||||
|
;-------------------------------------------------------------------------------
|
||||||
|
{{.Func "su_op_envelopexp" "Opcode"}}
|
||||||
|
{{- if .StereoAndMono "envelopexp"}}
|
||||||
|
jnc su_op_envelopexp_mono ; Carry Flag tells us whether Stereo
|
||||||
|
{{- end}}
|
||||||
|
{{- if .Stereo "envelopexp"}}
|
||||||
|
call su_op_envelopexp_mono
|
||||||
|
fld st0 ; clone the mono value to the stack -> makes it stereo
|
||||||
|
ret
|
||||||
|
su_op_envelopexp_mono:
|
||||||
|
{{- end}}
|
||||||
|
; qm210: I read that the general registers are fastest, so for this calculation, store
|
||||||
|
; - r10: the exponent of the current segment (A, D, or the default 0.5)
|
||||||
|
; - in unit.state[3] : the baseline of the current segment ( = sustain for D, S and 0 otherwise)
|
||||||
|
; ( I tried r8 beforehand, didn't make it work. now it is .WRK + 12 )
|
||||||
|
; PS: r9 is always used as temporary space for constants or su_nonlinear_map
|
||||||
|
; and if you change registers that are use somewhere unknown... you know -- danger zone :)
|
||||||
|
{{.Prepare (.Float 0.5)}} ; this produces mov r9, qword FCONST_0_500000
|
||||||
|
mov r10, {{.Use (.Float 0.5)}} ; default exponent = 0.5
|
||||||
|
mov dword [{{.WRK}} + 12], 0 ; default baseline = 0
|
||||||
|
; <-- qm210
|
||||||
|
mov eax, dword [{{.INP}}-su_voice.inputs+su_voice.sustain] ; eax = su_instrument.sustain
|
||||||
|
test eax, eax ; if (eax != 0)
|
||||||
|
jne su_op_envelopexp_process ; goto process
|
||||||
|
mov al, {{.InputNumber "envelopexp" "release"}} ; [state]=RELEASE
|
||||||
|
mov dword [{{.WRK}}], eax ; note that mov al, XXX; mov ..., eax is less bytes than doing it directly
|
||||||
|
su_op_envelopexp_process:
|
||||||
|
mov eax, dword [{{.WRK}}] ; al=[state]
|
||||||
|
fld dword [{{.WRK}}+4] ; x=[level]
|
||||||
|
cmp al, {{.InputNumber "envelopexp" "sustain"}} ; if (al==SUSTAIN)
|
||||||
|
je su_op_envelopexp_sustain
|
||||||
|
su_op_envelopexp_attac:
|
||||||
|
cmp al, {{.InputNumber "envelopexp" "attack"}} ; if (al!=ATTAC)
|
||||||
|
jne short su_op_envelopexp_decay ; goto decay
|
||||||
|
; qm210: see above. if in attack, let r10 point to exp_attack
|
||||||
|
lea r10, [{{.Input "envelopexp" "exp_attack"}}]
|
||||||
|
{{.Call "su_nonlinear_map"}} ; a x, where a=attack
|
||||||
|
faddp st1, st0 ; a+x
|
||||||
|
fld1 ; 1 a+x
|
||||||
|
fucomi st1 ; if (a+x<=1) // is attack complete?
|
||||||
|
fcmovnb st0, st1 ; a+x a+x
|
||||||
|
jbe short su_op_envelopexp_statechange ; else goto statechange
|
||||||
|
su_op_envelopexp_decay:
|
||||||
|
; <-- qm210: storing baseline
|
||||||
|
cmp al, {{.InputNumber "envelopexp" "decay"}} ; if (al!=DECAY)
|
||||||
|
jne short su_op_envelopexp_release ; goto release
|
||||||
|
; qm210: see above. if in decay, let r10 point to exp_decay, and load the sustain into unit.state[3]
|
||||||
|
lea r10, [{{.Input "envelopexp" "exp_decay"}}]
|
||||||
|
fld dword [{{.Input "envelopexp" "sustain"}}]
|
||||||
|
fstp dword[{{.WRK}} + 12]
|
||||||
|
; <-- qm210
|
||||||
|
{{.Call "su_nonlinear_map"}} ; d x, where d=decay
|
||||||
|
fsubp st1, st0 ; x-d
|
||||||
|
; qm210: we can ignore the sustain here, it will be applied via the "baseline" (cf. above / below)
|
||||||
|
fldz ; 0 x-d
|
||||||
|
fucomi st1 ; if (x-d>0) // is decay complete?
|
||||||
|
fcmovb st0, st1 ; x-d x-d
|
||||||
|
jnc short su_op_envelopexp_statechange ; else goto statechange
|
||||||
|
su_op_envelopexp_release:
|
||||||
|
cmp al, {{.InputNumber "envelopexp" "release"}} ; if (al!=RELEASE)
|
||||||
|
jne short su_op_envelopexp_applyexp ; goto leave
|
||||||
|
mov dword [{{.WRK}} + 12], 0 ; <-- qm210: no baseline anymore
|
||||||
|
{{.Call "su_nonlinear_map"}} ; r x, where r=release
|
||||||
|
fsubp st1, st0 ; x-r
|
||||||
|
fldz ; 0 x-r
|
||||||
|
fucomi st1 ; if (x-r>0) // is release complete?
|
||||||
|
fcmovb st0, st1 ; x-r x-r, then goto leave
|
||||||
|
jc short su_op_envelopexp_skipexp
|
||||||
|
su_op_envelopexp_statechange:
|
||||||
|
; qm210: this was:
|
||||||
|
; inc dword [{{.WRK}}] ; [state]++
|
||||||
|
; but as we land here after attack and decay, which now have another exp_ parameter, skip 2 to reach next state
|
||||||
|
add dword [{{.WRK}}], 2 ; [state]+=2
|
||||||
|
su_op_envelopexp_applyexp:
|
||||||
|
fstp st1 ; x', where x' is the new value
|
||||||
|
; qm120: store the linear envelope in [level] because this is read again for the next value (cf. "envelope")
|
||||||
|
fst dword [{{.WRK}} + 4] ; [level]=x'
|
||||||
|
; qm210: NOW THE ACTUAL EXPONENTIAL SCALING
|
||||||
|
; - scale the exponent in [0; 1] to [0.125; 8], call that kappa = 2^(6*(expo-0.5))
|
||||||
|
fld dword [r10] ; stack: [ expo, x' ]
|
||||||
|
fld qword [{{.Use (.Float 0.5)}}] ; stack: [ 0.5, expo, x' ]
|
||||||
|
fsubp st1, st0 ; stack: [ expo-0.5, x' ]
|
||||||
|
{{.Prepare (.Int 6)}}
|
||||||
|
fimul dword [{{.Use (.Int 6)}}] ; stack: [ 6*(expo-0.5), x' ]
|
||||||
|
{{.Call "su_power"}} ; stack: [ kappa, x' ]
|
||||||
|
fxch st1 ; stack: [ x', kappa ]
|
||||||
|
; - now we need (x')^(kappa), but care for x' == 0 first
|
||||||
|
fldz ; stack: [ 0, x', kappa ]
|
||||||
|
fucomip st1 ; stack [ x', kappa ] and ZF = (x' == 0)
|
||||||
|
jz su_op_envelopexp_avoid_zero_glitch
|
||||||
|
; - still around? calculate the actual x'' = x^kappa then
|
||||||
|
fyl2x ; stack: [ kappa * log2 x' ]
|
||||||
|
{{.Call "su_power"}} ; stack: [ x ^ kappa ]
|
||||||
|
jmp short su_op_envelopexp_applybaseline
|
||||||
|
su_op_envelopexp_avoid_zero_glitch:
|
||||||
|
fstp st1
|
||||||
|
su_op_envelopexp_applybaseline:
|
||||||
|
; - and scale the result to a different baseline: x''' = (B + (1 - B) * x'') for B != 0 (check not required)
|
||||||
|
fld dword [{{.WRK}} + 12] ; stack: [ B, x'' ]
|
||||||
|
fld1 ; stack: [ 1, B, x'' ]
|
||||||
|
fsub st0, st1 ; stack: [ 1-B, B, x'' ]
|
||||||
|
fmulp st2, st0 ; stack: [ (1-B) * x'', B ]
|
||||||
|
faddp st1, st0 ; stack: [ (1-B) * x'' + B ]
|
||||||
|
jmp short su_op_envelopexp_leave
|
||||||
|
su_op_envelopexp_sustain:
|
||||||
|
; qm210: overwrite level, because else the release cannot work
|
||||||
|
fld dword [{{.Input "envelopexp" "sustain"}}]
|
||||||
|
su_op_envelopexp_skipexp:
|
||||||
|
fst dword [{{.WRK}} + 4]
|
||||||
|
fstp st1
|
||||||
|
su_op_envelopexp_leave:
|
||||||
|
; qm210: scaling because I use my wave editor as a function plotter ;)
|
||||||
|
fmul dword [{{.Input "envelopexp" "gain"}}] ; [gain]*x''
|
||||||
|
ret
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
{{- if .HasOp "noise"}}
|
{{- if .HasOp "noise"}}
|
||||||
;-------------------------------------------------------------------------------
|
;-------------------------------------------------------------------------------
|
||||||
; NOISE opcode: creates noise
|
; NOISE opcode: creates noise
|
||||||
|
|||||||
@ -329,6 +329,42 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
|||||||
if stereo {
|
if stereo {
|
||||||
stack = append(stack, output)
|
stack = append(stack, output)
|
||||||
}
|
}
|
||||||
|
case opEnvelopexp:
|
||||||
|
// TODO @qm210 - for now, this just clones the envelope so everything is wired up, but IT IS NOT IMPLEMENTED YET
|
||||||
|
// is ignoring for now
|
||||||
|
// - params[1] - attack shape parameter (center value should mean "linear")
|
||||||
|
// - params[3] - decay shape parameter (center value should mean "linear")
|
||||||
|
if !voices[0].sustain {
|
||||||
|
unit.state[0] = envStateRelease // set state to release
|
||||||
|
}
|
||||||
|
state := unit.state[0]
|
||||||
|
level := unit.state[1]
|
||||||
|
switch state {
|
||||||
|
case envStateAttack:
|
||||||
|
level += nonLinearMap(params[0])
|
||||||
|
if level >= 1 {
|
||||||
|
level = 1
|
||||||
|
state = envStateDecay
|
||||||
|
}
|
||||||
|
case envStateDecay:
|
||||||
|
level -= nonLinearMap(params[2])
|
||||||
|
if sustain := params[4]; level <= sustain {
|
||||||
|
level = sustain
|
||||||
|
}
|
||||||
|
case envStateRelease:
|
||||||
|
level -= nonLinearMap(params[5])
|
||||||
|
if level <= 0 {
|
||||||
|
level = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unit.state[0] = state
|
||||||
|
unit.state[1] = level
|
||||||
|
output := level * params[6]
|
||||||
|
stack = append(stack, output)
|
||||||
|
if stereo {
|
||||||
|
stack = append(stack, output)
|
||||||
|
}
|
||||||
|
// <-- END TODO @qm210 -- ACTUALLY IMPLEMENT, BUT FIRST DO THE NATIVE ASM PART
|
||||||
case opNoise:
|
case opNoise:
|
||||||
if stereo {
|
if stereo {
|
||||||
value := waveshape(synth.rand(), params[0]) * params[1]
|
value := waveshape(synth.rand(), params[0]) * params[1]
|
||||||
|
|||||||
@ -12,27 +12,28 @@ const (
|
|||||||
opDelay = 8
|
opDelay = 8
|
||||||
opDistort = 9
|
opDistort = 9
|
||||||
opEnvelope = 10
|
opEnvelope = 10
|
||||||
opFilter = 11
|
opEnvelopexp = 11
|
||||||
opGain = 12
|
opFilter = 12
|
||||||
opHold = 13
|
opGain = 13
|
||||||
opIn = 14
|
opHold = 14
|
||||||
opInvgain = 15
|
opIn = 15
|
||||||
opLoadnote = 16
|
opInvgain = 16
|
||||||
opLoadval = 17
|
opLoadnote = 17
|
||||||
opMul = 18
|
opLoadval = 18
|
||||||
opMulp = 19
|
opMul = 19
|
||||||
opNoise = 20
|
opMulp = 20
|
||||||
opOscillator = 21
|
opNoise = 21
|
||||||
opOut = 22
|
opOscillator = 22
|
||||||
opOutaux = 23
|
opOut = 23
|
||||||
opPan = 24
|
opOutaux = 24
|
||||||
opPop = 25
|
opPan = 25
|
||||||
opPush = 26
|
opPop = 26
|
||||||
opReceive = 27
|
opPush = 27
|
||||||
opSend = 28
|
opReceive = 28
|
||||||
opSpeed = 29
|
opSend = 29
|
||||||
opSync = 30
|
opSpeed = 30
|
||||||
opXch = 31
|
opSync = 31
|
||||||
|
opXch = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 1, 4, 1, 5, 7, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user