From f52201cc116f09d58338eca705a02577a812e75c Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 2 Jun 2013 00:52:08 -0700 Subject: [PATCH] Add pitch LFO This patch wires up the LFO to the synth unit, and makes it affect pitch (control over amplitude is not there yet). It also adds LFO delay to the base LFO implementation, and there are some other cleanups as well (for example, not unpacking patch data every note). LFO speeds, delays, and pitch modulation ranges have been calibrated against the DX7, but testing hasn't been exhaustive. --- android/jni/Android.mk | 1 + cpp/src/android_glue.cc | 7 +------ cpp/src/dx7note.cc | 21 +++++++++++++++------ cpp/src/dx7note.h | 4 +++- cpp/src/lfo.cc | 28 ++++++++++++++++++++++++++++ cpp/src/lfo.h | 7 +++++++ cpp/src/synth_unit.cc | 32 ++++++++++++++++++++++++++------ cpp/src/synth_unit.h | 11 +++++++++++ 8 files changed, 92 insertions(+), 19 deletions(-) diff --git a/android/jni/Android.mk b/android/jni/Android.mk index c4a9db2..9c3b5d6 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -10,6 +10,7 @@ LOCAL_SRC_FILES := android_glue.cc \ fm_core.cc \ fm_op_kernel.cc \ freqlut.cc \ + lfo.cc \ patch.cc \ pitchenv.cc \ resofilter.cc \ diff --git a/cpp/src/android_glue.cc b/cpp/src/android_glue.cc index 511acff..b8b6ee5 100644 --- a/cpp/src/android_glue.cc +++ b/cpp/src/android_glue.cc @@ -26,9 +26,6 @@ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "synth", __VA_ARGS__) #include "synth.h" -#include "freqlut.h" -#include "exp2.h" -#include "sin.h" #include "synth_unit.h" RingBuffer *ring_buffer; @@ -171,9 +168,7 @@ Java_com_google_synthesizer_android_AndroidGlue_start(JNIEnv *env, assert(SL_RESULT_SUCCESS == result); buffer_size = buf_size; - Freqlut::init(sample_rate); - Exp2::init(); - Sin::init(); + SynthUnit::Init(sample_rate); ring_buffer = new RingBuffer(); stats_ring_buffer = new RingBuffer(); synth_unit = new SynthUnit(ring_buffer); diff --git a/cpp/src/dx7note.cc b/cpp/src/dx7note.cc index 805b7b5..caf3e0d 100644 --- a/cpp/src/dx7note.cc +++ b/cpp/src/dx7note.cc @@ -123,9 +123,11 @@ int ScaleLevel(int midinote, int break_pt, int left_depth, int right_depth, } } -void Dx7Note::init(const char bulk[128], int midinote, int velocity) { - char patch[156]; - UnpackPatch(bulk, patch); // TODO: move this out, take unpacked patch +static const uint8_t pitchmodsenstab[] = { + 0, 10, 20, 33, 55, 92, 153, 255 +}; + +void Dx7Note::init(const char patch[156], int midinote, int velocity) { int rates[4]; int levels[4]; for (int op = 0; op < 6; op++) { @@ -172,17 +174,24 @@ void Dx7Note::init(const char bulk[128], int midinote, int velocity) { algorithm_ = patch[134]; int feedback = patch[135]; fb_shift_ = feedback != 0 ? 8 - feedback : 16; + pitchmoddepth_ = (patch[139] * 165) >> 6; + pitchmodsens_ = pitchmodsenstab[patch[143] & 7]; } -void Dx7Note::compute(int32_t *buf) { - int32_t pitchenvlevel = pitchenv_.getsample(); +void Dx7Note::compute(int32_t *buf, int32_t lfo_val, int32_t lfo_delay) { + int32_t pitchmod = pitchenv_.getsample(); + uint32_t pmd = pitchmoddepth_ * lfo_delay; // Q32 + // TODO: add modulation sources (mod wheel, etc) + int32_t senslfo = pitchmodsens_ * (lfo_val - (1 << 23)); + pitchmod += (((int64_t)pmd) * (int64_t)senslfo) >> 39; + // TODO: add pitch bend for (int op = 0; op < 6; op++) { params_[op].gain[0] = params_[op].gain[1]; int32_t level = env_[op].getsample(); int32_t gain = Exp2::lookup(level - (14 * (1 << 24))); //int32_t gain = pow(2, 10 + level * (1.0 / (1 << 24))); // TODO: probably don't apply pitch env to fixed freq - params_[op].freq = Freqlut::lookup(basepitch_[op] + pitchenvlevel); + params_[op].freq = Freqlut::lookup(basepitch_[op] + pitchmod); params_[op].gain[1] = gain; } core_.compute(buf, params_, algorithm_, fb_buf_, fb_shift_); diff --git a/cpp/src/dx7note.h b/cpp/src/dx7note.h index 8e12159..947967a 100644 --- a/cpp/src/dx7note.h +++ b/cpp/src/dx7note.h @@ -36,7 +36,7 @@ class Dx7Note { // Note: this _adds_ to the buffer. Interesting question whether it's // worth it... - void compute(int32_t *buf); + void compute(int32_t *buf, int32_t lfo_val, int32_t lfo_delay); void keyup(); @@ -57,6 +57,8 @@ class Dx7Note { int32_t fb_shift_; int algorithm_; + int pitchmoddepth_; + int pitchmodsens_; }; #endif // SYNTH_DX7NOTE_H_ diff --git a/cpp/src/lfo.cc b/cpp/src/lfo.cc index 4089ae4..6b86df5 100644 --- a/cpp/src/lfo.cc +++ b/cpp/src/lfo.cc @@ -21,6 +21,8 @@ #include "sin.h" #include "lfo.h" +uint32_t Lfo::unit_; + void Lfo::init(double sample_rate) { // constant is 1 << 32 / 15.5s / 11 Lfo::unit_ = (int32_t)(N * 25190424 / sample_rate + 0.5); @@ -31,6 +33,17 @@ void Lfo::reset(const char params[6]) { int sr = rate == 0 ? 1 : (165 * rate) >> 6; sr *= sr < 160 ? 11 : (11 + ((sr - 160) >> 4)); delta_ = unit_ * sr; + int a = 99 - params[1]; // LFO delay + if (a == 99) { + delayinc_ = ~0u; + delayinc2_ = ~0u; + } else { + a = (16 + (a & 15)) << (1 + (a >> 4)); + delayinc_ = unit_ * a; + a &= 0xff80; + a = max(0x80, a); + delayinc2_ = unit_ * a; + } waveform_ = params[5]; sync_ = params[4] != 0; } @@ -62,8 +75,23 @@ int32_t Lfo::getsample() { return 1 << 23; } +int32_t Lfo::getdelay() { + uint32_t delta = delaystate_ < (1U << 31) ? delayinc_ : delayinc2_; + uint32_t d = delaystate_ + delta; + if (d < delayinc_) { + return 1 << 24; + } + delaystate_ = d; + if (d < (1U << 31)) { + return 0; + } else { + return (d >> 7) & ((1 << 24) - 1); + } +} + void Lfo::keydown() { if (sync_) { phase_ = (1U << 31) - 1; } + delaystate_ = 0; } diff --git a/cpp/src/lfo.h b/cpp/src/lfo.h index 18f4f70..809df1e 100644 --- a/cpp/src/lfo.h +++ b/cpp/src/lfo.h @@ -24,6 +24,9 @@ class Lfo { // result is 0..1 in Q24 int32_t getsample(); + // result is 0..1 in Q24 + int32_t getdelay(); + void keydown(); private: static uint32_t unit_; @@ -33,4 +36,8 @@ class Lfo { uint8_t waveform_; uint8_t randstate_; bool sync_; + + uint32_t delaystate_; + uint32_t delayinc_; + uint32_t delayinc2_; }; diff --git a/cpp/src/synth_unit.cc b/cpp/src/synth_unit.cc index 946150c..bc9e564 100644 --- a/cpp/src/synth_unit.cc +++ b/cpp/src/synth_unit.cc @@ -26,6 +26,10 @@ #include #include "synth.h" +#include "freqlut.h" +#include "sin.h" +#include "exp2.h" +#include "patch.h" #include "synth_unit.h" #include "aligned_buf.h" @@ -41,6 +45,13 @@ char epiano[] = { 69, 46, 80, 73, 65, 78, 79, 32, 49, 32 }; +void SynthUnit::Init(double sample_rate) { + Freqlut::init(sample_rate); + Exp2::init(); + Sin::init(); + Lfo::init(sample_rate); +} + SynthUnit::SynthUnit(RingBuffer *ring_buffer) { ring_buffer_ = ring_buffer; for (int note = 0; note < max_active_notes; ++note) { @@ -93,6 +104,13 @@ int SynthUnit::AllocateNote() { return -1; } +void SynthUnit::ProgramChange(int p) { + current_patch_ = p; + const uint8_t *patch = patch_data_ + 128 * current_patch_; + UnpackPatch((const char *)patch, unpacked_patch_); + lfo_.reset(unpacked_patch_ + 137); +} + int SynthUnit::ProcessMidiMessage(const uint8_t *buf, int buf_size) { uint8_t cmd = buf[0]; uint8_t cmd_type = cmd & 0xf0; @@ -118,13 +136,12 @@ int SynthUnit::ProcessMidiMessage(const uint8_t *buf, int buf_size) { // note on int note_ix = AllocateNote(); if (note_ix >= 0) { + lfo_.keydown(); // TODO: should only do this if # keys down was 0 active_note_[note_ix].midi_note = buf[1]; active_note_[note_ix].keydown = true; active_note_[note_ix].sustained = sustain_; active_note_[note_ix].live = true; - const uint8_t *patch = patch_data_ + 128 * current_patch_; - active_note_[note_ix].dx7_note->init( - (const char *)patch, buf[1], buf[2]); + active_note_[note_ix].dx7_note->init(unpacked_patch_, buf[1], buf[2]); } return 3; } @@ -154,9 +171,9 @@ int SynthUnit::ProcessMidiMessage(const uint8_t *buf, int buf_size) { if (buf_size >= 2) { // program change int program_number = buf[1]; - current_patch_ = min(program_number, 31); + ProgramChange(min(program_number, 31)); char name[11]; - memcpy(name, patch_data_ + 128 * current_patch_ + 118, 10); + memcpy(name, unpacked_patch_ + 145, 10); name[10] = 0; #ifdef VERBOSE std::cout << "Loaded patch " << current_patch_ << ": " << name << "\r"; @@ -172,6 +189,7 @@ int SynthUnit::ProcessMidiMessage(const uint8_t *buf, int buf_size) { if (buf_size >= 4104) { // TODO: check checksum? memcpy(patch_data_, buf + 6, 4096); + ProgramChange(current_patch_); return 4104; } return 0; @@ -218,9 +236,11 @@ void SynthUnit::GetSamples(int n_samples, int16_t *buffer) { for (int j = 0; j < N; ++j) { audiobuf.get()[j] = 0; } + int32_t lfovalue = lfo_.getsample(); + int32_t lfodelay = lfo_.getdelay(); for (int note = 0; note < max_active_notes; ++note) { if (active_note_[note].live) { - active_note_[note].dx7_note->compute(audiobuf.get()); + active_note_[note].dx7_note->compute(audiobuf.get(), lfovalue, lfodelay); } } const int32_t *bufs[] = { audiobuf.get() }; diff --git a/cpp/src/synth_unit.h b/cpp/src/synth_unit.h index a484000..94c0347 100644 --- a/cpp/src/synth_unit.h +++ b/cpp/src/synth_unit.h @@ -15,6 +15,7 @@ */ #include "dx7note.h" +#include "lfo.h" #include "ringbuffer.h" #include "resofilter.h" @@ -28,6 +29,8 @@ struct ActiveNote { class SynthUnit { public: + static void Init(double sample_rate); + explicit SynthUnit(RingBuffer *ring_buffer); void GetSamples(int n_samples, int16_t *buffer); @@ -40,6 +43,9 @@ class SynthUnit { // none available. int AllocateNote(); + // zero-based + void ProgramChange(int p); + int ProcessMidiMessage(const uint8_t *buf, int buf_size); RingBuffer *ring_buffer_; @@ -52,6 +58,11 @@ class SynthUnit { uint8_t patch_data_[4096]; int current_patch_; + char unpacked_patch_[156]; + + // The original DX7 had one single LFO. Later units had an LFO per note. + Lfo lfo_; + ResoFilter filter_; int32_t filter_control_[2]; bool sustain_;