Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed946aa48 | |||
| e4a489ae48 | |||
| 5baec48c28 | |||
| bec6599f35 | |||
| 2476bab44f | |||
| 023b8192a5 | |||
| 5afc5ff18a | |||
| 9b3e4deb30 | |||
| 4dbf88afcb | |||
| 22eea12b17 | |||
| 3ec3204cd8 | |||
| e5e156ce32 | |||
| 9485c74961 |
52
clip/wavefolder.h
Normal file
52
clip/wavefolder.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
namespace trnr {
|
||||
class wavefolder {
|
||||
public:
|
||||
float amount = 1.f;
|
||||
|
||||
void process_sample(float& _sample)
|
||||
{
|
||||
if (amount > 1.f) { _sample = fold(_sample * amount); }
|
||||
}
|
||||
|
||||
private:
|
||||
// folds the wave from -1 to 1
|
||||
float fold(float _sample)
|
||||
{
|
||||
while (_sample > 1.0 || _sample < -1.0) {
|
||||
|
||||
if (_sample > 1.0) {
|
||||
_sample = 2.0 - _sample;
|
||||
} else if (_sample < -1.0) {
|
||||
_sample = -2.0 - _sample;
|
||||
}
|
||||
}
|
||||
|
||||
return _sample;
|
||||
}
|
||||
|
||||
// folds the positive part of the wave independently from the negative part.
|
||||
float fold_bipolar(float _sample)
|
||||
{
|
||||
// fold positive values
|
||||
if (_sample > 1.0) {
|
||||
_sample = 2.0 - _sample;
|
||||
|
||||
if (_sample < 0.0) { _sample = -_sample; }
|
||||
|
||||
return fold(_sample);
|
||||
}
|
||||
// fold negative values
|
||||
else if (_sample < -1.0) {
|
||||
_sample = -2.0 - _sample;
|
||||
|
||||
if (_sample > 0.0) { _sample = -_sample; }
|
||||
|
||||
return fold(_sample);
|
||||
} else {
|
||||
return _sample;
|
||||
}
|
||||
}
|
||||
};
|
||||
}; // namespace trnr
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "../clip/wavefolder.h"
|
||||
#include "tx_envelope.h"
|
||||
#include "tx_sineosc.h"
|
||||
|
||||
@@ -8,27 +9,35 @@ public:
|
||||
tx_operator()
|
||||
: ratio {1}
|
||||
, amplitude {1.0f}
|
||||
, envelope_enabled {true}
|
||||
, velocity_enabled {true}
|
||||
{
|
||||
}
|
||||
|
||||
tx_envelope envelope;
|
||||
tx_sineosc oscillator;
|
||||
wavefolder folder;
|
||||
float ratio;
|
||||
float amplitude;
|
||||
bool envelope_enabled;
|
||||
bool velocity_enabled;
|
||||
|
||||
float process_sample(const bool& gate, const bool& trigger, const float& frequency, const float& velocity,
|
||||
const float& pm = 0)
|
||||
{
|
||||
float env = 1.f;
|
||||
|
||||
float env = envelope.process_sample(gate, trigger);
|
||||
|
||||
// drifts and sounds better!
|
||||
if (envelope.is_busy()) {
|
||||
double osc = oscillator.process_sample(trigger, frequency, pm);
|
||||
return osc * env * velocity;
|
||||
} else {
|
||||
return 0.;
|
||||
if (envelope_enabled) {
|
||||
env = envelope.process_sample(gate, trigger);
|
||||
if (!envelope.is_busy()) return 0.f;
|
||||
}
|
||||
|
||||
float osc = oscillator.process_sample(trigger, frequency, pm);
|
||||
folder.process_sample(osc);
|
||||
|
||||
float adjusted_velocity = velocity_enabled ? velocity : 1.f;
|
||||
|
||||
return osc * env * adjusted_velocity;
|
||||
}
|
||||
|
||||
void set_samplerate(double samplerate)
|
||||
|
||||
153
synth/tx_voice.h
153
synth/tx_voice.h
@@ -8,15 +8,21 @@
|
||||
|
||||
namespace trnr {
|
||||
|
||||
enum mod_dest {
|
||||
mod_dest_out = 0,
|
||||
mod_dest_fm,
|
||||
mod_dest_am
|
||||
};
|
||||
|
||||
template <typename t_sample>
|
||||
class tx_voice : public ivoice<t_sample> {
|
||||
public:
|
||||
tx_voice()
|
||||
: algorithm {0}
|
||||
, pitch_env_amt {0.f}
|
||||
: pitch_env_amt {0.f}
|
||||
, feedback_amt {0.f}
|
||||
, bit_resolution(12.f)
|
||||
{
|
||||
set_glide_time(0.f);
|
||||
}
|
||||
|
||||
bool gate = false;
|
||||
@@ -25,7 +31,9 @@ public:
|
||||
float velocity = 1.f;
|
||||
float additional_pitch_mod = 0.f; // modulates pitch in frequency
|
||||
|
||||
int algorithm;
|
||||
mod_dest op2_dest = mod_dest_fm;
|
||||
mod_dest op3_dest = mod_dest_fm;
|
||||
|
||||
float pitch_env_amt;
|
||||
float feedback_amt;
|
||||
float bit_resolution;
|
||||
@@ -35,6 +43,8 @@ public:
|
||||
tx_operator op2;
|
||||
tx_operator op3;
|
||||
|
||||
void set_glide_time(float time_ms) { glide = 1 - exp(-1.0 / (time_ms * samplerate / 1000.f)); }
|
||||
|
||||
void note_on(int _note, float _velocity) override
|
||||
{
|
||||
this->gate = true;
|
||||
@@ -50,40 +60,24 @@ public:
|
||||
|
||||
void process_samples(t_sample** _outputs, int _start_index, int _block_size) override
|
||||
{
|
||||
float frequency = midi_to_frequency(midi_note + pitch_mod + additional_pitch_mod);
|
||||
target_frequency = midi_to_frequency(midi_note + pitch_mod + additional_pitch_mod);
|
||||
|
||||
for (int s = _start_index; s < _start_index + _block_size; s++) {
|
||||
|
||||
// calculate moving average for portamento
|
||||
current_frequency = (1 - glide) * current_frequency + glide * target_frequency;
|
||||
|
||||
float pitch_env_signal = pitch_env.process_sample(gate, trigger) * pitch_env_amt;
|
||||
float pitched_freq = frequency + pitch_env_signal;
|
||||
float pitched_freq = current_frequency + pitch_env_signal;
|
||||
|
||||
float output = 0.f;
|
||||
|
||||
// mix operator signals according to selected algorithm
|
||||
switch (algorithm) {
|
||||
case 0:
|
||||
output = calc_algo1(pitched_freq);
|
||||
break;
|
||||
case 1:
|
||||
output = calc_algo2(pitched_freq);
|
||||
break;
|
||||
case 2:
|
||||
output = calc_algo3(pitched_freq);
|
||||
break;
|
||||
case 3:
|
||||
output = calc_algo4(pitched_freq);
|
||||
break;
|
||||
default:
|
||||
output = calc_algo1(pitched_freq);
|
||||
break;
|
||||
}
|
||||
float signal = process_operators(pitched_freq);
|
||||
|
||||
// reset trigger
|
||||
trigger = false;
|
||||
|
||||
redux(output, bit_resolution);
|
||||
redux(signal, bit_resolution);
|
||||
|
||||
_outputs[0][s] += output / 3.;
|
||||
_outputs[0][s] += signal / 3.;
|
||||
_outputs[1][s] = _outputs[0][s];
|
||||
}
|
||||
}
|
||||
@@ -93,13 +87,14 @@ public:
|
||||
return gate || op1.envelope.is_busy() || op2.envelope.is_busy() || op3.envelope.is_busy();
|
||||
}
|
||||
|
||||
void set_samplerate(double samplerate) override
|
||||
void set_samplerate(double _samplerate) override
|
||||
{
|
||||
pitch_env.set_samplerate(samplerate);
|
||||
feedback_osc.set_samplerate(samplerate);
|
||||
op1.set_samplerate(samplerate);
|
||||
op2.set_samplerate(samplerate);
|
||||
op3.set_samplerate(samplerate);
|
||||
samplerate = _samplerate;
|
||||
pitch_env.set_samplerate(_samplerate);
|
||||
feedback_osc.set_samplerate(_samplerate);
|
||||
op1.set_samplerate(_samplerate);
|
||||
op2.set_samplerate(_samplerate);
|
||||
op3.set_samplerate(_samplerate);
|
||||
}
|
||||
|
||||
void set_phase_reset(bool phase_reset)
|
||||
@@ -119,80 +114,76 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
double samplerate;
|
||||
const float MOD_INDEX_COEFF = 4.f;
|
||||
float pitch_mod = 0.f; // modulates pitch in semi-tones
|
||||
|
||||
float calc_algo1(const float frequency)
|
||||
float current_frequency;
|
||||
float target_frequency;
|
||||
float glide;
|
||||
|
||||
float process_op3(const float frequency)
|
||||
{
|
||||
float fb_freq = frequency * op3.ratio;
|
||||
float fb_mod_index = (feedback_amt * MOD_INDEX_COEFF);
|
||||
float fb_mod_index = feedback_amt * MOD_INDEX_COEFF;
|
||||
float fb_signal = feedback_osc.process_sample(trigger, fb_freq) * fb_mod_index;
|
||||
|
||||
float op3_Freq = frequency * op3.ratio;
|
||||
float op3_mod_index = (op3.amplitude * MOD_INDEX_COEFF);
|
||||
float op3_signal = op3.process_sample(gate, trigger, op3_Freq, velocity, fb_signal) * op3_mod_index;
|
||||
|
||||
float op2_freq = frequency * op2.ratio;
|
||||
float op2_mod_index = (op2.amplitude * MOD_INDEX_COEFF);
|
||||
float op2_signal = op2.process_sample(gate, trigger, op2_freq, velocity, op3_signal) * op2_mod_index;
|
||||
|
||||
float op1_freq = frequency * op1.ratio;
|
||||
return op1.process_sample(gate, trigger, op1_freq, velocity, op2_signal) * op1.amplitude;
|
||||
return op3.process_sample(gate, trigger, op3_Freq, velocity, fb_signal) * op3.amplitude;
|
||||
}
|
||||
|
||||
float calc_algo2(const float frequency)
|
||||
float process_op2(const float frequency, const float modulator)
|
||||
{
|
||||
float fb_freq = frequency * op3.ratio;
|
||||
float fb_mod_index = (feedback_amt * MOD_INDEX_COEFF);
|
||||
float fb_signal = feedback_osc.process_sample(trigger, fb_freq) * fb_mod_index;
|
||||
// if patched, op3 modulates the phase of op2
|
||||
float pm = op3_dest == mod_dest_fm ? modulator : 0.f;
|
||||
|
||||
float op3_freq = frequency * op3.ratio;
|
||||
float op3_signal = op3.process_sample(gate, trigger, op3_freq, velocity, fb_signal) * op3.amplitude;
|
||||
float adjusted_freq = frequency * op2.ratio;
|
||||
float signal = op2.process_sample(gate, trigger, adjusted_freq, velocity, pm * MOD_INDEX_COEFF) * op2.amplitude;
|
||||
|
||||
float op2_freq = frequency * op2.ratio;
|
||||
float op2_mod_index = (op2.amplitude * MOD_INDEX_COEFF);
|
||||
float op2_signal = op2.process_sample(gate, trigger, op2_freq, velocity) * op2_mod_index;
|
||||
// if patched, op3 modulated the amplitude of op2
|
||||
if (op3_dest == mod_dest_am) ring_mod(signal, modulator, op3.amplitude);
|
||||
|
||||
float op1_freq = frequency * op1.ratio;
|
||||
float op1_signal = op1.process_sample(gate, trigger, op1_freq, velocity, op2_signal) * op1.amplitude;
|
||||
|
||||
return op1_signal + op3_signal;
|
||||
return signal;
|
||||
}
|
||||
|
||||
float calc_algo3(const float frequency)
|
||||
float process_op1(const float frequency, const float modulator)
|
||||
{
|
||||
float fb_freq = frequency * op3.ratio;
|
||||
float fb_mod_index = (feedback_amt * MOD_INDEX_COEFF);
|
||||
float fb_signal = feedback_osc.process_sample(trigger, fb_freq) * fb_mod_index;
|
||||
|
||||
float op3_freq = frequency * op3.ratio;
|
||||
float op3_signal = op3.process_sample(gate, trigger, op3_freq, velocity, fb_signal) * op3.amplitude;
|
||||
|
||||
float op2_freq = frequency * op2.ratio;
|
||||
float op2_signal = op2.process_sample(gate, trigger, op2_freq, velocity) * op2.amplitude;
|
||||
// if patched, op2 modulates the phase of op1
|
||||
float pm = op2_dest == mod_dest_fm ? modulator : 0.f;
|
||||
|
||||
float op1_freq = frequency * op1.ratio;
|
||||
float op1_signal = op1.process_sample(gate, trigger, op1_freq, velocity) * op1.amplitude;
|
||||
float signal = op1.process_sample(gate, trigger, op1_freq, velocity, pm * MOD_INDEX_COEFF) * op1.amplitude;
|
||||
|
||||
return op1_signal + op2_signal + op3_signal;
|
||||
// if patched, op2 modulates the amplitude of op1
|
||||
if (op2_dest == mod_dest_am) ring_mod(signal, modulator, op2.amplitude);
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
float calc_algo4(const float frequency)
|
||||
float process_operators(float frequency)
|
||||
{
|
||||
float fb_freq = frequency * op3.ratio;
|
||||
float fb_mod_index = (feedback_amt * MOD_INDEX_COEFF);
|
||||
float fb_signal = feedback_osc.process_sample(trigger, fb_freq) * fb_mod_index;
|
||||
float op3_signal = process_op3(frequency);
|
||||
|
||||
float op3_freq = frequency * op3.ratio;
|
||||
float op3_mod_index = (op3.amplitude * MOD_INDEX_COEFF);
|
||||
float op3_signal = op3.process_sample(gate, trigger, op3_freq, velocity, fb_signal) * op3_mod_index;
|
||||
float op2_signal = process_op2(frequency, op3_signal);
|
||||
|
||||
float op2_freq = frequency * op2.ratio;
|
||||
float op2_mod_index = (op2.amplitude * MOD_INDEX_COEFF);
|
||||
float op2_signal = op2.process_sample(gate, trigger, op2_freq, velocity) * op2_mod_index;
|
||||
float op1_signal = process_op1(frequency, op2_signal);
|
||||
|
||||
float op1_freq = frequency * op1.ratio;
|
||||
return op1.process_sample(gate, trigger, op1_freq, velocity, op2_signal + op3_signal) * op1.amplitude;
|
||||
float signal_mix = op1_signal;
|
||||
if (op3_dest == mod_dest_out) { signal_mix += op3_signal; }
|
||||
if (op2_dest == mod_dest_out) { signal_mix += op2_signal; }
|
||||
|
||||
return signal_mix;
|
||||
}
|
||||
|
||||
void ring_mod(float& carrier, float modulator, float blend)
|
||||
{
|
||||
float dry_lvl = 1.f - blend;
|
||||
float wet_lvl = blend;
|
||||
|
||||
float dry_signal = carrier;
|
||||
float wet_signal = carrier * modulator * 2.0f;
|
||||
|
||||
carrier = dry_lvl * dry_signal + wet_lvl * wet_signal;
|
||||
}
|
||||
|
||||
float redux(float& value, float resolution)
|
||||
|
||||
33
util/averager.h
Normal file
33
util/averager.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
|
||||
namespace trnr {
|
||||
template <typename t_sample, unsigned int sample_size>
|
||||
class averager {
|
||||
public:
|
||||
averager() { samples.fill(0); }
|
||||
|
||||
t_sample process_sample(t_sample& _sample)
|
||||
{
|
||||
t_sample sum = t_sample(0);
|
||||
|
||||
for (unsigned int i = 0; i < sample_size; i++) {
|
||||
|
||||
if (i < sample_size - 1) {
|
||||
// shift to the left
|
||||
samples[i] = samples[i + 1];
|
||||
} else {
|
||||
// put new sample last
|
||||
samples[i] = _sample;
|
||||
}
|
||||
|
||||
sum += samples[i];
|
||||
}
|
||||
|
||||
return sum / sample_size;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<t_sample, sample_size> samples;
|
||||
};
|
||||
} // namespace trnr
|
||||
104
util/sample.h
Normal file
104
util/sample.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace trnr {
|
||||
class sample {
|
||||
public:
|
||||
sample(int16_t initial_value = 0) {
|
||||
set_value(initial_value);
|
||||
instances.push_back(this); // track this instance
|
||||
}
|
||||
|
||||
~sample() {
|
||||
// remove this instance from the tracking vector
|
||||
auto it = std::find(instances.begin(), instances.end(), this);
|
||||
if (it != instances.end()) {
|
||||
instances.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// set value while ensuring the global bit depth
|
||||
void set_value(int16_t new_value) {
|
||||
int16_t max_val = (1 << (global_bit_depth - 1)) - 1; // Max value for signed bit depth
|
||||
int16_t min_val = -(1 << (global_bit_depth - 1)); // Min value for signed bit depth
|
||||
|
||||
// Clamp the value to the allowed range
|
||||
if (new_value > max_val) {
|
||||
value = max_val;
|
||||
} else if (new_value < min_val) {
|
||||
value = min_val;
|
||||
} else {
|
||||
value = new_value;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t get_value() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
static void set_global_bit_depth(int depth) {
|
||||
if (depth < 1 || depth > 16) {
|
||||
throw std::invalid_argument("Bit depth must be between 1 and 16.");
|
||||
}
|
||||
|
||||
// rescale all existing values if the bit depth changes
|
||||
float scaling_factor = get_scaling_factor(previous_bit_depth, global_bit_depth);
|
||||
|
||||
// Rescale all existing instances
|
||||
for (auto* instance : instances) {
|
||||
instance->value = std::round(instance->value * scaling_factor);
|
||||
}
|
||||
|
||||
previous_bit_depth = global_bit_depth; // Store the old bit depth
|
||||
global_bit_depth = depth; // Update the global bit depth
|
||||
}
|
||||
|
||||
static int get_global_bit_depth() {
|
||||
return global_bit_depth;
|
||||
}
|
||||
|
||||
// arithmetic operators (all respect global bit depth)
|
||||
sample operator+(const sample& other) const {
|
||||
return sample(this->value + other.value);
|
||||
}
|
||||
|
||||
sample operator-(const sample& other) const {
|
||||
return sample(this->value - other.value);
|
||||
}
|
||||
|
||||
sample operator*(const sample& other) const {
|
||||
return sample(this->value * other.value);
|
||||
}
|
||||
|
||||
sample operator/(const sample& other) const {
|
||||
if (other.value == 0) {
|
||||
throw std::runtime_error("division by zero.");
|
||||
}
|
||||
return sample(this->value / other.value);
|
||||
}
|
||||
|
||||
// Cast to int16_t for convenience
|
||||
operator int16_t() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
int16_t value;
|
||||
static int global_bit_depth; // global bit depth
|
||||
static int previous_bit_depth; // previous bit depth
|
||||
static std::vector<sample*> instances; // track all instances for rescaling
|
||||
|
||||
// helper function to calculate the scaling factor
|
||||
static float get_scaling_factor(int old_bit_depth, int new_bit_depth) {
|
||||
if (old_bit_depth == new_bit_depth) {
|
||||
return 1.0f; // No scaling needed
|
||||
}
|
||||
float old_max = (1 << (old_bit_depth - 1)) - 1;
|
||||
float new_max = (1 << (new_bit_depth - 1)) - 1;
|
||||
return new_max / old_max;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user