Add resonant filter to C++ synth

This change adds a (single) resonant filter to the C++ synth unit, and
wires up both USB MIDI and on-screen controls for cutoff and resonance.

Also fixes a bug in KnobView which caused the knob value to jump around.
bklimt
Raph Levien 13 years ago
parent 8fcd77a762
commit ee72c697c1
  1. 69
      android/res/layout/piano2.xml
  2. 1
      android/res/values/strings.xml
  3. 28
      android/src/com/google/synthesizer/android/ui/PianoActivity2.java
  4. 4
      android/src/com/google/synthesizer/android/widgets/knob/KnobView.java
  5. 6
      cpp/src/module.h
  6. 13
      cpp/src/resofilter.cc
  7. 7
      cpp/src/resofilter.h
  8. 19
      cpp/src/synth_unit.cc
  9. 6
      cpp/src/synth_unit.h

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.google.synthesizer"
android:id="@+id/TableLayout01"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:stretchColumns="*"
android:shrinkColumns="*">
<TableRow>
<TextView
android:text="@string/cutoff"
android:id="@+id/cutoffLabel"
android:gravity="center_horizontal" />
<TextView
android:text="@+string/resonance"
android:id="@+id/resonanceLabel"
android:gravity="center_horizontal" />
<TextView
android:text=""
android:id="@+id/emptyLabel"
android:gravity="center_horizontal" />
<TextView
android:text=""
android:id="@+id/emptyLabel"
android:gravity="center_horizontal" />
<TextView
android:text=""
android:id="@+id/emptyLabel"
android:gravity="center_horizontal" />
<TextView
android:text=""
android:id="@+id/emptyLabel"
android:gravity="center_horizontal" />
</TableRow>
<TableRow>
<com.google.synthesizer.android.widgets.knob.KnobView
android:id="@+id/cutoffKnob"
app:value="1.0"
app:min="0"
app:max="1"
android:layout_margin="2px" />
<com.google.synthesizer.android.widgets.knob.KnobView
android:id="@+id/resonanceKnob"
app:value="0.0"
app:min="0"
app:max="1"
android:layout_margin="2px" />
<Spinner
android:id="@+id/presetSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_span="4" />
</TableRow>
<TableRow>
<LinearLayout
android:layout_height="fill_parent"
android:layout_span="6"
android:orientation="vertical">
<com.google.synthesizer.android.widgets.piano.PianoView
android:id="@+id/piano"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
app:octaves="2"
app:first_octave="4" />
</LinearLayout>
</TableRow>
</TableLayout>

@ -47,6 +47,7 @@
<!-- Low Pass Filter --> <!-- Low Pass Filter -->
<string name="cutoff">Cutoff</string> <string name="cutoff">Cutoff</string>
<string name="resonance">Resonance</string>
<!-- Amp --> <!-- Amp -->
<string name="attack">Attack</string> <string name="attack">Attack</string>

@ -39,10 +39,10 @@ import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ToggleButton;
import com.google.synthesizer.R; import com.google.synthesizer.R;
import com.google.synthesizer.android.AndroidGlue; import com.google.synthesizer.android.AndroidGlue;
import com.google.synthesizer.android.widgets.knob.KnobListener;
import com.google.synthesizer.android.widgets.knob.KnobView; import com.google.synthesizer.android.widgets.knob.KnobView;
import com.google.synthesizer.android.widgets.piano.PianoView; import com.google.synthesizer.android.widgets.piano.PianoView;
@ -57,10 +57,11 @@ public class PianoActivity2 extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.piano); setContentView(R.layout.piano2);
piano_ = (PianoView)findViewById(R.id.piano); piano_ = (PianoView)findViewById(R.id.piano);
volumeKnob_ = (KnobView)findViewById(R.id.volumeKnob); cutoffKnob_ = (KnobView)findViewById(R.id.cutoffKnob);
resonanceKnob_ = (KnobView)findViewById(R.id.resonanceKnob);
presetSpinner_ = (Spinner)findViewById(R.id.presetSpinner); presetSpinner_ = (Spinner)findViewById(R.id.presetSpinner);
androidGlue_ = new AndroidGlue(); androidGlue_ = new AndroidGlue();
@ -89,6 +90,20 @@ public class PianoActivity2 extends Activity {
public void onNothingSelected(AdapterView<?> parent) { public void onNothingSelected(AdapterView<?> parent) {
} }
}); });
cutoffKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 1, value);
}
});
resonanceKnob_.setKnobListener(new KnobListener() {
public void onKnobChanged(double newValue) {
int value = (int)Math.round(newValue * 127);
androidGlue_.onController(0, 2, value);
}
});
piano_.bindTo(androidGlue_); piano_.bindTo(androidGlue_);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
tryConnectUsb(); tryConnectUsb();
@ -134,8 +149,8 @@ public class PianoActivity2 extends Activity {
for (int i = 0; i < nBytes; i += 4) { for (int i = 0; i < nBytes; i += 4) {
int codeIndexNumber = buf[i] & 0xf; int codeIndexNumber = buf[i] & 0xf;
int payloadBytes = 0; int payloadBytes = 0;
if (codeIndexNumber == 8 || codeIndexNumber == 9) { if (codeIndexNumber == 8 || codeIndexNumber == 9 || codeIndexNumber == 11 ||
// TODO: pitchbend, control, etc codeIndexNumber == 14) {
payloadBytes = 3; payloadBytes = 3;
} else if (codeIndexNumber == 12) { } else if (codeIndexNumber == 12) {
payloadBytes = 2; payloadBytes = 2;
@ -173,6 +188,7 @@ public class PianoActivity2 extends Activity {
private AndroidGlue androidGlue_; private AndroidGlue androidGlue_;
private PianoView piano_; private PianoView piano_;
private KnobView volumeKnob_; private KnobView cutoffKnob_;
private KnobView resonanceKnob_;
private Spinner presetSpinner_; private Spinner presetSpinner_;
} }

@ -74,7 +74,7 @@ public class KnobView extends View {
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction(); int action = event.getAction();
switch (action) { switch (action) {
case MotionEvent.ACTION_POINTER_DOWN: { case MotionEvent.ACTION_DOWN: {
// Just record the current finger position. // Just record the current finger position.
getDrawingRect(rect_); getDrawingRect(rect_);
previousX_ = event.getX() - rect_.centerX(); previousX_ = event.getX() - rect_.centerX();
@ -116,7 +116,7 @@ public class KnobView extends View {
break; break;
} }
case MotionEvent.ACTION_POINTER_UP: { case MotionEvent.ACTION_UP: {
break; break;
} }
} }

@ -14,6 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
#ifndef SYNTH_MODULE_H
#define SYNTH_MODULE_H
#include <stdint.h> #include <stdint.h>
class Module { class Module {
@ -23,3 +26,6 @@ class Module {
virtual void process(const int32_t **inbufs, const int32_t *control_in, virtual void process(const int32_t **inbufs, const int32_t *control_in,
const int32_t *control_last, int32_t **outbufs) = 0; const int32_t *control_last, int32_t **outbufs) = 0;
}; };
#endif // SYNTH_MODULE_H

@ -14,7 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
#include "module.h" #include "synth.h"
#include "freqlut.h"
#include "resofilter.h" #include "resofilter.h"
double this_sample_rate; double this_sample_rate;
@ -30,8 +31,8 @@ ResoFilter::ResoFilter() {
} }
int32_t compute_alpha(int32_t logf) { int32_t compute_alpha(int32_t logf) {
// TODO // TODO: better tuning
return 1 << 21; return min(1 << 24, Freqlut::lookup(logf));
} }
void ResoFilter::process(const int32_t **inbufs, const int32_t *control_in, void ResoFilter::process(const int32_t **inbufs, const int32_t *control_in,
@ -42,6 +43,12 @@ void ResoFilter::process(const int32_t **inbufs, const int32_t *control_in,
int32_t k = control_last[1]; int32_t k = control_last[1];
int32_t k_in = control_in[1]; int32_t k_in = control_in[1];
int32_t delta_k = (k_in - k) >> lg_n; int32_t delta_k = (k_in - k) >> lg_n;
if ((((int64_t)alpha_in * (int64_t)k_in) >> 24) > 1 << 24) {
k_in = ((1 << 30) / alpha_in) << 18;
}
if ((((int64_t)alpha * (int64_t)k) >> 24) > 1 << 24) {
k = ((1 << 30) / alpha) << 18;
}
const int32_t *ibuf = inbufs[0]; const int32_t *ibuf = inbufs[0];
int32_t *obuf = outbufs[0]; int32_t *obuf = outbufs[0];
int x0 = x[0]; int x0 = x[0];

@ -14,6 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
#ifndef SYNTH_RESOFILTER_H_
#define SYNTH_RESOFILTER_H_
#include "module.h"
class ResoFilter : Module { class ResoFilter : Module {
public: public:
ResoFilter(); ResoFilter();
@ -25,3 +30,5 @@ class ResoFilter : Module {
private: private:
int32_t x[4]; int32_t x[4];
}; };
#endif // SYNTH_RESOFILTER_H_

@ -44,6 +44,8 @@ SynthUnit::SynthUnit(RingBuffer *ring_buffer) {
memcpy(patch_data_, epiano, sizeof(epiano)); memcpy(patch_data_, epiano, sizeof(epiano));
current_patch_ = 0; current_patch_ = 0;
current_note_ = 0; current_note_ = 0;
filter_control_[0] = 258847126;
filter_control_[1] = 0;
} }
// Transfer as many bytes as possible from ring buffer to input buffer. // Transfer as many bytes as possible from ring buffer to input buffer.
@ -97,6 +99,17 @@ int SynthUnit::ProcessMidiMessage(const uint8_t *buf, int buf_size) {
return 3; return 3;
} }
return 0; return 0;
} else if (cmd_type == 0xb0) {
if (buf_size >= 3) {
int controller = buf[1];
int value = buf[2];
if (controller == 1) {
filter_control_[0] = 129423563 + value * 1019083;
} else if (controller == 2) {
filter_control_[1] = value * 528416;
}
return 3;
} return 0;
} else if (cmd_type == 0xc0) { } else if (cmd_type == 0xc0) {
if (buf_size >= 2) { if (buf_size >= 2) {
// program change // program change
@ -149,6 +162,7 @@ void SynthUnit::GetSamples(int n_samples, int16_t *buffer) {
for (int i = 0; i < n_samples; i += N) { for (int i = 0; i < n_samples; i += N) {
int32_t audiobuf[N]; int32_t audiobuf[N];
int32_t audiobuf2[N];
for (int j = 0; j < N; ++j) { for (int j = 0; j < N; ++j) {
audiobuf[j] = 0; audiobuf[j] = 0;
} }
@ -157,8 +171,11 @@ void SynthUnit::GetSamples(int n_samples, int16_t *buffer) {
active_note_[note].dx7_note->compute(audiobuf); active_note_[note].dx7_note->compute(audiobuf);
} }
} }
const int32_t *bufs[] = { audiobuf };
int32_t *bufs2[] = { audiobuf2 };
filter_.process(bufs, filter_control_, filter_control_, bufs2);
for (int j = 0; j < N; ++j) { for (int j = 0; j < N; ++j) {
int32_t val = audiobuf[j] >> 4; int32_t val = audiobuf2[j] >> 4;
int clip_val = val < -(1 << 24) ? 0x8000 : val >= (1 << 24) ? 0x7fff : int clip_val = val < -(1 << 24) ? 0x8000 : val >= (1 << 24) ? 0x7fff :
val >> 9; val >> 9;
// TODO: maybe some dithering? // TODO: maybe some dithering?

@ -16,6 +16,7 @@
#include "dx7note.h" #include "dx7note.h"
#include "ringbuffer.h" #include "ringbuffer.h"
#include "resofilter.h"
struct ActiveNote { struct ActiveNote {
int midi_note; int midi_note;
@ -35,7 +36,7 @@ class SynthUnit {
int ProcessMidiMessage(const uint8_t *buf, int buf_size); int ProcessMidiMessage(const uint8_t *buf, int buf_size);
RingBuffer *ring_buffer_; RingBuffer *ring_buffer_;
static const int max_active_notes = 16; static const int max_active_notes = 8;
ActiveNote active_note_[max_active_notes]; ActiveNote active_note_[max_active_notes];
int current_note_; int current_note_;
uint8_t input_buffer_[8192]; uint8_t input_buffer_[8192];
@ -43,4 +44,7 @@ class SynthUnit {
uint8_t patch_data_[4096]; uint8_t patch_data_[4096];
int current_patch_; int current_patch_;
ResoFilter filter_;
int32_t filter_control_[2];
}; };

Loading…
Cancel
Save