mirror of
https://github.com/LMMS/lmms.git
synced 2026-06-23 21:48:40 -04:00
Makes the following changes to the Lb302 plugin for increased functionality, maintainability, and readability: - Add note velocity & note panning to Lb302 - Remove evil mutex+dynamic array combo in favor of a real-time safe ringbuffer queue - Use non-static data member initializers where possible - Use `std::numbers::pi` instead of `M_PI` - Use `std::array` and `std::unique_ptr` instead of manual memory management - Change plain-old-data classes with only public members to structs - Change some ints to f_cnt_t to better communicate their purpose and avoid weird implicit casting nonsense - Change floats to sample_t where appropriate to better communicate their purpose - Find suitable homes for loose constants - Prefix standard library math function calls with std:: - Ensure all floating-point literals intended to be floats end in 'f' and are not doubles to appease MSVC - Change uses of SIGNAL and SLOT macros to whatever they should be post-Qt6 upgrade - Tidy code to conform to current code conventions Also adds Hardware.h for low-level performance tools: - busyWaitHint(), supporting x86, ARM, and RISC-V - hardware_destructive_interference_size polyfill, supporting x86, ARM, RISC-V, and PPC - Moved the contents of denormals.h to Hardware.h, renamed disable_denormals() to disableDenormals() - Added ARM support to disableDenormals()
780 lines
26 KiB
C++
780 lines
26 KiB
C++
/*
|
|
* Lb302.cpp - Incomplete Roland TB-303 bass synth emulation
|
|
*
|
|
* Copyright (c) 2006-2008 Paul Giblock <pgib/at/users.sourceforge.net>
|
|
* Copyright (c) 2026 Fawn Sannar <rubiefawn/at/gmail.com>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* Lb302FilterIIR2 is based on the gsyn filter code by Andy Sloane.
|
|
*
|
|
* Lb302Filter3Pole is based on the TB-303 instrument written by
|
|
* Josep M Comajuncosas for the CSounds library
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*
|
|
*/
|
|
|
|
#include "Lb302.h"
|
|
|
|
#include <cmath>
|
|
#include <numbers>
|
|
|
|
#include <QDebug>
|
|
|
|
#include "AutomatableButton.h"
|
|
#include "BandLimitedWave.h"
|
|
#include "DspEffectLibrary.h"
|
|
#include "Engine.h"
|
|
#include "embed.h"
|
|
#include "InstrumentPlayHandle.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "Knob.h"
|
|
#include "LedCheckBox.h"
|
|
#include "NotePlayHandle.h"
|
|
#include "Oscillator.h"
|
|
#include "PixmapButton.h"
|
|
#include "plugin_export.h"
|
|
|
|
#define LB_24_IGNORE_ENVELOPE
|
|
//#define LB_24_RES_TRICK
|
|
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
// Helper to get the phase increment per sample, given a note's frequency and the current sample rate
|
|
static inline float phaseInc(float freq) { return freq / Engine::audioEngine()->outputSampleRate(); }
|
|
|
|
extern "C"
|
|
{
|
|
|
|
Plugin::Descriptor PLUGIN_EXPORT lb302_plugin_descriptor =
|
|
{
|
|
LMMS_STRINGIFY(PLUGIN_NAME),
|
|
"LB302",
|
|
QT_TRANSLATE_NOOP("PluginBrowser", "Incomplete monophonic imitation TB-303"),
|
|
"Paul Giblock <pgib/at/users.sf.net>",
|
|
0x0100,
|
|
Plugin::Type::Instrument,
|
|
new PluginPixmapLoader("logo"),
|
|
nullptr,
|
|
nullptr,
|
|
};
|
|
|
|
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*)
|
|
{
|
|
return new Lb302Synth(static_cast<InstrumentTrack*>(m));
|
|
}
|
|
|
|
} // extern "C"
|
|
|
|
|
|
//
|
|
// Lb302Filter
|
|
//
|
|
|
|
void Lb302Filter::recalc()
|
|
{
|
|
m_vcf.e[1] = std::exp(6.109f + 1.5876f * fs->envmod + 2.1553f * fs->cutoff - 1.2f * (1.0f - fs->reso));
|
|
m_vcf.e[0] = std::exp(5.613f - 0.8f * fs->envmod + 2.1553f * fs->cutoff - 0.7696f * (1.0f - fs->reso));
|
|
const float pi_sr = std::numbers::pi_v<float> / Engine::audioEngine()->outputSampleRate();
|
|
m_vcf.e[0] *= pi_sr;
|
|
m_vcf.e[1] *= pi_sr;
|
|
m_vcf.e[1] -= m_vcf.e[0];
|
|
|
|
m_vcf.resCoeff = std::exp(-1.20f + 3.455f * fs->reso);
|
|
};
|
|
|
|
|
|
void Lb302Filter::envRecalc() { m_vcf.c0 *= fs->envdecay; }
|
|
|
|
|
|
void Lb302Filter::playNote() { m_vcf.c0 = m_vcf.e[1]; }
|
|
|
|
|
|
//
|
|
// Lb302FilterIIR2
|
|
//
|
|
|
|
Lb302FilterIIR2::Lb302FilterIIR2(Lb302FilterKnobState* p_fs)
|
|
: Lb302Filter(p_fs)
|
|
, m_dist{std::make_unique<DspEffectLibrary::Distortion>(1.f, 1.f)}
|
|
{};
|
|
|
|
|
|
void Lb302FilterIIR2::recalc()
|
|
{
|
|
Lb302Filter::recalc();
|
|
m_dist->setThreshold(fs->dist * 75.f);
|
|
};
|
|
|
|
|
|
void Lb302FilterIIR2::envRecalc()
|
|
{
|
|
Lb302Filter::envRecalc();
|
|
const float w = m_vcf.e[0] + m_vcf.c0; // e[0] is adjusted for Hz and doesn't need s_envInc
|
|
const float k = std::exp(-w / m_vcf.resCoeff); // Does this mean c0 is inheritantly?
|
|
|
|
m_iir2.a = 2.f * std::cos(2.f * w) * k;
|
|
m_iir2.b = -k * k;
|
|
m_iir2.c = 1.f - m_iir2.a - m_iir2.b;
|
|
}
|
|
|
|
|
|
sample_t Lb302FilterIIR2::process(sample_t samp)
|
|
{
|
|
sample_t ret = m_iir2.a * m_iir2.d[0] + m_iir2.b * m_iir2.d[1] + m_iir2.c * samp;
|
|
// Delayed samples for filter
|
|
m_iir2.d[1] = m_iir2.d[0];
|
|
m_iir2.d[0] = ret;
|
|
|
|
if (fs->dist > 0.f) { ret = m_dist->nextSample(ret); }
|
|
|
|
// output = IIR2 + dry
|
|
return ret;
|
|
}
|
|
|
|
|
|
//
|
|
// Lb302Filter3Pole
|
|
//
|
|
|
|
void Lb302Filter3Pole::recalc()
|
|
{
|
|
// DO NOT CALL BASE CLASS
|
|
m_vcf.e[0] = 0.000001f;
|
|
m_vcf.e[1] = 1.f;
|
|
}
|
|
|
|
|
|
void Lb302Filter3Pole::envRecalc()
|
|
{
|
|
Lb302Filter::envRecalc();
|
|
|
|
// e0 is adjusted for Hz and doesn't need s_envInc
|
|
float w = m_vcf.e[0] + m_vcf.c0;
|
|
float k = std::min(fs->cutoff, 0.975f);
|
|
// sampleRateCutoff should not be changed to anything dynamic that is outside the
|
|
// scope of LB302 (like e.g. the audio engine's sample rate) as this changes the filter's cutoff
|
|
// behavior without any modification to its controls.
|
|
constexpr float sampleRateCutoff = 44100.0f;
|
|
float kfco = 50.f + k * (
|
|
(2300.f - 1600.f * fs->envmod)
|
|
+ w * (700.f + 1500.f * k + (1500.f + k * (sampleRateCutoff / 2.f - 6000.f)) * fs->envmod)
|
|
); // + iacc * (0.3f + 0.7f * kfco * kenvmod) * kaccent * kaccurve * 2000.f
|
|
|
|
#ifdef LB_24_IGNORE_ENVELOPE
|
|
// m_kfcn = fs->cutoff;
|
|
m_kfcn = 2.f * kfco / Engine::audioEngine()->outputSampleRate();
|
|
#else
|
|
m_kfcn = w;
|
|
#endif
|
|
m_kp = ((-2.7528f * m_kfcn + 3.0429f) * m_kfcn + 1.718f) * m_kfcn - 0.9984f;
|
|
const auto kp1 = m_kp + 1.f;
|
|
m_kp1h = 0.5f * kp1;
|
|
#ifdef LB_24_RES_TRICK
|
|
k = std::exp(-w / m_vcf.resCoeff);
|
|
m_kres = k * (((-2.7079f * kp1 + 10.963f) * kp1 - 14.934f) * kp1 + 8.4974f);
|
|
#else
|
|
m_kres = fs->reso * (((-2.7079f * kp1 + 10.963f) * kp1 - 14.934f) * kp1 + 8.4974f);
|
|
#endif
|
|
m_value = 1.f + (fs->dist * (1.5f + 2.f * m_kres * (1.f - m_kfcn))); // ENVMOD was DIST
|
|
}
|
|
|
|
|
|
sample_t Lb302Filter3Pole::process(sample_t samp)
|
|
{
|
|
constexpr float volAdjust = 3.f;
|
|
const sample_t ax1 = m_lastin;
|
|
const std::array<sample_t, 2> ay1 = { m_ay[0], m_ay[1] };
|
|
m_lastin = samp - std::tanh(m_kres * m_ay[2]);
|
|
m_ay[0] = m_kp1h * (m_lastin + ax1) - m_kp * m_ay[0];
|
|
m_ay[1] = m_kp1h * (m_ay[0] + ay1[0]) - m_kp * m_ay[1];
|
|
m_ay[2] = m_kp1h * (m_ay[1] + ay1[1]) - m_kp * m_ay[2];
|
|
return std::tanh(m_ay[2] * m_value) * volAdjust / (1.f + fs->dist);
|
|
}
|
|
|
|
|
|
//
|
|
// Lb302Synth
|
|
//
|
|
|
|
Lb302Synth::Lb302Synth(InstrumentTrack* instrumentTrack)
|
|
: Instrument(instrumentTrack, &lb302_plugin_descriptor, nullptr, Flag::IsSingleStreamed)
|
|
, m_vcfCutKnob(0.75f, 0.0f, 1.5f, 0.005f, this, tr("VCF Cutoff Frequency"))
|
|
, m_vcfResKnob(0.75f, 0.0f, 1.25f, 0.005f, this, tr("VCF Resonance"))
|
|
, m_vcfModKnob(0.1f, 0.0f, 1.0f, 0.005f, this, tr("VCF Envelope Mod"))
|
|
, m_vcfDecKnob(0.1f, 0.0f, 1.0f, 0.005f, this, tr("VCF Envelope Decay"))
|
|
, m_distKnob(0.0f, 0.0f, 1.0f, 0.01f, this, tr("Distortion"))
|
|
, m_waveShape(8.0f, 0.0f, 11.0f, this, tr("Waveform"))
|
|
, m_slideDecKnob(0.6f, 0.0f, 1.0f, 0.005f, this, tr("Slide Decay"))
|
|
, m_slideToggle(false, this, tr("Slide"))
|
|
, m_accentToggle(false, this, tr("Accent"))
|
|
, m_deadToggle(false, this, tr("Dead"))
|
|
, m_db24Toggle(false, this, tr("24dB/oct Filter"))
|
|
, m_vcfs{std::make_unique<Lb302FilterIIR2>(&m_fs), std::make_unique<Lb302Filter3Pole>(&m_fs)}
|
|
{
|
|
connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &Lb302Synth::filterChanged);
|
|
connect(&m_vcfCutKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
|
|
connect(&m_vcfResKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
|
|
connect(&m_vcfModKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
|
|
connect(&m_vcfDecKnob, &FloatModel::dataChanged, this, &Lb302Synth::decayChanged);
|
|
connect(&m_db24Toggle, &BoolModel::dataChanged, this, &Lb302Synth::db24Toggled);
|
|
connect(&m_distKnob, &FloatModel::dataChanged, this, &Lb302Synth::filterChanged);
|
|
|
|
// db24Toggled() would be called here, but all it does is call
|
|
// recalcFilter(), which is already done in filterChanged(), so there's no
|
|
// need to explicitly call recalcFilter() again.
|
|
filterChanged();
|
|
decayChanged();
|
|
|
|
Engine::audioEngine()->addPlayHandle(new InstrumentPlayHandle(this, instrumentTrack));
|
|
}
|
|
|
|
|
|
void Lb302Synth::saveSettings(QDomDocument& doc, QDomElement& el)
|
|
{
|
|
m_vcfCutKnob.saveSettings(doc, el, "vcf_cut");
|
|
m_vcfResKnob.saveSettings(doc, el, "vcf_res");
|
|
m_vcfModKnob.saveSettings(doc, el, "vcf_mod");
|
|
m_vcfDecKnob.saveSettings(doc, el, "vcf_dec");
|
|
|
|
m_waveShape.saveSettings(doc, el, "shape");
|
|
m_distKnob.saveSettings(doc, el, "dist");
|
|
m_slideDecKnob.saveSettings(doc, el, "slide_dec");
|
|
|
|
m_slideToggle.saveSettings(doc, el, "slide");
|
|
m_deadToggle.saveSettings(doc, el, "dead");
|
|
m_db24Toggle.saveSettings(doc, el, "db24");
|
|
}
|
|
|
|
|
|
void Lb302Synth::loadSettings(const QDomElement& el)
|
|
{
|
|
m_vcfCutKnob.loadSettings(el, "vcf_cut");
|
|
m_vcfResKnob.loadSettings(el, "vcf_res");
|
|
m_vcfModKnob.loadSettings(el, "vcf_mod");
|
|
m_vcfDecKnob.loadSettings(el, "vcf_dec");
|
|
|
|
m_distKnob.loadSettings(el, "dist");
|
|
m_slideDecKnob.loadSettings(el, "slide_dec");
|
|
m_waveShape.loadSettings(el, "shape");
|
|
m_slideToggle.loadSettings(el, "slide");
|
|
m_deadToggle.loadSettings(el, "dead");
|
|
m_db24Toggle.loadSettings(el, "db24");
|
|
|
|
// db24Toggled() would be called here, but all it does is call
|
|
// recalcFilter(), which is already done in filterChanged(), so there's no
|
|
// need to explicitly call recalcFilter() again.
|
|
filterChanged();
|
|
decayChanged();
|
|
}
|
|
|
|
|
|
void Lb302Synth::filterChanged()
|
|
{
|
|
m_fs.cutoff = m_vcfCutKnob.value();
|
|
m_fs.reso = m_vcfResKnob.value();
|
|
m_fs.envmod = m_vcfModKnob.value();
|
|
m_fs.dist = m_distKnob.value() * s_distRatio;
|
|
recalcFilter();
|
|
}
|
|
|
|
|
|
void Lb302Synth::decayChanged()
|
|
{
|
|
float d = (2.3f * m_vcfDecKnob.value() + 0.2f) * Engine::audioEngine()->outputSampleRate();
|
|
m_fs.envdecay = std::pow(0.1f, 1.0f / d * s_envInc);
|
|
}
|
|
|
|
|
|
void Lb302Synth::db24Toggled() { recalcFilter(); }
|
|
|
|
|
|
QString Lb302Synth::nodeName() const { return lb302_plugin_descriptor.name; }
|
|
|
|
|
|
void Lb302Synth::recalcFilter()
|
|
{
|
|
vcf().recalc();
|
|
m_vcfEnvPos = s_envInc; // Trigger filter envelope update in process()
|
|
}
|
|
|
|
|
|
void Lb302Synth::process(SampleFrame* outbuf, const f_cnt_t size)
|
|
{
|
|
if (m_releaseFrame == 0 || !m_playingNote) { m_vcaMode = VcaMode::Decay; }
|
|
|
|
if (m_playingNote)
|
|
{
|
|
constexpr float volRatio = 1.f / DefaultVolume;
|
|
m_noteVolume = m_playingNote->getVolume() * volRatio;
|
|
m_notePan = std::clamp(m_playingNote->getPanning(), PanningLeft, PanningRight);
|
|
}
|
|
const auto vv = panningToVolumeVector(m_notePan, m_noteVolume);
|
|
|
|
Lb302Filter& filter = vcf(); // Hold on to the current VCF, and use it throughout this period
|
|
if (m_newFreq)
|
|
{
|
|
m_newFreq = false;
|
|
const bool noteIsDead = m_deadToggle.value();
|
|
m_vcoInc = phaseInc(m_trueFreq);
|
|
|
|
// Always reset vca on non-dead notes, and only reset vca on decaying (decayed) and never-played
|
|
if (!noteIsDead || (m_vcaMode == VcaMode::Decay || m_vcaMode == VcaMode::NeverPlayed))
|
|
{
|
|
m_vcaMode = VcaMode::Attack;
|
|
}
|
|
else { m_vcaMode = VcaMode::Idle; }
|
|
|
|
if (m_slideInc != 0.f)
|
|
{
|
|
// Initiate Slide
|
|
m_slide = m_vcoInc - m_slideInc; // Slide amount
|
|
m_slideBase = m_vcoInc; // The REAL frequency
|
|
m_slideInc = 0.f; // reset from-note
|
|
}
|
|
else { m_slide = 0.f; }
|
|
|
|
// Slide-from note, save inc for next note
|
|
// May need to equal m_slideBase + m_slide if last note slid
|
|
if (m_slideToggle.value()) { m_slideInc = m_vcoInc; }
|
|
|
|
recalcFilter();
|
|
if (!noteIsDead)
|
|
{
|
|
filter.playNote();
|
|
m_vcfEnvPos = s_envInc; // Ensure envelope is recalculated
|
|
}
|
|
}
|
|
// Note: this has to be computed during processing and cannot be initialized
|
|
// in the constructor because it's dependent on the sample rate and that might
|
|
// change during rendering!
|
|
//
|
|
// At 44.1 kHz this will compute something very close to the previously
|
|
// hard coded value of 0.99897516.
|
|
constexpr auto computeDecayFactor = [](float decayTimeInSeconds, float targetedAttenuation) -> float
|
|
{
|
|
// This is the number of samples that correspond to the decay time in seconds
|
|
auto samplesNeededForDecay = decayTimeInSeconds * Engine::audioEngine()->outputSampleRate();
|
|
|
|
// This computes the factor that's needed to make a signal with a value of 1 decay to the
|
|
// targeted attenuation over the time in number of samples.
|
|
return std::pow(targetedAttenuation, 1.f / samplesNeededForDecay);
|
|
};
|
|
constexpr auto gateThreshold = 1.f / 65536.f; // Signal below this value is silenced
|
|
const auto decay = computeDecayFactor(0.245260770975f, gateThreshold);
|
|
|
|
const float sampleRatio = 44100.f / Engine::audioEngine()->outputSampleRate();
|
|
for (f_cnt_t i = 0; i < size; ++i)
|
|
{
|
|
// start decay if we're past release
|
|
if (i >= m_releaseFrame) { m_vcaMode = VcaMode::Decay; }
|
|
|
|
// update vcf
|
|
if (m_vcfEnvPos >= s_envInc)
|
|
{
|
|
filter.envRecalc();
|
|
m_vcfEnvPos = 0;
|
|
|
|
if (m_slide)
|
|
{
|
|
m_vcoInc = m_slideBase - m_slide;
|
|
// Calculate coeff from dec_knob on knob change.
|
|
// TODO: Adjust for s_envInc
|
|
m_slide -= m_slide * (0.1f - m_slideDecKnob.value() * 0.0999f) * sampleRatio;
|
|
}
|
|
}
|
|
|
|
m_vcfEnvPos++;
|
|
|
|
// update vco
|
|
m_vcoC += m_vcoInc;
|
|
if (m_vcoC > 0.5f) { m_vcoC -= 1.f; }
|
|
m_vcoShape = static_cast<VcoShape>(m_waveShape.value());
|
|
|
|
// TODO: Add VCO shape parameters (p0, p1) that changes the shape of
|
|
// each waveform. Merge sawtooths with triangle, and merge square with
|
|
// round square?
|
|
switch (m_vcoShape)
|
|
{
|
|
// p0: curviness of line
|
|
// Is this sawtooth backwards?
|
|
case VcoShape::Sawtooth: m_vcoK = m_vcoC; break;
|
|
|
|
// p0: duty rev.saw<->triangle<->saw
|
|
// p1: curviness
|
|
case VcoShape::Triangle:
|
|
m_vcoK = m_vcoC * 2.f + 0.5f;
|
|
if (m_vcoK > 0.5f) { m_vcoK = 1.f - m_vcoK; }
|
|
break;
|
|
|
|
// p0: slope of top
|
|
case VcoShape::Square:
|
|
m_vcoK = m_vcoC < 0.f ? 0.5f : -0.5f;
|
|
break;
|
|
|
|
// p0: width of round
|
|
case VcoShape::RoundSquare:
|
|
m_vcoK = m_vcoC < 0.f ? std::sqrt(1.f - (m_vcoC * m_vcoC * 4.f)) - 0.5f : -0.5f;
|
|
break;
|
|
|
|
// Maybe the fall should be exponential/sinsoidal instead of quadric.
|
|
// [-0.5, 0]: Rise, [0,0.25]: Slope down, [0.25,0.5]: Low
|
|
case VcoShape::Moog:
|
|
m_vcoK = m_vcoC * 2.f + 0.5f;
|
|
if (m_vcoK > 1.f) { m_vcoK = -0.5f; }
|
|
else if (m_vcoK > 0.5f)
|
|
{
|
|
float w = 2.f * (m_vcoK - 0.5f) - 1.f;
|
|
m_vcoK = 0.5f - std::sqrt(1.f - (w * w));
|
|
}
|
|
m_vcoK *= 2.f; // MOOG wave gets filtered away
|
|
break;
|
|
|
|
// [-0.5, 0.5] : [-pi, pi]
|
|
case VcoShape::Sine: m_vcoK = 0.5f * Oscillator::sinSample(m_vcoC); break;
|
|
case VcoShape::Exponential: m_vcoK = 0.5f * Oscillator::expSample(m_vcoC); break;
|
|
case VcoShape::WhiteNoise: m_vcoK = 0.5f * Oscillator::noiseSample(m_vcoC); break;
|
|
|
|
// The next cases all use the BandLimitedWave class which uses the oscillator increment `m_vcoInc` to compute samples.
|
|
// If that oscillator increment is 0 we return a 0 sample because calling BandLimitedWave::pdToLen(0) leads to a
|
|
// division by 0 which in turn leads to floating point exceptions.
|
|
case VcoShape::BLSawtooth:
|
|
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLSaw) * 0.5f;
|
|
break;
|
|
|
|
case VcoShape::BLSquare:
|
|
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLSquare) * 0.5f;
|
|
break;
|
|
|
|
case VcoShape::BLTriangle:
|
|
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLTriangle) * 0.5f;
|
|
break;
|
|
|
|
case VcoShape::BLMoog:
|
|
m_vcoK = m_vcoInc == 0.f ? 0.f : BandLimitedWave::oscillate(m_vcoC + 0.5f, BandLimitedWave::pdToLen(m_vcoInc), BandLimitedWave::Waveform::BLMoog);
|
|
break;
|
|
}
|
|
|
|
// Write out samples.
|
|
sample_t samp = filter.process(m_vcoK) * m_vca;
|
|
for (ch_cnt_t c = 0; c < DEFAULT_CHANNELS; c++) { outbuf[i][c] = samp * vv.vol[c]; }
|
|
|
|
// Handle Envelope
|
|
if (m_vcaMode == VcaMode::Attack)
|
|
{
|
|
m_vca += (s_vcaInitial - m_vca) * s_vcaAttack;
|
|
}
|
|
else if (m_vcaMode == VcaMode::Decay)
|
|
{
|
|
m_vca *= decay;
|
|
|
|
// the following line actually speeds up processing
|
|
if (m_vca < gateThreshold)
|
|
{
|
|
m_vca = 0;
|
|
m_vcaMode = VcaMode::NeverPlayed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void Lb302Synth::playNote(NotePlayHandle* nph, SampleFrame*)
|
|
{
|
|
if (nph->isMasterNote() || (nph->hasParent() && nph->isReleased())) { return; }
|
|
|
|
// Enqueue new note in m_notes
|
|
auto tries = s_maxNoteEnqueueRetries;
|
|
auto writeClaimedExpected = m_notesWriteClaimed.load(std::memory_order_relaxed);
|
|
std::size_t index;
|
|
std::size_t nextIndex;
|
|
for (;;)
|
|
{
|
|
if (!tries--)
|
|
{
|
|
qDebug() << "Lb302: Note dropped due to catastrophically poor performance! This should never happen!";
|
|
return;
|
|
}
|
|
const auto occupied = static_cast<std::ptrdiff_t>(writeClaimedExpected)
|
|
- static_cast<std::ptrdiff_t>(m_notesReadSeq.load(std::memory_order_acquire));
|
|
assert(occupied >= 0);
|
|
// TODO C++23: [[assume(occupied >= 0)]]
|
|
if (static_cast<std::size_t>(occupied) >= s_maxPendingNotes)
|
|
{
|
|
// Queue is full, try again (at least one sender always makes progress)
|
|
busyWaitHint();
|
|
writeClaimedExpected = m_notesWriteClaimed.load(std::memory_order_relaxed);
|
|
continue;
|
|
}
|
|
index = writeClaimedExpected;
|
|
nextIndex = writeClaimedExpected + 1;
|
|
if (m_notesWriteClaimed.compare_exchange_strong(
|
|
writeClaimedExpected,
|
|
nextIndex,
|
|
std::memory_order_acquire)
|
|
) { break; } // Note sent
|
|
}
|
|
|
|
m_notes[index & s_notesBufMask] = nph;
|
|
|
|
std::size_t writeCommittedExpected = index;
|
|
while (!m_notesWriteCommitted.compare_exchange_strong(writeCommittedExpected, nextIndex, std::memory_order_release))
|
|
{
|
|
writeCommittedExpected = index; // Reset this as the CAS will have changed it
|
|
busyWaitHint();
|
|
}
|
|
}
|
|
|
|
|
|
void Lb302Synth::processNote(NotePlayHandle* nph)
|
|
{
|
|
/// Start a new note.
|
|
if (nph->m_pluginData != this)
|
|
{
|
|
m_playingNote = nph;
|
|
nph->m_pluginData = this;
|
|
m_newFreq = true;
|
|
}
|
|
|
|
m_releaseFrame = std::max(m_releaseFrame, nph->framesLeft() + nph->offset());
|
|
|
|
if (!m_playingNote && !nph->isReleased() && m_releaseFrame > 0)
|
|
{
|
|
m_playingNote = nph;
|
|
nph->m_pluginData = this;
|
|
if (m_slideToggle.value()) { m_slideInc = phaseInc(nph->frequency()); }
|
|
}
|
|
|
|
// Check for slide
|
|
if (m_playingNote == nph)
|
|
{
|
|
m_trueFreq = nph->frequency();
|
|
const auto trueInc = phaseInc(m_trueFreq);
|
|
if (m_slideToggle.value()) { m_slideBase = trueInc; } else { m_vcoInc = trueInc; }
|
|
}
|
|
}
|
|
|
|
|
|
void Lb302Synth::play(SampleFrame* working_buffer)
|
|
{
|
|
const auto readIdx = m_notesReadSeq.load(std::memory_order_relaxed);
|
|
const auto writeCommitted = m_notesWriteCommitted.load(std::memory_order_acquire);
|
|
// Process notes, but process new notes last
|
|
for (auto i = readIdx; i < writeCommitted; ++i)
|
|
{
|
|
const auto& nph = m_notes[i & s_notesBufMask];
|
|
if (nph->totalFramesPlayed() == 0) { continue; }
|
|
processNote(nph);
|
|
}
|
|
for (auto i = readIdx; i < writeCommitted; ++i)
|
|
{
|
|
const auto& nph = m_notes[i & s_notesBufMask];
|
|
if (nph->totalFramesPlayed() != 0) { continue; }
|
|
processNote(nph);
|
|
}
|
|
// Mark the processed notes as having been read so that playNote() calls can overwrite them
|
|
m_notesReadSeq.fetch_add(writeCommitted - readIdx, std::memory_order_release);
|
|
|
|
process(working_buffer, Engine::audioEngine()->framesPerPeriod());
|
|
}
|
|
|
|
|
|
void Lb302Synth::deleteNotePluginData(NotePlayHandle* nph)
|
|
{
|
|
if (m_playingNote == nph) { m_playingNote = nullptr; }
|
|
}
|
|
|
|
|
|
gui::PluginView* Lb302Synth::instantiateView(QWidget* parent)
|
|
{
|
|
return new gui::Lb302SynthView(this, parent);
|
|
}
|
|
|
|
|
|
//
|
|
// gui::Lb302SynthView
|
|
//
|
|
|
|
namespace gui
|
|
{
|
|
|
|
|
|
Lb302SynthView::Lb302SynthView(Instrument* instrument, QWidget* parent)
|
|
: InstrumentViewFixedSize(instrument, parent)
|
|
{
|
|
setAutoFillBackground(true);
|
|
static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork");
|
|
QPalette pal;
|
|
pal.setBrush(backgroundRole(), s_artwork);
|
|
setPalette(pal);
|
|
|
|
// GUI
|
|
m_vcfCutKnob = new Knob(KnobType::Bright26, this);
|
|
m_vcfCutKnob->move(75, 130);
|
|
m_vcfCutKnob->setHintText(tr("Cutoff Freq:"), "");
|
|
|
|
m_vcfResKnob = new Knob(KnobType::Bright26, this);
|
|
m_vcfResKnob->move(120, 130);
|
|
m_vcfResKnob->setHintText(tr("Resonance:"), "");
|
|
|
|
m_vcfModKnob = new Knob(KnobType::Bright26, this);
|
|
m_vcfModKnob->move(165, 130);
|
|
m_vcfModKnob->setHintText(tr("Env Mod:"), "");
|
|
|
|
m_vcfDecKnob = new Knob(KnobType::Bright26, this);
|
|
m_vcfDecKnob->move(210, 130);
|
|
m_vcfDecKnob->setHintText(tr("Decay:"), "");
|
|
|
|
m_slideToggle = new LedCheckBox("", this);
|
|
m_slideToggle->move(10, 180);
|
|
|
|
// accent removed pending real implementation - no need for non-functional buttons
|
|
/* m_accentToggle = new LedCheckBox("", this);
|
|
m_accentToggle->move(10, 200); */
|
|
|
|
m_deadToggle = new LedCheckBox("", this);
|
|
m_deadToggle->move(10, 200);
|
|
|
|
m_db24Toggle = new LedCheckBox("", this);
|
|
m_db24Toggle->move(10, 150);
|
|
m_db24Toggle->setToolTip(tr("303-es-que, 24dB/octave, 3 pole filter"));
|
|
|
|
m_slideDecKnob = new Knob(KnobType::Bright26, this);
|
|
m_slideDecKnob->move(210, 75);
|
|
m_slideDecKnob->setHintText(tr("Slide Decay:"), "");
|
|
|
|
m_distKnob = new Knob(KnobType::Bright26, this);
|
|
m_distKnob->move(210, 190);
|
|
m_distKnob->setHintText(tr("DIST:"), "");
|
|
|
|
// Shapes
|
|
const int waveBtnX = 10;
|
|
const int waveBtnY = 96;
|
|
m_waveBtnGrp = new AutomatableButtonGroup(this);
|
|
|
|
auto sawWaveBtn = new PixmapButton(this, tr("Saw wave"));
|
|
sawWaveBtn->move(waveBtnX, waveBtnY);
|
|
sawWaveBtn->setActiveGraphic(embed::getIconPixmap("saw_wave_active"));
|
|
sawWaveBtn->setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive"));
|
|
sawWaveBtn->setToolTip(tr("Click here for a sawtooth wave."));
|
|
m_waveBtnGrp->addButton(sawWaveBtn);
|
|
|
|
auto triangleWaveBtn = new PixmapButton(this, tr("Triangle wave"));
|
|
triangleWaveBtn->move(waveBtnX + (16 * 1), waveBtnY);
|
|
triangleWaveBtn->setActiveGraphic(embed::getIconPixmap("triangle_wave_active"));
|
|
triangleWaveBtn->setInactiveGraphic(embed::getIconPixmap("triangle_wave_inactive"));
|
|
triangleWaveBtn->setToolTip(tr("Click here for a triangle wave."));
|
|
m_waveBtnGrp->addButton(triangleWaveBtn);
|
|
|
|
auto sqrWaveBtn = new PixmapButton(this, tr("Square wave"));
|
|
sqrWaveBtn->move(waveBtnX + (16 * 2), waveBtnY);
|
|
sqrWaveBtn->setActiveGraphic(embed::getIconPixmap("square_wave_active"));
|
|
sqrWaveBtn->setInactiveGraphic(embed::getIconPixmap("square_wave_inactive"));
|
|
sqrWaveBtn->setToolTip(tr("Click here for a square wave."));
|
|
m_waveBtnGrp->addButton(sqrWaveBtn);
|
|
|
|
auto roundSqrWaveBtn = new PixmapButton(this, tr("Rounded square wave"));
|
|
roundSqrWaveBtn->move(waveBtnX + (16 * 3), waveBtnY);
|
|
roundSqrWaveBtn->setActiveGraphic( embed::getIconPixmap("round_square_wave_active"));
|
|
roundSqrWaveBtn->setInactiveGraphic( embed::getIconPixmap("round_square_wave_inactive"));
|
|
roundSqrWaveBtn->setToolTip(tr("Click here for a square wave with a rounded end."));
|
|
m_waveBtnGrp->addButton(roundSqrWaveBtn);
|
|
|
|
auto moogWaveBtn = new PixmapButton(this, tr("Moog wave"));
|
|
moogWaveBtn->move(waveBtnX + (16 * 4), waveBtnY);
|
|
moogWaveBtn->setActiveGraphic(embed::getIconPixmap("moog_saw_wave_active"));
|
|
moogWaveBtn->setInactiveGraphic(embed::getIconPixmap("moog_saw_wave_inactive"));
|
|
moogWaveBtn->setToolTip(tr("Click here for a moog-like wave."));
|
|
m_waveBtnGrp->addButton(moogWaveBtn);
|
|
|
|
auto sinWaveBtn = new PixmapButton(this, tr("Sine wave"));
|
|
sinWaveBtn->move(waveBtnX + (16 * 5), waveBtnY);
|
|
sinWaveBtn->setActiveGraphic(embed::getIconPixmap("sin_wave_active"));
|
|
sinWaveBtn->setInactiveGraphic(embed::getIconPixmap("sin_wave_inactive"));
|
|
sinWaveBtn->setToolTip(tr("Click for a sine wave."));
|
|
m_waveBtnGrp->addButton(sinWaveBtn);
|
|
|
|
auto exponentialWaveBtn = new PixmapButton(this, tr("White noise wave"));
|
|
exponentialWaveBtn->move(waveBtnX + (16 * 6), waveBtnY);
|
|
exponentialWaveBtn->setActiveGraphic(embed::getIconPixmap("exp_wave_active"));
|
|
exponentialWaveBtn->setInactiveGraphic(embed::getIconPixmap("exp_wave_inactive"));
|
|
exponentialWaveBtn->setToolTip(tr("Click here for an exponential wave."));
|
|
m_waveBtnGrp->addButton(exponentialWaveBtn);
|
|
|
|
auto whiteNoiseWaveBtn = new PixmapButton(this, tr("White noise wave"));
|
|
whiteNoiseWaveBtn->move(waveBtnX + (16 * 7), waveBtnY);
|
|
whiteNoiseWaveBtn->setActiveGraphic(embed::getIconPixmap("white_noise_wave_active"));
|
|
whiteNoiseWaveBtn->setInactiveGraphic(embed::getIconPixmap("white_noise_wave_inactive"));
|
|
whiteNoiseWaveBtn->setToolTip(tr("Click here for white noise."));
|
|
m_waveBtnGrp->addButton(whiteNoiseWaveBtn);
|
|
|
|
auto blSawWaveBtn = new PixmapButton(this, tr("Bandlimited saw wave"));
|
|
blSawWaveBtn->move(waveBtnX + (16 * 9) - 8, waveBtnY);
|
|
blSawWaveBtn->setActiveGraphic(embed::getIconPixmap("saw_wave_active"));
|
|
blSawWaveBtn->setInactiveGraphic(embed::getIconPixmap("saw_wave_inactive"));
|
|
blSawWaveBtn->setToolTip(tr("Click here for bandlimited sawtooth wave."));
|
|
m_waveBtnGrp->addButton(blSawWaveBtn);
|
|
|
|
auto blSquareWaveBtn = new PixmapButton(this, tr("Bandlimited square wave"));
|
|
blSquareWaveBtn->move(waveBtnX + (16 * 10) - 8, waveBtnY);
|
|
blSquareWaveBtn->setActiveGraphic(embed::getIconPixmap("square_wave_active"));
|
|
blSquareWaveBtn->setInactiveGraphic(embed::getIconPixmap("square_wave_inactive"));
|
|
blSquareWaveBtn->setToolTip(tr("Click here for bandlimited square wave."));
|
|
m_waveBtnGrp->addButton(blSquareWaveBtn);
|
|
|
|
auto blTriangleWaveBtn = new PixmapButton(this, tr("Bandlimited triangle wave"));
|
|
blTriangleWaveBtn->move(waveBtnX + (16 * 11) - 8, waveBtnY);
|
|
blTriangleWaveBtn->setActiveGraphic(embed::getIconPixmap("triangle_wave_active"));
|
|
blTriangleWaveBtn->setInactiveGraphic(embed::getIconPixmap("triangle_wave_inactive"));
|
|
blTriangleWaveBtn->setToolTip(tr("Click here for bandlimited triangle wave."));
|
|
m_waveBtnGrp->addButton(blTriangleWaveBtn);
|
|
|
|
auto blMoogWaveBtn = new PixmapButton(this, tr("Bandlimited moog saw wave"));
|
|
blMoogWaveBtn->move(waveBtnX + (16 * 12) - 8, waveBtnY);
|
|
blMoogWaveBtn->setActiveGraphic(embed::getIconPixmap("moog_saw_wave_active"));
|
|
blMoogWaveBtn->setInactiveGraphic(embed::getIconPixmap("moog_saw_wave_inactive"));
|
|
blMoogWaveBtn->setToolTip(tr("Click here for bandlimited moog-like wave."));
|
|
m_waveBtnGrp->addButton(blMoogWaveBtn);
|
|
}
|
|
|
|
|
|
void Lb302SynthView::modelChanged()
|
|
{
|
|
auto syn = castModel<Lb302Synth>();
|
|
|
|
m_vcfCutKnob->setModel(&syn->m_vcfCutKnob);
|
|
m_vcfResKnob->setModel(&syn->m_vcfResKnob);
|
|
m_vcfDecKnob->setModel(&syn->m_vcfDecKnob);
|
|
m_vcfModKnob->setModel(&syn->m_vcfModKnob);
|
|
m_slideDecKnob->setModel(&syn->m_slideDecKnob);
|
|
|
|
m_distKnob->setModel(&syn->m_distKnob);
|
|
m_waveBtnGrp->setModel(&syn->m_waveShape);
|
|
|
|
m_slideToggle->setModel(&syn->m_slideToggle);
|
|
// m_accentToggle->setModel(&syn->accentToggle);
|
|
m_deadToggle->setModel(&syn->m_deadToggle);
|
|
m_db24Toggle->setModel(&syn->m_db24Toggle);
|
|
}
|
|
|
|
|
|
} // namespace gui
|
|
|
|
} // namespace lmms
|