Tweaking of the sawtooth algorithm

This change makes the saw do most of the slices without interpolation.
It also uses a fixed frequency for the entire 64 sample buffer, rather
than trying to sweep it linearly (the impact on quality is inaudible).

The calculation of slices is done with actual freq relative to sample
rate, so it should be more robust to sample rate changes. And the
strategy to use is lifted up out of the inner loop.

The total number of slices computed is reduced on both the low and high
ends (from 64 to 36).

Lastly, there's a very clever trick: the slice boundary is placed so
that the interpolation zone falls evenly between notes in 12tet in 44.1
and 48 kHz sampling rates, so the interpolated path will be very
unlikely in practice.
master
Raph Levien 10 years ago
parent 7e0f289ed7
commit 102484e439
  1. 6
      cpp/src/SynthApp.xcodeproj/project.pbxproj
  2. 2
      cpp/src/SynthApp/SynthAppDelegate.h
  3. 68
      cpp/src/core.xcodeproj/project.pbxproj
  4. 5
      cpp/src/main.cc
  5. 133
      cpp/src/sawtooth.cc
  6. 10
      cpp/src/sawtooth.h

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 45;
objects = {
/* Begin PBXBuildFile section */
@ -182,11 +182,7 @@
};
buildConfigurationList = F2D5F35CD2FF169326ADC8E8 /* Build configuration list for PBXProject "SynthApp" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 1;
knownRegions = (
en,
);
mainGroup = EEAFDBA4970041AA25C22652;
projectDirPath = "";
projectReferences = (

@ -16,7 +16,7 @@
#import <Cocoa/Cocoa.h>
#import <SynthMain.h>
#import "SynthMain.h"
@interface SynthAppDelegate : NSObject <NSApplicationDelegate> {
NSWindow *window;

@ -7,20 +7,22 @@
objects = {
/* Begin PBXBuildFile section */
2DD71F3E9AE0C4253B8B7747 /* exp2.cc in Sources */ = {isa = PBXBuildFile; fileRef = D5669C7FDAF95F10639CF5AB /* exp2.cc */; };
391223C6D7249361C00A14AF /* dx7note.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA00975977E2704F74104728 /* dx7note.cc */; };
3DFFE98F73F04D2F66DA9FA4 /* env.cc in Sources */ = {isa = PBXBuildFile; fileRef = B73D485E55EBD9CD5950A375 /* env.cc */; };
50C9BC20B9DB9E51A9B852BC /* test_ringbuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 521793C71CAA078F5598EFC0 /* test_ringbuffer.cc */; };
6E01450F6BFF126216C8164C /* ringbuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 68FD17910F296961541A67E4 /* ringbuffer.cc */; };
79D1CB0F9A4671DA79729544 /* fm_op_kernel.cc in Sources */ = {isa = PBXBuildFile; fileRef = 509D811344DB98984FD6C126 /* fm_op_kernel.cc */; };
97788F06DA1810835D9F0A50 /* lfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 42D2A2AD8EDACBEF9B884718 /* lfo.cc */; };
AA0F43347EBDEE6D29032DAF /* sin.cc in Sources */ = {isa = PBXBuildFile; fileRef = D1D8B6FB01C9E7E2D99378F0 /* sin.cc */; };
AA1CD32CDCFEB4469067D98E /* resofilter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 97A5CBACD479212282D0BFD6 /* resofilter.cc */; };
B64D26CA7CC8017052D3E963 /* patch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 779AFCD7AFEB1CDEACBC0425 /* patch.cc */; };
D80FBC17492647DCFDE96F75 /* fm_core.cc in Sources */ = {isa = PBXBuildFile; fileRef = DA7AAEE2AD874001F6B71D52 /* fm_core.cc */; };
E2649182D734B708D5E72B66 /* synth_unit.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2082841A11DF6E62596265CF /* synth_unit.cc */; };
E31A9F6965B9E56115BAE59F /* sawtooth.cc in Sources */ = {isa = PBXBuildFile; fileRef = 48B6535400CF3AC8BABB3299 /* sawtooth.cc */; };
FB78A2E9904695ABCC799CC3 /* freqlut.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9FF02D488CE6FD5017D7D81A /* freqlut.cc */; };
15B76856F0421489F3EEA593 /* freqlut.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9FF02D488CE6FD5017D7D81A /* freqlut.cc */; };
1A75F75619C78DB07A6085B0 /* exp2.cc in Sources */ = {isa = PBXBuildFile; fileRef = D5669C7FDAF95F10639CF5AB /* exp2.cc */; };
20C8EA4478F3F0E2429332F4 /* sawtooth.cc in Sources */ = {isa = PBXBuildFile; fileRef = 48B6535400CF3AC8BABB3299 /* sawtooth.cc */; };
2300749E4E3C2F5E2E47B813 /* fm_core.cc in Sources */ = {isa = PBXBuildFile; fileRef = DA7AAEE2AD874001F6B71D52 /* fm_core.cc */; };
25F144B38B846E3C5850E7BD /* dx7note.cc in Sources */ = {isa = PBXBuildFile; fileRef = BA00975977E2704F74104728 /* dx7note.cc */; };
2A6CCD05E26CB8A8DCB0070C /* log2.cc in Sources */ = {isa = PBXBuildFile; fileRef = 9A44725EA215BBEA625F264E /* log2.cc */; };
413968CF2316B4C71CC32B83 /* ringbuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 68FD17910F296961541A67E4 /* ringbuffer.cc */; };
539ED55158DD0A1FC841C627 /* lfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 42D2A2AD8EDACBEF9B884718 /* lfo.cc */; };
5BFBFDCD915B0AE650CE6798 /* patch.cc in Sources */ = {isa = PBXBuildFile; fileRef = 779AFCD7AFEB1CDEACBC0425 /* patch.cc */; };
9F42FBCD78CEF75B6A043433 /* env.cc in Sources */ = {isa = PBXBuildFile; fileRef = B73D485E55EBD9CD5950A375 /* env.cc */; };
B4D196C5D9E56B0238F25397 /* fm_op_kernel.cc in Sources */ = {isa = PBXBuildFile; fileRef = 509D811344DB98984FD6C126 /* fm_op_kernel.cc */; };
B9A52CE8D262A4428581D53C /* resofilter.cc in Sources */ = {isa = PBXBuildFile; fileRef = 97A5CBACD479212282D0BFD6 /* resofilter.cc */; };
D541FD687D3305820E5F8FE2 /* sin.cc in Sources */ = {isa = PBXBuildFile; fileRef = D1D8B6FB01C9E7E2D99378F0 /* sin.cc */; };
E071B9E5AD56CCD871968387 /* synth_unit.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2082841A11DF6E62596265CF /* synth_unit.cc */; };
E5B6FED47570BBD53CD55329 /* pitchenv.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB03474DCAA7D2AC7378C51E /* pitchenv.cc */; };
EB08E41E39595C13C10972F1 /* test_ringbuffer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 521793C71CAA078F5598EFC0 /* test_ringbuffer.cc */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -34,7 +36,9 @@
779AFCD7AFEB1CDEACBC0425 /* patch.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = patch.cc; sourceTree = "<group>"; };
8B1FC9FF853D5C32F4771091 /* libcore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcore.a; sourceTree = BUILT_PRODUCTS_DIR; };
97A5CBACD479212282D0BFD6 /* resofilter.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = resofilter.cc; sourceTree = "<group>"; };
9A44725EA215BBEA625F264E /* log2.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = log2.cc; sourceTree = "<group>"; };
9FF02D488CE6FD5017D7D81A /* freqlut.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = freqlut.cc; sourceTree = "<group>"; };
AB03474DCAA7D2AC7378C51E /* pitchenv.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = pitchenv.cc; sourceTree = "<group>"; };
B73D485E55EBD9CD5950A375 /* env.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = env.cc; sourceTree = "<group>"; };
BA00975977E2704F74104728 /* dx7note.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = dx7note.cc; sourceTree = "<group>"; };
D1D8B6FB01C9E7E2D99378F0 /* sin.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = sin.cc; sourceTree = "<group>"; };
@ -56,13 +60,13 @@
19BC3E7E3EDF760717F26DF5 = {
isa = PBXGroup;
children = (
1B875637F4FC04501E9170B8 /* Source */,
7BF99477CB1A413FA62AFE65 /* Source */,
E53EB7DD466ADEEC9F094623 /* Products */,
8512968DD6AB145582824E65 /* Build */,
);
sourceTree = "<group>";
};
1B875637F4FC04501E9170B8 /* Source */ = {
7BF99477CB1A413FA62AFE65 /* Source */ = {
isa = PBXGroup;
children = (
BA00975977E2704F74104728 /* dx7note.cc */,
@ -72,7 +76,9 @@
509D811344DB98984FD6C126 /* fm_op_kernel.cc */,
9FF02D488CE6FD5017D7D81A /* freqlut.cc */,
42D2A2AD8EDACBEF9B884718 /* lfo.cc */,
9A44725EA215BBEA625F264E /* log2.cc */,
779AFCD7AFEB1CDEACBC0425 /* patch.cc */,
AB03474DCAA7D2AC7378C51E /* pitchenv.cc */,
97A5CBACD479212282D0BFD6 /* resofilter.cc */,
68FD17910F296961541A67E4 /* ringbuffer.cc */,
48B6535400CF3AC8BABB3299 /* sawtooth.cc */,
@ -143,20 +149,22 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
391223C6D7249361C00A14AF /* dx7note.cc in Sources */,
3DFFE98F73F04D2F66DA9FA4 /* env.cc in Sources */,
2DD71F3E9AE0C4253B8B7747 /* exp2.cc in Sources */,
D80FBC17492647DCFDE96F75 /* fm_core.cc in Sources */,
79D1CB0F9A4671DA79729544 /* fm_op_kernel.cc in Sources */,
FB78A2E9904695ABCC799CC3 /* freqlut.cc in Sources */,
97788F06DA1810835D9F0A50 /* lfo.cc in Sources */,
B64D26CA7CC8017052D3E963 /* patch.cc in Sources */,
AA1CD32CDCFEB4469067D98E /* resofilter.cc in Sources */,
6E01450F6BFF126216C8164C /* ringbuffer.cc in Sources */,
E31A9F6965B9E56115BAE59F /* sawtooth.cc in Sources */,
AA0F43347EBDEE6D29032DAF /* sin.cc in Sources */,
E2649182D734B708D5E72B66 /* synth_unit.cc in Sources */,
50C9BC20B9DB9E51A9B852BC /* test_ringbuffer.cc in Sources */,
25F144B38B846E3C5850E7BD /* dx7note.cc in Sources */,
9F42FBCD78CEF75B6A043433 /* env.cc in Sources */,
1A75F75619C78DB07A6085B0 /* exp2.cc in Sources */,
2300749E4E3C2F5E2E47B813 /* fm_core.cc in Sources */,
B4D196C5D9E56B0238F25397 /* fm_op_kernel.cc in Sources */,
15B76856F0421489F3EEA593 /* freqlut.cc in Sources */,
539ED55158DD0A1FC841C627 /* lfo.cc in Sources */,
2A6CCD05E26CB8A8DCB0070C /* log2.cc in Sources */,
5BFBFDCD915B0AE650CE6798 /* patch.cc in Sources */,
E5B6FED47570BBD53CD55329 /* pitchenv.cc in Sources */,
B9A52CE8D262A4428581D53C /* resofilter.cc in Sources */,
413968CF2316B4C71CC32B83 /* ringbuffer.cc in Sources */,
20C8EA4478F3F0E2429332F4 /* sawtooth.cc in Sources */,
D541FD687D3305820E5F8FE2 /* sin.cc in Sources */,
E071B9E5AD56CCD871968387 /* synth_unit.cc in Sources */,
EB08E41E39595C13C10972F1 /* test_ringbuffer.cc in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -20,6 +20,7 @@
#include "synth.h"
#include "module.h"
#include "aligned_buf.h"
#include "freqlut.h"
#include "wavout.h"
#include "sawtooth.h"
@ -287,8 +288,8 @@ int main(int argc, char **argv) {
//int32_t freq = atoi(argv[1]);
//cout << "Logfreq(" << freq << ") = " << Freqlut::lookup(freq) << endl;
mkdx7note(sample_rate);
//mksaw(sample_rate);
//mkdx7note(sample_rate);
mksaw(sample_rate);
//test_ringbuffer();
test_exp2();
return 0;

@ -18,41 +18,39 @@
#include "module.h"
#include "sawtooth.h"
#include "freqlut.h"
#include "exp2.h"
// There's a fair amount of lookup table and so on that needs to be set before
// generating any signal. In Java, this would be done by a separate factory class.
// Here, we're just going to do it as globals.
#define HIGH_QUALITY
#define noLOW_FREQ_HACK
#define FANCY_GOERTZEL_SIN
#define noPRINT_ERROR
#define R (1 << 29)
#ifdef LOW_FREQ_HACK
#define LG_N_SAMPLES 9
#else
#define LG_N_SAMPLES 11
#endif
#define LG_N_SAMPLES 10
#define N_SAMPLES (1 << LG_N_SAMPLES)
#define N_PARTIALS_MAX (N_SAMPLES / 2)
#define LG_SLICES_PER_OCTAVE 2
#define SLICES_PER_OCTAVE (1 << LG_SLICES_PER_OCTAVE)
#define SLICE_SHIFT (24 - LG_SLICES_PER_OCTAVE)
#define SLICE_EXTRA 3
#define LG_N_SLICES (LG_SLICES_PER_OCTAVE + 4)
#define N_SLICES (1 << LG_N_SLICES)
#define N_SLICES 36
// 0.5 * (log(440./44100) / log(2) + log(440./48000) / log(2) + 2./12) + 1./64 - 3 in Q24
#define SLICE_BASE 161217316
#define LOW_FREQ_LIMIT (-SLICE_BASE)
#define NEG2OVERPI -0.63661977236758138
int32_t sawtooth[N_SLICES][N_SAMPLES];
int32_t sawtooth_freq_off;
void Sawtooth::init(double sample_rate) {
sawtooth_freq_off = -(1 << 24) * log(sample_rate) / log(2);
int32_t lut[N_SAMPLES / 2];
for (int i = 0; i < N_SAMPLES / 2; i++) {
@ -60,11 +58,12 @@ void Sawtooth::init(double sample_rate) {
}
double slice_inc = pow(2.0, 1.0 / SLICES_PER_OCTAVE);
double f_0 = pow(slice_inc, N_SLICES - 1);
double f_0 = pow(slice_inc, N_SLICES - 1) * pow(0.5, SLICE_BASE * 1.0 / (1 << 24));
int n_partials_last = 0;
for (int j = N_SLICES - 1; j >= 0; j--) {
int n_partials = floor(0.5 * sample_rate / f_0);
int n_partials = floor(0.5 / f_0);
n_partials = n_partials < N_PARTIALS_MAX ? n_partials : N_PARTIALS_MAX;
//printf("slice %d: n_partials=%d\n", j, n_partials);
for (int k = n_partials_last + 1; k <= n_partials; k++) {
double scale = NEG2OVERPI / k;
scale = (N_PARTIALS_MAX - k) > (N_PARTIALS_MAX >> 2) ? scale :
@ -94,7 +93,7 @@ void Sawtooth::init(double sample_rate) {
int abs_err = err > 0 ? err : -err;
maxerr = abs_err > maxerr ? abs_err : maxerr;
#endif
ds += ((int64_t)cm2 * (int64_t)s + R) >> 29;
ds += ((int64_t)cm2 * (int64_t)s + (1 << 28)) >> 29;
s += (ds + round) >> dshift;
}
#else
@ -135,56 +134,82 @@ Sawtooth::Sawtooth() {
phase = 0;
}
int32_t Sawtooth::lookup(int32_t phase, int32_t log_f) {
int32_t Sawtooth::compute(int32_t phase) {
return phase * 2 - (1 << 24);
}
log_f = log_f < 0 ? 0 : log_f;
int slice = (log_f + (1 << SLICE_SHIFT) - 1) >> SLICE_SHIFT;
int phase_int = (phase >> (24 - LG_N_SAMPLES)) & (N_SAMPLES - 1);
int lowbits = phase & ((1 << (24 - LG_N_SAMPLES)) - 1);
int y0 = sawtooth[slice][phase_int];
int y1 = sawtooth[slice][(phase_int + 1) & (N_SAMPLES - 1)];
int32_t Sawtooth::lookup_1(int32_t phase, int slice) {
int32_t phase_int = (phase >> (24 - LG_N_SAMPLES)) & (N_SAMPLES - 1);
int32_t lowbits = phase & ((1 << (24 - LG_N_SAMPLES)) - 1);
int32_t y0 = sawtooth[slice][phase_int];
int32_t y1 = sawtooth[slice][(phase_int + 1) & (N_SAMPLES - 1)];
int y4 = y0 + ((((int64_t)(y1 - y0) * (int64_t)lowbits)) >> (24 - LG_N_SAMPLES));
return y0 + ((((int64_t)(y1 - y0) * (int64_t)lowbits)) >> (24 - LG_N_SAMPLES));
}
// TODO: lift this out of loop
// TODO: optimal threshold probably depends on sample rate
#ifdef LOW_FREQ_HACK
if (log_f < (8 << 24))
y4 = phase * 2 - (1 << 24);
#endif
int32_t Sawtooth::lookup_2(int32_t phase, int slice, int32_t slice_lowbits) {
int32_t phase_int = (phase >> (24 - LG_N_SAMPLES)) & (N_SAMPLES - 1);
int32_t lowbits = phase & ((1 << (24 - LG_N_SAMPLES)) - 1);
int32_t y0 = sawtooth[slice][phase_int];
int32_t y1 = sawtooth[slice][(phase_int + 1) & (N_SAMPLES - 1)];
int32_t y4 = y0 + ((((int64_t)(y1 - y0) * (int64_t)lowbits)) >> (24 - LG_N_SAMPLES));
#ifdef HIGH_QUALITY
int y2 = sawtooth[slice + 1][phase_int];
int y3 = sawtooth[slice + 1][(phase_int + 1) & (N_SAMPLES - 1)];
int y5 = y2 + ((((int64_t)(y3 - y2) * (int64_t)lowbits)) >> (24 - LG_N_SAMPLES));
#ifdef LOW_FREQ_HACK
if (log_f < (8 << 24) - (1 << SLICE_SHIFT))
y5 = phase * 2 - (1 << 24);
#endif
int slice_lowbits = log_f & ((1 << SLICE_SHIFT) - 1);
int y = y4 + ((((int64_t)(y5 - y4) * (int64_t)slice_lowbits)) >> SLICE_SHIFT);
return y;
#else
return y4;
#endif
int32_t y2 = sawtooth[slice + 1][phase_int];
int32_t y3 = sawtooth[slice + 1][(phase_int + 1) & (N_SAMPLES - 1)];
int32_t y5 = y2 + ((((int64_t)(y3 - y2) * (int64_t)lowbits)) >> (24 - LG_N_SAMPLES));
return y4 + ((((int64_t)(y5 - y4) * (int64_t)slice_lowbits)) >> (SLICE_SHIFT - SLICE_EXTRA));
}
void Sawtooth::process(const int32_t **inbufs, const int32_t *control_in,
const int32_t *control_last, int32_t **outbufs) {
int32_t logf = control_last[0];
int32_t logf_in = control_in[0];
int32_t *obuf = outbufs[0];
int32_t delta_logf = (logf_in - logf) >> lg_n;
int f = Freqlut::lookup(logf);
int f_in = Freqlut::lookup(logf_in);
int32_t delta_f = (f_in - f) >> lg_n;
int32_t actual_logf = logf + sawtooth_freq_off;
int f = Exp2::lookup(actual_logf);
int32_t p = phase;
for (int i = 0; i < n; i++) {
f += delta_f;
logf += delta_logf;
obuf[i] = lookup(p, logf);
p += f;
p &= (1 << 24) - 1;
// choose a strategy based on the frequency
if (actual_logf < LOW_FREQ_LIMIT - (1 << (SLICE_SHIFT - SLICE_EXTRA))) {
for (int i = 0; i < n; i++) {
obuf[i] = compute(p);
p += f;
p &= (1 << 24) - 1;
}
} else if (actual_logf < LOW_FREQ_LIMIT) {
// interpolate between computed and lookup
int slice = (LOW_FREQ_LIMIT + SLICE_BASE + (1 << SLICE_SHIFT) - 1) >> SLICE_SHIFT;
int slice_lowbits = actual_logf - LOW_FREQ_LIMIT + (1 << (SLICE_SHIFT - SLICE_EXTRA));
for (int i = 0; i < n; i++) {
int32_t yc = compute(p);
int32_t yl = lookup_1(p, slice + 1);
obuf[i] = yc + ((((int64_t)(yl - yc) * (int64_t)slice_lowbits)) >> (SLICE_SHIFT - SLICE_EXTRA));
p += f;
p &= (1 << 24) - 1;
}
} else {
int slice = (actual_logf + SLICE_BASE + (1 << SLICE_SHIFT) - 1) >> SLICE_SHIFT;
const int slice_start = (1 << SLICE_SHIFT) - (1 << (SLICE_SHIFT - SLICE_EXTRA));
int slice_lowbits = ((actual_logf + SLICE_BASE) & ((1 << SLICE_SHIFT) - 1)) - slice_start;
// slice < 0 can't happen because LOW_FREQ_LIMIT kicks in first
if (slice > N_SLICES - 2) {
if (slice > N_SLICES - 1 || slice_lowbits > 0) {
slice = N_SLICES - 1;
slice_lowbits = 0;
}
}
if (slice_lowbits <= 0) {
for (int i = 0; i < n; i++) {
obuf[i] = lookup_1(p, slice);
p += f;
p &= (1 << 24) - 1;
}
} else {
for (int i = 0; i < n; i++) {
obuf[i] = lookup_2(p, slice, slice_lowbits);
p += f;
p &= (1 << 24) - 1;
}
}
}
phase = p;
}

@ -19,9 +19,17 @@ class Sawtooth : Module {
Sawtooth();
static void init(double sample_rate);
static int32_t lookup(int32_t phase, int32_t log_f);
void process(const int32_t **inbufs, const int32_t *control_in,
const int32_t *control_last, int32_t **outbufs);
private:
int32_t phase;
// Compute mathematical function with no antialiasing
static int32_t compute(int32_t phase);
// Lookup from a single slice
static int32_t lookup_1(int32_t phase, int slice);
// Lookup from two slices, interpolate
static int32_t lookup_2(int32_t phase, int slice, int32_t slice_lowbits);
};

Loading…
Cancel
Save