Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 32 additions & 14 deletions patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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) {
Comment thread
qm210 marked this conversation as resolved.
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{},
Expand Down Expand Up @@ -426,6 +417,16 @@ var UnitTypes = map[string]UnitType{
},
StackUse: stackUseEffect,
},
Comment thread
qm210 marked this conversation as resolved.
"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 {
Expand All @@ -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{}

Expand Down Expand Up @@ -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"
Expand Down
74 changes: 74 additions & 0 deletions vm/compiler/templates/amd64-386/effects.asm
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
33 changes: 33 additions & 0 deletions vm/go_synth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Copy link
Copy Markdown
Owner

@vsariola vsariola Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I toyed a bit with this and how about restructuring it like this:

signal := stack[l-1] * stack[l-1]
if stereo {
	signal += stack[l-2] * stack[l-2]
}
threshold := params[3] * params[3]
// unit.state takes inverse level, to be initialized at 1
level := unit.state[0]
// attacking is delayed until "holding" did count down to 0
if signal > threshold {
	level += nonLinearMap(params[0])
	unit.state[1] = 1
	if level > 1 {
		level = 1
	}
}
holding := unit.state[1]
unit.state[1] = holding - nonLinearMap(params[1])
if holding < 0 {
	level -= nonLinearMap(params[2])
	if level < 0 {
		level = 0
	}
}
unit.state[0] = level
// 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)
}

I think this will help the x86 implementation:

  1. Renumbering the parameters in this order allows we to use xor eax,eax ... inc eax to select the right paramater for su_nonlinear_map
  2. The value 1 that is used to compare the level in the release can be reused to set the hold to 1
  3. The value 0 that is compared to holding value can be reused for the level comparison later, in the attack

Very much untested (the comparison are probably wrong way around, but I just wanted to roughly see how this would look in code; I think it's less bytes?)

{{.Func "su_op_noisegate" "Opcode"}}
    fld     st0                                 ; x x
    fmul    st0, st0                            ; x^2 x
{{- if .StereoAndMono "noisegate"}}
    jnc     su_op_noisegate_mono
{{- end}}
{{- if .Stereo "noisegate"}}
    fld     st2                                 ; r x^2 l r    
    fmul    st0, st0                            ; y^2 x^2 l r
    faddp   st1, st0    
{{- if .StereoAndMono "noisegate"}}
    call    su_op_noisegate_mono
    fld     st0
    ret
su_op_noisegate_mono:                           ; (...==signal) x
{{- end}}
{{- end}}
    xor     eax, eax
    fld     dword [{{.Input "noisegate" "threshold"}}] ; threshold signal
    fmul    st0, st0                            ; threshold^2 signal
    fucomip st0, st1                            ; signal
    fstp    st0                                 ; 
    fld     dword [{{.WRK}}]                    ; level
    jae     su_op_noisegate_no_release             ;; (signal <= threshold) -> jump                
    {{.Call "su_nonlinear_map"}}                ; release level
    faddp   st1, st0                            ; level+release
    fld1                                        ; 1 level+release
    fst     dword [{{.WRK}}+4]                  ; set hold to 1
    fucomi  st0, st1                            
    fcmovnb st0, st1                            ; min(1,level+release) level+release 
    fstp    st1                                 ; min(1,level+release)
su_op_noisegate_no_release:
    inc     eax
    fldz                                        ; 0 level
    fld     dword [{{.WRK}}+4]                  ; h 0 level
    fucomi  st0, st1                            ;     
    {{.Call "su_nonlinear_map"}}                ; holding h 0 level    
    fsubp   st1, st0                            ; h-holding 0 level    
    fstp    dword [{{.WRK}}+4]                  ; 0 level
    jb      su_op_noisegate_no_attack           
    inc     eax
    {{.Call "su_nonlinear_map"}}                ; attack level
    fsubp   st2, st0                            ; 0 level-attack
    fucomi  st0, st1                            ; 
    fcmovb  st0, st1                            ; max(level-attack,0) 0        
    fst     st1, st0                            ; max(level-attack,0) max(level-attack,0)
su_op_noisegate_no_attack:
    fstp    st0                                 ; level
    fst     dword [{{.WRK}}]
    ret

case opSync:
break
default:
Expand Down
41 changes: 21 additions & 20 deletions vm/opcodes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading