Skip to content

Commit

Permalink
Merge pull request #13 from Jojosito/synth
Browse files Browse the repository at this point in the history
Synth
  • Loading branch information
Jojosito authored Sep 12, 2024
2 parents 1b9eff9 + 73695be commit 4479074
Show file tree
Hide file tree
Showing 73 changed files with 79,145 additions and 360 deletions.
133 changes: 133 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Build VCV Rack Plugin
on: [push, pull_request]

env:
rack-sdk-version: latest
rack-plugin-toolchain-dir: /home/build/rack-plugin-toolchain

defaults:
run:
shell: bash

jobs:

modify-plugin-version:
name: Modify plugin version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
id: plugin-version-cache
with:
path: plugin.json
key: ${{ github.sha }}-${{ github.run_id }}
- run: |
gitrev=`git rev-parse --short HEAD`
pluginversion=`jq -r '.version' plugin.json`
echo "Set plugin version from $pluginversion to $pluginversion-$gitrev"
cat <<< `jq --arg VERSION "$pluginversion-$gitrev" '.version=$VERSION' plugin.json` > plugin.json
# only modify plugin version if no tag was created
if: "! startsWith(github.ref, 'refs/tags/v')"
build:
name: ${{ matrix.platform }}
needs: modify-plugin-version
runs-on: ubuntu-latest
container:
image: ghcr.io/qno/rack-plugin-toolchain-win-linux
options: --user root
strategy:
matrix:
platform: [win-x64, lin-x64]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/cache@v4
id: plugin-version-cache
with:
path: plugin.json
key: ${{ github.sha }}-${{ github.run_id }}
- name: Build plugin
run: |
export PLUGIN_DIR=$GITHUB_WORKSPACE
pushd ${{ env.rack-plugin-toolchain-dir }}
make plugin-build-${{ matrix.platform }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
path: ${{ env.rack-plugin-toolchain-dir }}/plugin-build
name: ${{ matrix.platform }}

build-mac:
name: mac
needs: modify-plugin-version
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
platform: [x64, arm64]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/cache@v4
id: plugin-version-cache
with:
path: plugin.json
key: ${{ github.sha }}-${{ github.run_id }}
- name: Get Rack-SDK
run: |
pushd $HOME
wget -O Rack-SDK.zip https://vcvrack.com/downloads/Rack-SDK-${{ env.rack-sdk-version }}-mac-x64+arm64.zip
unzip Rack-SDK.zip
- name: Build plugin
run: |
CROSS_COMPILE_TARGET_x64=x86_64-apple-darwin
CROSS_COMPILE_TARGET_arm64=arm64-apple-darwin
export RACK_DIR=$HOME/Rack-SDK
export CROSS_COMPILE=$CROSS_COMPILE_TARGET_${{ matrix.platform }}
make dep
make dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
path: dist/*.vcvplugin
name: mac-${{ matrix.platform }}

publish:
name: Publish plugin
# only create a release if a tag was created that is called e.g. v1.2.3
# see also https://vcvrack.com/manual/Manifest#version
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: [build, build-mac]
steps:
- uses: actions/checkout@v4
- uses: FranzDiebold/github-env-vars-action@v2
- name: Check if plugin version matches tag
run: |
pluginversion=`jq -r '.version' plugin.json`
if [ "v$pluginversion" != "${{ env.CI_REF_NAME }}" ]; then
echo "Plugin version from plugin.json 'v$pluginversion' doesn't match with tag version '${{ env.CI_REF_NAME }}'"
exit 1
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref }}
name: Release ${{ env.CI_REF_NAME }}
body: |
${{ env.CI_REPOSITORY_NAME }} VCV Rack Plugin ${{ env.CI_REF_NAME }}
draft: false
prerelease: false
- uses: actions/download-artifact@v4
with:
path: _artifacts
- name: Upload release assets
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: _artifacts/**/*.vcvplugin
tag: ${{ github.ref }}
file_glob: true
101 changes: 98 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ The 'Poly' input determines the polyphony channels of the output.
* 'Drift' adjusts the amount of a random (per voice) drift.
* 'Drift Rate' adjusts the frequency of the random drift.

## Filter
A collection of filters.
* 'Cutoff' sets the filter cutoff frequency.
* 'Reso' sets the filter resonance
* for non-resonant filters, this has no effect
* for comb filters, this controls the feedback
* for diode clippers, this controls the drive
* 'Mode' sets the filter type. A selection of ladder filters, Sallen-Key filters, comb filters and diode clippers are available.
* The 'Mode' can also be set to bypass (if enabled, the 'Post-filter Saturator' will still be active) or mute.

### Context menu options
* 'Oversampling rate' sets the internal oversampling rate.
* 'ODE Solver': The filters are implemented with differential equations, which are solved with numerical methods. 4th order Runkge-Kutta is recommended, the other options use less CPU, but are also less accurate.
* 'Integrator type': This affects the placement of the nonlinearities nl() in the integrators:
* Linear: dx/dt = ω (in - x)
* OTA: dx/dt = ω nl(in - x)
* Transistor: dx/dt = ω (nl(in) - nl(x))
* OTA and Transistor are available with a tanh() nonlinearity, and an alternative softer saturator.
* 'Post-filter Saturator' limits the output to around ±10V.

## Last
A utility module, which allows to map multiple sources to one destination.

Expand Down Expand Up @@ -87,10 +107,10 @@ There are two extra outputs to the right of the control knobs. 'Base control kno
Instead, it can happen that your physical knobs are at the end of their travel, but the on screen knob is not. In that case, you have to turn the physical knob back the whole way, and forth again.

## OnePole
A simple CV controllable 1-pole highpass and lowpass filter.
A simple CV controllable linear 1-pole highpass and lowpass filter.

## OnePoleLP
A simple CV controllable 1-pole lowpass filter.
A simple CV controllable linear 1-pole lowpass filter.

## Oscillators
A pair of analog-style oscillators.
Expand All @@ -107,15 +127,90 @@ Every parameter is CV controllable.

There are two 'V/Oct' inputs, one for each oscillator. You can use the 'Tune' module to add tuning controls.

'Out' outputs the mix of oscillator 1, the sub-oscillator, oscillator 2 and the ring modulator. The output is soft-clipped to ±10V.
'Out' outputs the mix of oscillator 1, the sub-oscillator, oscillator 2 and the ring modulator.

### Context menu options
* 'Oversampling rate': The oscillators use a naive implementation, which is quite CPU friendly, and can therefore be massively oversampled to reduce aliasing.
This is especially useful for FM and sync sounds.
With no oversampling, the oscillators alias a lot.
* 'Anti-aliasing': Apply additional anti-aliasing (with polyBLEPs and polyBLAMPs). This option greatly reduces aliasing, and does not need much additional CPU time. It also works well with sync and FM.
* 'DC blocker': FM and the ring modulator can create a DC offset. Therefore, a DC blocker is enabled by default, but can be disabled in the context menu.
* 'Saturator' limits the output to around ±10V.
* 'LFO mode' lets you use the module as an LFO. It lowers the frequencies of the oscillators to 2 Hz @ 0V, and internally disables oversampling and the DC blocker.

## Spit/Stack
A utility for splitting or layering two synthesizer parts.

Connect the inputs to the corresponding ports of e.g. a [MIDI to CV](https://library.vcvrack.com/Core/MIDIToCVInterface) module, and the outputs of parts A and B two any modules.

If no buttons are active, only part A is played, part B is "disconnected" by setting the number of channels to 0. If "Stack A+B" is activated, both part A and part B play the exact same notes.
If "Split A|B" is activated, part A plays notes below the split point, part B plays notes at and above the split point.
To set the split point, press and hold "Split A|B" while it is activated, and play a note.
The note name of the split point is shown in the small display below.
When "Switch A↔B" is activated, parts A and B are switched.

## Synth
A virtual-analogue polyphonic synthesizer with 2 oscillators, dual filters and unique modulation system.

### Motivation
There is no shortage of virtual analogue synthesizers, both in hardware and software. Why did I create a new one?
The point of this synthesizer is its unique modulation system.

In most other synthesizers, part of the modulation is hardcoded (e.g. V/Oct to oscillator pitch, envelope to amp), part is directly accessible from the panel (e.g. envelope to filter cutoff), others are hidden in menus (e.g. pitch bend range).
Some synthesizers have a vintage knob, which applies a predetermined amount of diverge and drift to certain parameters.
And many synthesizers have a separate modulation matrix with a limited number of entries.
Often, this mod matrix seems like an afterthought. You have a nice knob to control the filter cutoff, but in order to modulate it from the matrix, you have to select filter cutoff as a destination from a menu, and dial in the amount with some encoder. Why not use the nice filter cutoff knob also to control the modulation depth?

Here I unified and streamlined the modulation system. There are almost no hardcoded connections, and every source can modulate every destination at the same time.
Applying a modulation source to a destination is straightforward: press a button, turn a knob.
There are 22 modulation sources, and 50 destinations. That means there is a 1100-slot modulation matrix at your fingertips.

I also find synthesizers with a single filter kind of boring, so I added two filters, with many filter types to chose from.

Since this synthesizer lives inside Rack, it does not have to be a fully self contained synth.
Therefore I deliberately did not add effects, sequencers, an arpeggiator, or voice modes like mono, legato, stack, chord mode etc.
Rack has plenty of options for those, and the modulation system allows external modulation sources to be included easily, as well as to modulate external modules with the 5 individual modulation outputs.

### Overview
On the left side are the (modulation) inputs, on the right side are 5 modulation outputs which can be used to modulate other modules, like a third oscillator or effects.
In the top half are the internal modulation sources: two envelopes, two per-voice LFOs and a global LFO, and two diverge and drift generators.
In the bottom half is the audio path, consisting of two oscillators with FM and ring modulator, a mixer, two filters and an amp.

### Modulation system
All parameters with a ring around them can be modulated. When no assign button is active, the base values of the parameters can be adjusted, and a green ring shows the current value.
If 'Mix Route' is active, a red ring is shown in the mixer section.

Every modulation source has one or more assign buttons. Once an assign button is pressed, it is active and lit bright blue.
The modulation depths for each parameter can be adjusted, the modulation depth is shown with a blue ring.
If 'Mix Route' is active, a purple ring is shown in the mixer section.
By clicking on the assign button once again, the synthesizer goes back into base mode.

The assign buttons are dimly lit, if any modulations are active. Hovering a button with the mouse shows all modulation destinations which are affected.
In the context menu, the modulations for the source can be cleared.

If a parameter is modulated, a little light below the knob is lit. Hovering a knob with the mouse shows all modulation sources which affect the parameter.
In the context menu, the modulations for the destination can be cleared.

In base mode, Racks 'initialize' and 'randomize' functions only change the base values and non-modulatable parameters.
When an assign button is active, 'initialize' and 'randomize' only affect the modulation assignments for the active modulation source.

#### Caveats
Some modulation assignments have to be made in order for Synth to behave 'normally':
- V/Oct modulates Osc 1 and 2 semi by 100%
- An Envelope or Gate modulates amp by 100%
- For pitch bend, I recommend to turn the pitch bend range in the 'MIDI to CV' module to 'Off', and modulate Osc 1 and 2 semi from 'PW' by 2 semitones

### Signal path
There are 6 audio sources: two [oscillators](#oscillators) with sub-oscillator and ring modulator (multiplication of oscillator 1 and 2), noise, and external input. If no external input is connected, 'Ext' controls the loopback of the mono signal before the amp.

The volume for each source can be adjusted and modulated.
When the 'Mix Route' button is active, each source can be routed to filter 1 (-100%), filter 2 (100%) or anything in between.
The routing can also be modulated.

So there are effectively two mix buses, for filter 1 and filter 2.

The [filters](#filter) can be operated in serial (the output of filter 1 is added to the filter 2 mix bus, filter 1 is not routed to the amp, and filter 1 pan has no effect), or in parallel, or anything in between.

## Tune
Tune by octaves, plus coarse and fine (1 semitone) tuning.

Expand Down
Loading

0 comments on commit 4479074

Please sign in to comment.