Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quantizer v2 #26

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/firmware.yml
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ jobs:
run: |
sudo apt-get update -qq
sudo apt-get install -qq eatmydata
sudo eatmydata apt-get install -qq ninja-build clang-tidy clang-format
sudo eatmydata apt-get install -qq ninja-build clang-tidy clang-format librtmidi-dev

- name: Set up Python 3.9
uses: actions/setup-python@v1
@@ -59,7 +59,7 @@ jobs:
run: |
sudo apt-get update -qq
sudo apt-get install -qq eatmydata
sudo eatmydata apt-get install -qq ninja-build wget
sudo eatmydata apt-get install -qq ninja-build wget librtmidi-dev

- name: Install ARM embedded toolchain
# TODO: Put this file on static.winterbloom.com
@@ -110,7 +110,7 @@ jobs:
run: |
sudo apt-get update -qq
sudo apt-get install -qq eatmydata
sudo eatmydata apt-get install -qq ninja-build wget clang clang-tools
sudo eatmydata apt-get install -qq ninja-build wget clang clang-tools librtmidi-dev

- name: Set up Python 3.9
uses: actions/setup-python@v1
18 changes: 16 additions & 2 deletions factory/libgemini/gem_settings.py
Original file line number Diff line number Diff line change
@@ -9,9 +9,9 @@

@dataclass
class GemSettings(structy.Struct):
_PACK_STRING : ClassVar[str] = "HhHiiiiiiiiiiH??iiiBBii"
_PACK_STRING : ClassVar[str] = "HhHiiiiiiiiiiH??iiiBBii??"

PACKED_SIZE : ClassVar[int] = 72
PACKED_SIZE : ClassVar[int] = 74
"""The total size of the struct once packed."""

adc_gain_corr: int = 2048
@@ -86,3 +86,17 @@ class GemSettings(structy.Struct):

lfo_2_factor: structy.Fix16 = 0
"""LFO 2's factor."""

castor_quantize: bool = False
"""Quantize pitch CV inputs. This only affects the CV input, not the
pitch knob, so that the oscillator can still be tuned. The base CV
offset is added before quantization, so that the quantizer can be
calibrated against an external CV source.

If you find that Castor & Pollux's pitch keeps jumping back and forth
between adjacent notes, this means that the CV source is outputing
values near to the boundary between two notes. Try increasing the base
CV offset by 1/24 (ie, 0.0416...), to recenter the voltages properly.
"""

pollux_quantize: bool = False
2 changes: 1 addition & 1 deletion firmware/configure.py
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@
# The amount of SRAM to set aside for the program stack. This is
# application-specific. Wintertools enables warnings about using more stack
# than available to help tune this.
STACK_SIZE = 0x800
STACK_SIZE = 0x2000

# Each device has a seperate linker script. This was copied from
# third_party/samd21/gcc
15 changes: 15 additions & 0 deletions firmware/data/gem_settings.structy
Original file line number Diff line number Diff line change
@@ -71,3 +71,18 @@ class GemSettings:

"""LFO 2's factor."""
lfo_2_factor: fix16 = 0

# Added in V4

"""Quantize pitch CV inputs. This only affects the CV input, not the
pitch knob, so that the oscillator can still be tuned. The base CV
offset is added before quantization, so that the quantizer can be
calibrated against an external CV source.

If you find that Castor & Pollux's pitch keeps jumping back and forth
between adjacent notes, this means that the CV source is outputing
values near to the boundary between two notes. Try increasing the base
CV offset by 1/24 (ie, 0.0416...), to recenter the voltages properly.
"""
castor_quantize: bool = False
pollux_quantize: bool = False
44 changes: 34 additions & 10 deletions firmware/scripts/samd21g18a.ld
Original file line number Diff line number Diff line change
@@ -55,6 +55,20 @@ BOOTLOADER_SIZE = 0x2000; /* 8kB, 8,192 bytes */
NVM_SIZE = 0x400; /* 1kB, 1,024 bytes */
SRAM_SIZE = 0x8000; /* 32kB, 32,768 bytes */

/*
We reserve a chunk at the end of the flash for "non-volatile memory" (NVM) -
which is used by the application to store per-device information.
This is divided into three sections:

* User-selected quantizer table
* User settings
* DAC calibration lookup table (LUT)
*/
QUANTIZER_SIZE = 0x1000; /* 4kB, 4,096 bytes - TODO reduce once format is settled */
SETTINGS_SIZE = 0x200; /* 0.5kB, 512 bytes */
LUT_SIZE = 0x200; /* 0.5kB, 512 bytes */
NVM_SIZE = QUANTIZER_SIZE + SETTINGS_SIZE + LUT_SIZE;

/*
ARM Cortex-M processors use a descending stack and generally
require stack space to be set aside in RAM.
@@ -73,7 +87,7 @@ SRAM_SIZE = 0x8000; /* 32kB, 32,768 bytes */
* https://gcc.gnu.org/onlinedocs/gnat_ugn/Static-Stack-Usage-Analysis.html
* https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
*/
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x800;
STACK_SIZE = __stack_size__;

/*
Memory space definition.
@@ -530,27 +544,37 @@ SECTIONS
to use.
*/

/*
Symbols for the quantizer section in the NVM memory block.

Gemini uses this section to store the user's custom quantization
table (if any).

References:
* ../src/gem_quantizer.c
*/
_nvm_quantizer_base_address = ORIGIN(nvm);
_nvm_quantizer_length = QUANTIZER_SIZE;

/*
Symbols for the settings section in the NVM memory block.

Gemini uses the first half of the NVM block for user settings.
This symbol is used by the settings module to know where to
load and save settings.
Gemini uses this section to load and save user settings.

References:
* ../src/gem_settings_load_save.c
*/
_nvm_settings_base_address = ORIGIN(nvm);
_nvm_settings_length = LENGTH(nvm) / 2;
_nvm_settings_base_address = ORIGIN(nvm) + QUANTIZER_SIZE;
_nvm_settings_length = SETTINGS_SIZE;

/*
Symbols for the calibration/look-up table in the NVM memory block.

Gemini uses the other half of the NVM block to store the factory-
calibrated look-up table for translating ADC -> frequency/DAC codes.
Gemini uses this section to store the factory-calibrated look-up table
for translating ADC -> frequency/DAC codes.

References:
* ../src/gem_ramp_table_load_save.c
*/
_nvm_lut_base_address = ORIGIN(nvm) + LENGTH(nvm) / 2;
_nvm_lut_length = LENGTH(nvm) / 2;
_nvm_lut_base_address = ORIGIN(nvm) + QUANTIZER_SIZE + SETTINGS_SIZE;
_nvm_lut_length = LUT_SIZE;
1 change: 1 addition & 0 deletions firmware/src/gem.h
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
#include "gem_nvm.h"
#include "gem_oscillator.h"
#include "gem_pulseout.h"
#include "gem_quantizer.h"
#include "gem_settings.h"
#include "gem_settings_load_save.h"
#include "gem_spi.h"
67 changes: 62 additions & 5 deletions firmware/src/gem_oscillator.c
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@

#include "gem_oscillator.h"
#include "gem_config.h"
#include "gem_quantizer.h"
#include "wntr_bezier.h"
#include "wntr_uint12.h"

@@ -38,7 +39,8 @@ void GemOscillator_init(
fix16_t base_offset,
fix16_t knob_min,
fix16_t knob_max,
bool lfo_pwm) {
bool lfo_pwm,
bool quantize) {

osc->number = number;
osc->pitch_cv_channel = pitch_cv_channel;
@@ -51,6 +53,7 @@ void GemOscillator_init(
osc->follower_threshold = 0;
osc->lfo_pwm = lfo_pwm;
osc->lfo_pitch = false;
osc->quantize = quantize;

osc->outputs = (struct GemOscillatorOutputs){};
osc->smooth.initial_gain = smooth_initial_gain;
@@ -59,6 +62,7 @@ void GemOscillator_init(
osc->smooth._lowpass2 = F16(0);
osc->pitch = F16(0);
osc->pulse_width = 2048;
osc->quantizer_bin = 0;
}

void GemOscillator_update(struct GemOscillator* osc, struct GemOscillatorInputs inputs) {
@@ -72,8 +76,13 @@ void GemOscillator_post_update(struct GemOscillator* osc, struct GemOscillatorIn
This is done in post update so that main() can grab Castor's unfiltered
value so if Pollux is following Castor it will be lock-step instead of
lagging slightly behind.

This is not done if quantization is enabled since it would just introduce
unnecessary latency.
*/
osc->pitch = WntrSmoothie_step(&osc->smooth, osc->pitch);
if (!osc->quantize) {
osc->pitch = WntrSmoothie_step(&osc->smooth, osc->pitch);
}

/* Apply LFO to pitch if its enabled for this oscillator. */
if (osc->lfo_pitch) {
@@ -94,8 +103,20 @@ void GemOscillator_post_update(struct GemOscillator* osc, struct GemOscillatorIn

static void calculate_pitch_cv_(struct GemOscillator* osc, struct GemOscillatorInputs inputs) {
/*
The basic pitch CV determination formula is:
(base offset) + (CV in * CV_RANGE) + ((CV knob * KNOB_RANGE) - KNOB_RANGE / 2)
To determine pitch, we sum two terms:
* CV knob (never quantized)
(CV knob * KNOB_RANGE) - KNOB_RANGE / 2
* Pitch CV input (quantized if enabled in the settings)
quantize(base_cv_offset + (CV in * CV_RANGE))

Note that base_cv_offset is included in the quantized part.
This is so that it is possible to calibrate against an external
CV source, so that the pitch CVs it produces get mapped to the
middle of the range for each note, for maximum noise tolerance.

Without this, we could encounter a nasty edge case where the CVs
land right near the boundary between two notes, causing the quantizer
to flip back and forth between two adjacent notes.
*/

uint16_t cv_adc_code = inputs.adc[osc->pitch_cv_channel];
@@ -121,7 +142,43 @@ static void calculate_pitch_cv_(struct GemOscillator* osc, struct GemOscillatorI
*/
else {
fix16_t cv = UINT12_NORMALIZE_F(cv_adc_code_f16);
osc->pitch_cv = fix16_add(osc->base_offset, fix16_mul(GEM_CV_INPUT_RANGE, cv));
fix16_t pitch_cv = fix16_add(osc->base_offset, fix16_mul(GEM_CV_INPUT_RANGE, cv));

if (osc->quantize) {
// TODO: Move this code into a function in gem_quantizer.c
// TODO: Add fast variant which just adds/subtracts 1 from bin number
// instead of doing a binary search. This should converge very
// fast anyway, due to moving by a bin per sample, while
// significantly reducing the worst-case runtime
const fix16_t hysteresis = gem_quantizer_config.hysteresis;
const uint32_t notes_len = gem_quantizer_config.notes_len;
const struct GemQuantizerTableEntry* notes = &gem_quantizer_config.notes[0];

/* Find the upper and lower bounds of the current quantizer bin,
including hysteresis */
fix16_t bin_bottom = notes[osc->quantizer_bin].threshold - hysteresis;
fix16_t bin_top;
if (osc->quantizer_bin == notes_len - 1) {
/*
Prevent reading off the end of the table.
*/
bin_top = INT32_MAX;
} else {
bin_top = notes[osc->quantizer_bin + 1].threshold + hysteresis;
}

if (pitch_cv < bin_bottom || pitch_cv >= bin_top) {
osc->quantizer_bin = GemQuantizer_search_table(pitch_cv);
}

// Should never be hit, but just to be sure...
if (osc->quantizer_bin >= notes_len) {
osc->quantizer_bin = notes_len - 1;
}
pitch_cv = notes[osc->quantizer_bin].output;
}

osc->pitch_cv = pitch_cv;
}

/* Read the pitch knob and normalize (0.0 -> 1.0) its value. */
5 changes: 4 additions & 1 deletion firmware/src/gem_oscillator.h
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ struct GemOscillator {
uint16_t follower_threshold;
bool lfo_pitch;
bool lfo_pwm;
bool quantize;

/* State */
struct GemOscillatorOutputs outputs;
@@ -51,6 +52,7 @@ struct GemOscillator {
uint16_t pulse_width_knob;
uint16_t pulse_width_cv;
uint16_t pulse_width;
uint16_t quantizer_bin;
};

void gem_oscillator_init(struct WntrErrorCorrection pitch_cv_adc_error_correction, fix16_t pitch_knob_nonlinearity);
@@ -67,7 +69,8 @@ void GemOscillator_init(
fix16_t base_offset,
fix16_t knob_min,
fix16_t knob_max,
bool lfo_pwm);
bool lfo_pwm,
bool quantize);

void GemOscillator_update(struct GemOscillator* osc, struct GemOscillatorInputs inputs) RAMFUNC;

Loading