diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8dc7b1..5aab32ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). the loudness and peak detector. ([#210][i210]) - More presets from Reaby, and all new and existing presets were normalized roughly to -12 dBFS true peak. ([#211][i211]) +- noisegate unit: suppress signals below a threshold power. Parameters are + the attack (time to close the gate), release (time to open up again) and + hold times (how long, below threshold, to delay the closing) ([#109][i109]) ### Fixed - VSTi queries the host sample rate more robustly. Cubase previously reported diff --git a/patch.go b/patch.go index bbabdd5a..2ef7905c 100644 --- a/patch.go +++ b/patch.go @@ -201,14 +201,14 @@ var UnitTypes = map[string]UnitType{ "gain": { Params: []UnitParameter{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "gain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return strconv.FormatFloat(toDecibel(float64(v)/128), 'g', 3, 64), "dB" }}, + {Name: "gain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: decibelLevelDispFunc}, }, StackUse: stackUseEffect, }, "invgain": { Params: []UnitParameter{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, - {Name: "invgain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return strconv.FormatFloat(toDecibel(128/float64(v)), 'g', 3, 64), "dB" }}, + {Name: "invgain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: decibelInverseDispFunc}, }, StackUse: stackUseEffect, }, @@ -273,20 +273,11 @@ var UnitTypes = map[string]UnitType{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "attack", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc}, {Name: "release", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc}, - {Name: "invgain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { - return strconv.FormatFloat(toDecibel(128/float64(v)), 'g', 3, 64), "dB" - }}, - {Name: "threshold", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { - return strconv.FormatFloat(toDecibel(float64(v)/128), 'g', 3, 64), "dB" - }}, + {Name: "invgain", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: decibelInverseDispFunc}, + {Name: "threshold", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: decibelLevelDispFunc}, {Name: "ratio", MinValue: 0, Default: 64, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(1 - float64(v)/128), "" }}, }, - StackUse: func(u *Unit) StackUse { - if stereo, ok := u.Parameters["stereo"]; ok && stereo == 1 { - return StackUse{Inputs: [][]int{{0, 2, 3}, {1, 2, 3}}, Modifies: []bool{false, false, true, true}, NumOutputs: 4} - } - return StackUse{Inputs: [][]int{{0, 1}}, Modifies: []bool{false, true}, NumOutputs: 2} - }, + StackUse: stackUseCalculateFactor, }, "speed": { Params: []UnitParameter{}, @@ -426,6 +417,16 @@ var UnitTypes = map[string]UnitType{ }, StackUse: stackUseEffect, }, + "gate": { + Params: []UnitParameter{ + {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, + {Name: "release", MinValue: 0, Default: 24, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc}, + {Name: "hold", MinValue: 0, Default: 24, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc}, + {Name: "attack", MinValue: 0, Default: 24, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: compressorTimeDispFunc}, + {Name: "threshold", MinValue: 0, Default: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: decibelLevelDispFunc}, + }, + StackUse: stackUseCalculateFactor, + }, } func stackUseSource(u *Unit) StackUse { @@ -449,6 +450,15 @@ func stackUseEffect(u *Unit) StackUse { return StackUse{Inputs: [][]int{{0}}, Modifies: []bool{true}, NumOutputs: 1} } +// Effects like the Compressor add their calculated factor on top of the stack, +// for greater flexibility (so you usually "mulp" this directly, but can choose otherwise) +func stackUseCalculateFactor(u *Unit) StackUse { + if stereo, ok := u.Parameters["stereo"]; ok && stereo == 1 { + return StackUse{Inputs: [][]int{{0, 2, 3}, {1, 2, 3}}, Modifies: []bool{false, false, true, true}, NumOutputs: 4} + } + return StackUse{Inputs: [][]int{{0, 1}}, Modifies: []bool{false, true}, NumOutputs: 2} +} + // compile errors if interface is not implemented. var _ yaml.Unmarshaler = &ParamMap{} @@ -490,6 +500,14 @@ func compressorTimeDispFunc(v int) (string, string) { return engineeringTime(sec) } +func decibelLevelDispFunc(v int) (string, string) { + return strconv.FormatFloat(toDecibel(float64(v)/128), 'g', 3, 64), "dB" +} + +func decibelInverseDispFunc(v int) (string, string) { + return strconv.FormatFloat(toDecibel(128/float64(v)), 'g', 3, 64), "dB" +} + func engineeringTime(sec float64) (string, string) { if sec < 1e-3 { return fmt.Sprintf("%.2f", sec*1e6), "us" diff --git a/vm/compiler/templates/amd64-386/effects.asm b/vm/compiler/templates/amd64-386/effects.asm index c6cfa5c1..139fd6b2 100644 --- a/vm/compiler/templates/amd64-386/effects.asm +++ b/vm/compiler/templates/amd64-386/effects.asm @@ -491,3 +491,77 @@ su_op_compressor_mono: {{- end}} ret {{- end}} + + + +{{- if .HasOp "gate"}} +;------------------------------------------------------------------------------- +; GATE opcode: push noise gate gain to stack +;------------------------------------------------------------------------------- +; Mono: push g on stack, where g is a suitable gain for the signal +; you can then MULP to actually gate the signal or SEND it somewhere +; Stereo: push g g on stack, where g is calculated using the max(l, r) signal +;------------------------------------------------------------------------------- +{{.Func "su_op_gate" "Opcode"}} + fld st0 ; x x + fmul st0, st0 ; x^2 x +{{- if .StereoAndMono "gate"}} + jnc su_op_gate_mono +{{- end}} +{{- if .Stereo "gate"}} + fld st2 ; r x^2 l r + fst st3 ; y x^2 l r + fmul st0, st0 ; y^2 x^2 l r + faddp st1, st0 ; y^2+x^2 l r +{{- if .StereoAndMono "gate"}} + call su_op_gate_mono + fld st0 + ret +su_op_gate_mono: ; (...==signal) x +{{- end}} +{{- end}} + fld dword [{{.Input "gate" "threshold"}}] ; threshold signal x + fmul st0, st0 ; threshold^2 signal x + fucomip st0, st1 ; signal x + fstp st0 ; x + fld dword [{{.WRK}}+4] ; holding x + jae su_op_gate_holding ;; (signal <= threshold) -> jump + fstp st0 ; x + fld1 ; (1==holding) x + jmp su_op_gate_evaluate +su_op_gate_holding: + mov al, {{.InputNumber "gate" "hold"}} + {{.Call "su_nonlinear_map"}} ; holdrate holding x + fsubp st1, st0 ; (remaining holding) x +su_op_gate_evaluate: + fld dword [{{.WRK}}] ; level holding x + fldz ; 0 level holding x + fucomip st0, st2 ; level holding x + jae su_op_gate_attack ;; if (holding <= 0) -> jump +su_op_gate_release: + mov al, {{.InputNumber "gate" "release"}} + {{.Call "su_nonlinear_map"}} ; release level holding x + faddp st1, st0 ; (level+release) holding x + fld1 ; 1 level' holding x + fucomi st0, st1 ; limit level holding x + jae su_op_gate_leave ;; if (limit >= level) -> jump + fxch ; level limit holding x + jmp su_op_gate_leave ; level (limit==level) holding x +su_op_gate_attack: + mov al, {{.InputNumber "gate" "attack"}} + {{.Call "su_nonlinear_map"}} ; attack level holding x + fsubp st1, st0 ; (level-attack) holding x + fldz ; 0 level' holding x + fucomi st0, st1 ; limit level holding x + jbe su_op_gate_leave ;; if (limit <= level) -> jump + fxch ; level (limit==level) holding x +su_op_gate_leave: + fstp st0 ; level holding x + fst dword [{{.WRK}}] ; level holding x + fxch ; holding level x + fstp dword [{{.WRK}}+4] ; level x +{{- if and (.Stereo "gate") (not (.Mono "gate"))}} + fld st0 ; and return the computed gain two times, ready for MULP STEREO +{{- end}} + ret +{{- end}} diff --git a/vm/go_synth.go b/vm/go_synth.go index 262d8eda..a9b595b7 100644 --- a/vm/go_synth.go +++ b/vm/go_synth.go @@ -616,6 +616,39 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, r unit.state[2+i] = b2*x - a2*y stack[l-1-i] = y } + case opGate: + signal := stack[l-1] * stack[l-1] + if stereo { + signal += stack[l-2] * stack[l-2] + } + threshold := params[3] * params[3] + level := unit.state[0] + holding := unit.state[1] + // attacking is delayed until "holding" did count down to 0 + if signal > threshold { + holding = 1 + release := nonLinearMap(params[0]) + level += release + if level > 1 { + level = 1 + } + } + holding -= nonLinearMap(params[1]) + if holding < 0 { + attack := nonLinearMap(params[2]) + level -= attack + if level < 0 { + level = 0 + } + } + unit.state[0] = level + unit.state[1] = holding + // like the compressor, this does not directly multiply the factor + // but writes it onto the stack for the user to decide what to do + stack = append(stack, level) + if stereo { + stack = append(stack, level) + } case opSync: break default: diff --git a/vm/opcodes.go b/vm/opcodes.go index f3015e1c..89a0b3ee 100644 --- a/vm/opcodes.go +++ b/vm/opcodes.go @@ -15,25 +15,26 @@ const ( opEnvelope = 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 + opGate = 14 + opHold = 15 + opIn = 16 + opInvgain = 17 + opLoadnote = 18 + opLoadval = 19 + opMul = 20 + opMulp = 21 + opNoise = 22 + opOscillator = 23 + opOut = 24 + opOutaux = 25 + opPan = 26 + opPop = 27 + opPush = 28 + opReceive = 29 + opSend = 30 + opSpeed = 31 + opSync = 32 + opXch = 33 ) -var transformCounts = [...]int{0, 0, 1, 3, 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, 3, 0, 5, 1, 1, 4, 1, 5, 2, 1, 4, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}