Case study: SH-101 subtractive synthesiser

The Roland SH-101 (1982) is a single-oscillator monophonic synth with a ladder filter and a simple ADSR. This circuit reproduces its signal path in Valis. The full Turtle is at examples/sh101.ttl.

Signal path

MidiPitch ──────────────────────────────────► VCO (sawtooth)
                                                    │
MidiPitch × 0.5 ─► Sub-oscillator (square) ─► subGain
                                                    │
                                      Noise ─► noiseGain
                                                    │
LFO (vibrato) ── depth=0 ───────────────────► (VCO freq)
                                                    │
                          ┌─────────────────────────┘
                          ▼
                        Mixer (sum VCO + sub + noise)
                          │ audio
Envelope ──► Scale(400–8000 Hz) ── control ──► Ladder VCF
                          │ audio
Envelope ─────────────────────── control ──► VCA
                          │ audio
                        Output

Key elements

MidiPitch

Outputs the currently held MIDI note as a frequency in Hz. Holds the last value between notes (default A4 = 440 Hz until the first note-on arrives).

:pitch a val:MidiPitch .

:aPitchOsc a val:ControlArc ;
    val:from [ val:node :pitch ; val:port "out" ] ;
    val:to   [ val:node :osc   ; val:port "frequency" ] .

Sub-oscillator

The SH-101 has a dedicated sub-oscillator generating a square wave one or two octaves below the VCO. This circuit implements the one-octave version using val:ControlMultiply to halve the MidiPitch frequency before feeding a second oscillator. The b port is set to 0.5 on the instance; no control arc connects to it, so it keeps that baked value each block.

:subMult a val:ControlMultiply ;
    val:b 0.5 .

:subOsc a val:Oscillator ;
    val:shape 2 .    # square

:aPitchSubMult a val:ControlArc ;
    val:from [ val:node :pitch   ; val:port "out" ] ;
    val:to   [ val:node :subMult ; val:port "a"   ] .

:aSubMultSubOsc a val:ControlArc ;
    val:from [ val:node :subMult ; val:port "out"       ] ;
    val:to   [ val:node :subOsc  ; val:port "frequency" ] .

The "Sub Level" knob (slot 9) controls a val:Gain element between the sub-oscillator and the mixer. Default is -12 dB; reduce to -60 to silence it.

Noise

The SH-101 has a white noise source that blends into the filter input. Here a val:Noise element feeds a level-control val:Gain before the mixer. The "Noise Level" knob (slot 10) defaults to -60 dB (near silent). Raise it for breath-noise texture or to thicken the filter resonance.

Mixer

All three audio sources (VCO, sub-oscillator, noise) are summed in a val:Mixer element before entering the VCF. Multiple audio arcs may target the mixer's in port; the mixer sums them.

:vcfMix a val:Mixer .

:aOscMix    a val:Arc ; val:from [ val:node :osc       ; val:port "out" ] ;
                        val:to   [ val:node :vcfMix    ; val:port "in"  ] .
:aSubGainMix a val:Arc ; val:from [ val:node :subGain  ; val:port "out" ] ;
                         val:to   [ val:node :vcfMix   ; val:port "in"  ] .
:aNoiseGainMix a val:Arc ; val:from [ val:node :noiseGain ; val:port "out" ] ;
                            val:to   [ val:node :vcfMix    ; val:port "in"  ] .

VCA (voltage-controlled amplifier)

Takes a 0–1 control signal at cv and multiplies the audio linearly. Connecting the Envelope output here gives a natural amplitude contour.

:vca a val:VCA .

:aEnvVca a val:ControlArc ;
    val:from [ val:node :env ; val:port "out" ] ;
    val:to   [ val:node :vca ; val:port "cv"  ] .

Control arcs replace fixed values

A control arc that reaches a port replaces the port's declared value every block. The consequence for filter control: you cannot have both a fixed val:cutoff on the Ladder instance and a control arc targeting cutoff — the arc wins every block, making the fixed value unreachable. The Ladder's resting cutoff must live somewhere in the control path, not on the element itself.

:vcf a val:Ladder ;
    val:resonance 0.3 ;
    val:drive     1.0 .
# val:cutoff is intentionally absent - the control arc from :envScale
# sets the cutoff every block, overriding any fixed value here.

Scale - bridging 0–1 to physical units

The Envelope output is 0–1. The Ladder filter expects a cutoff in Hz. val:Scale maps the 0–1 control signal to a [min, max] range. The Scale's min value is the resting cutoff (envelope closed). The Scale's max value is the cutoff at the envelope's peak.

:envScale a val:Scale ;
    val:min 400.0 ;    # Hz - base cutoff when envelope is silent
    val:max 8000.0 .   # Hz - cutoff at envelope peak

Vibrato

The LFO drives the oscillator frequency with val:depth 0.0 by default — set depth to a small positive value (e.g. 0.02 for about a semitone) to introduce vibrato:

:aLfoOsc a val:ControlArc ; val:depth 0.02 ;
    val:from [ val:node :lfo ; val:port "out" ] ;
    val:to   [ val:node :osc ; val:port "frequency" ] .

Processing order

Control sources (MidiPitch, Envelope, Scale) must be processed before the elements they drive, so that each block sees fresh values. Valis's compiler performs a topological sort that includes control arcs — a control arc A → B guarantees A is processed before B in the same block.

Loading the circuit

Load examples/sh101.ttl via File → Load Circuit…, or paste it into the Code tab. The eleven host parameters cover:

Slot Name What it controls
0 Osc Shape Waveform (0=sine … 3=square)
1 Cutoff Filter resting frequency (Hz)
2 Resonance Filter self-resonance (0–1)
3 Attack Envelope attack time (ms)
4 Decay Envelope decay time (ms)
5 Sustain Envelope sustain level (0–1)
6 Release Envelope release time (ms)
7 Filter Peak Filter cutoff at envelope peak (Hz)
8 Vibrato Rate LFO rate (Hz)
9 Sub Level Sub-oscillator level (dB)
10 Noise Level Noise level (dB)