Skip to content

Commit

Permalink
Upsample SYNC and FM inputs to EvenVCO
Browse files Browse the repository at this point in the history
  • Loading branch information
hemmer committed Nov 22, 2024
1 parent 4a0da48 commit cd957f1
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 16 deletions.
85 changes: 85 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

name: Build VCV Rack Plugin
on: [push, pull_request]

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

defaults:
run:
shell: bash

jobs:
build:
name: ${{ matrix.platform }}
runs-on: ubuntu-latest
container:
image: ghcr.io/qno/rack-plugin-toolchain-win-linux
options: --user root
strategy:
fail-fast: false
matrix:
platform: [win-x64, lin-x64]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- 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@v3
with:
path: ${{ env.rack-plugin-toolchain-dir }}/plugin-build
name: ${{ matrix.platform }}

build-mac:
name: mac
runs-on: macos-12
strategy:
fail-fast: false
matrix:
platform: [x64, arm64]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Get Rack-SDK
run: |
pushd $HOME
curl -o Rack-SDK.zip https://vcvrack.com/downloads/Rack-SDK-${{ env.rack-sdk-version }}-mac-${{ matrix.platform }}.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
echo "Plugin architecture '$(lipo -archs plugin.dylib)'"
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
path: dist/*.vcvplugin
name: mac-${{ matrix.platform }}

publish:
name: Publish plugin
runs-on: ubuntu-latest
needs: [build, build-mac]
steps:
- uses: actions/download-artifact@v3
with:
path: _artifacts
- uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: true
title: "Development Build"
files: |
_artifacts/**/*.vcvplugin
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## v2.8.2
* EvenVCO
* Upsample Hard Sync and FM inputs

## v2.8.1
* Noise Plethora
* Fix bug where program choice is wrongly copied between top and bottom sections
Expand Down
4 changes: 3 additions & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"slug": "Befaco",
"version": "2.8.1",
"version": "2.8.2",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
Expand Down Expand Up @@ -349,6 +349,8 @@
"slug": "Bandit",
"name": "Bandit",
"description": "Bandit is a spectral processing playground.",
"manualUrl": "https://www.befaco.org/bandit/",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-bandit",
"tags": [
"Equalizer",
"Filter",
Expand Down
60 changes: 45 additions & 15 deletions src/EvenVCO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct EvenVCO : Module {
configInput(PITCH1_INPUT, "Pitch 1");
configInput(PITCH2_INPUT, "Pitch 2");
configInput(FM_INPUT, "FM");
configInput(SYNC_INPUT, "Sync");
configInput(SYNC_INPUT, "Hard Sync");
configInput(PWM_INPUT, "Pulse Width Modulation");

configOutput(TRI_OUTPUT, "Triangle");
Expand All @@ -52,8 +52,6 @@ struct EvenVCO : Module {
configOutput(SAW_OUTPUT, "Sawtooth");
configOutput(SQUARE_OUTPUT, "Square");

// calculate up/downsampling rates
onSampleRateChange();
}

void onSampleRateChange() override {
Expand All @@ -65,6 +63,13 @@ struct EvenVCO : Module {
}
}

for (int c = 0; c < 4; c++) {
for (int i = 0; i < NUM_UPSAMPLED_INPUTS; i++) {
oversamplerInputs[i][c].setOversamplingIndex(oversamplingIndex);
oversamplerInputs[i][c].reset(sampleRate);
}
}

const float lowFreqRegime = oversampler[0][0].getOversamplingRatio() * 1e-3 * sampleRate;
DEBUG("Low freq regime: %g", lowFreqRegime);
}
Expand Down Expand Up @@ -111,6 +116,12 @@ struct EvenVCO : Module {
return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
}

enum UpsampledInputs {
FM_INPUT_UP,
SYNC_INPUT_UP,
NUM_UPSAMPLED_INPUTS
};
chowdsp::VariableOversampling<6, float_4> oversamplerInputs[NUM_UPSAMPLED_INPUTS][4]; // uses a 2*6=12th order Butterworth filter
chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling

Expand All @@ -131,36 +142,55 @@ struct EvenVCO : Module {
pw = simd::rescale(pw, -1.f, +1.f, 0.f, 1.f);
}

const float_4 fmVoltage = inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * 0.25f;
const float_4 pitch = inputs[PITCH1_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[PITCH2_INPUT].getPolyVoltageSimd<float_4>(c);
const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f);
// floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
// becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
// a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
// 1 / denominator for the second-order FD
const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);

// pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option
// for it to be added back in for hardware compatibility reasons
const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw);

// hard sync
const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);
// input oversampling buffers
float_4* osBufferSync = oversamplerInputs[SYNC_INPUT_UP][c / 4].getOSBuffer();
float_4* osBufferFM = oversamplerInputs[FM_INPUT_UP][c / 4].getOSBuffer();

// upsample hard sync input (if connected)
if (inputs[SYNC_INPUT].isConnected()) {
oversamplerInputs[SYNC_INPUT_UP][c].upsample(inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c));
}
else {
std::fill(osBufferSync, &osBufferSync[oversamplingRatio], float_4::zero());
}
// upsample FM input (if connected)
if (inputs[FM_INPUT].isConnected()) {
oversamplerInputs[FM_INPUT_UP][c].upsample(inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c));
}
else {
std::fill(osBufferFM, &osBufferFM[oversamplingRatio], float_4::zero());
}

float_4* osBufferTri = oversampler[TRI_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSaw = oversampler[SAW_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSin = oversampler[SINE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferSquare = oversampler[SQUARE_OUTPUT][c / 4].getOSBuffer();
float_4* osBufferEven = oversampler[EVEN_OUTPUT][c / 4].getOSBuffer();
for (int i = 0; i < oversamplingRatio; ++i) {
// use upsampled FM input
const float_4 fmVoltage = osBufferFM[i] * 0.25f;
const float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitchKnobs + pitch + fmVoltage);
const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, 1e-6, 0.5f);
// floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator
// becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't
// a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz.
const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3;
// 1 / denominator for the second-order FD
const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase);

phase[c / 4] += deltaBasePhase;
// ensure within [0, 1]
phase[c / 4] -= simd::floor(phase[c / 4]);

const float_4 syncMask = syncTrigger[c / 4].process(osBufferSync[i]);
phase[c / 4] = simd::ifelse(syncMask, 0.5f, phase[c / 4]);

float_4 phases[3]; // phase as extrapolated to the current and two previous samples

phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f);
Expand Down

0 comments on commit cd957f1

Please sign in to comment.