diff --git a/README.md b/README.md index fa8135b..0a13261 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,27 @@ Prods using Sointu - [21](https://demozoo.org/music/338597/) by NR4 / 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//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 ------------ diff --git a/patch.go b/patch.go index f9b7cd2..eaa29f9 100644 --- a/patch.go +++ b/patch.go @@ -157,6 +157,15 @@ var UnitTypes = map[string]([]UnitParameter){ {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}}, + "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{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}, @@ -317,7 +326,7 @@ 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": + case "envelope", "envelopexp", "oscillator", "push", "noise", "receive", "loadnote", "loadval", "in", "compressor": return 1 + u.Parameters["stereo"] case "pan": return 1 - u.Parameters["stereo"] @@ -337,7 +346,7 @@ func (u *Unit) StackNeed() int { return 0 } switch u.Type { - case "", "envelope", "oscillator", "noise", "receive", "loadnote", "loadval", "in": + case "", "envelope", "envelopexp", "oscillator", "noise", "receive", "loadnote", "loadval", "in": return 0 case "mulp", "mul", "add", "addp", "xch": 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) } + +func envelopExpDisplayFunc(v int) (string, string) { + return fmt.Sprintf("= %.3f", math.Pow(2, float64(v-64)/32)), "" +} diff --git a/tracker/presets.go b/tracker/presets.go index 3cf7bfd..dec40f5 100644 --- a/tracker/presets.go +++ b/tracker/presets.go @@ -44,6 +44,7 @@ func init() { 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}}, + "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}}, "noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}}, "mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}}, diff --git a/vm/compiler/templates/amd64-386/sources.asm b/vm/compiler/templates/amd64-386/sources.asm index 558c46c..da02543 100644 --- a/vm/compiler/templates/amd64-386/sources.asm +++ b/vm/compiler/templates/amd64-386/sources.asm @@ -3,7 +3,7 @@ ; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1] ;------------------------------------------------------------------------------- ; 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"}} {{- if .StereoAndMono "envelope"}} @@ -63,6 +63,129 @@ su_op_envelope_leave2: {{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"}} ;------------------------------------------------------------------------------- ; NOISE opcode: creates noise diff --git a/vm/go_synth.go b/vm/go_synth.go index e288ef2..ffcbdad 100644 --- a/vm/go_synth.go +++ b/vm/go_synth.go @@ -329,6 +329,42 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t if stereo { 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: if stereo { value := waveshape(synth.rand(), params[0]) * params[1] diff --git a/vm/opcodes.go b/vm/opcodes.go index a16263a..8dd308b 100644 --- a/vm/opcodes.go +++ b/vm/opcodes.go @@ -12,27 +12,28 @@ const ( opDelay = 8 opDistort = 9 opEnvelope = 10 - opFilter = 11 - opGain = 12 - opHold = 13 - opIn = 14 - opInvgain = 15 - opLoadnote = 16 - opLoadval = 17 - opMul = 18 - opMulp = 19 - opNoise = 20 - opOscillator = 21 - opOut = 22 - opOutaux = 23 - opPan = 24 - opPop = 25 - opPush = 26 - opReceive = 27 - opSend = 28 - opSpeed = 29 - opSync = 30 - opXch = 31 + opEnvelopexp = 11 + opFilter = 12 + opGain = 13 + opHold = 14 + opIn = 15 + opInvgain = 16 + opLoadnote = 17 + opLoadval = 18 + opMul = 19 + opMulp = 20 + opNoise = 21 + opOscillator = 22 + opOut = 23 + opOutaux = 24 + opPan = 25 + opPop = 26 + opPush = 27 + opReceive = 28 + opSend = 29 + opSpeed = 30 + 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}