diff --git a/.gitignore b/.gitignore index 190a750..6748134 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ DerivedData/ # macOS .DS_Store + +# JUCE +juce/ diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 8700cb8..7d08f42 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -576,10 +576,8 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess accentKnob ("Accent"), rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }), octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }), - zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}), - wave1Bar (processor.getAPVTS(), "osc1Wave", { "Sin", "Tri", "Saw", "Sq" }), - wave2Bar (processor.getAPVTS(), "osc2Wave", { "Sin", "Tri", "Saw", "Sq" }), - matrix (processor), + zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}), + matrix (processor), stepPanel (processor) { setLookAndFeel (lnf.get()); @@ -593,8 +591,37 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess content.addAndMakeVisible (rateBar); content.addAndMakeVisible (octaveBar); content.addAndMakeVisible (zoomBar); - content.addAndMakeVisible (wave1Bar); - content.addAndMakeVisible (wave2Bar); + + static const juce::StringArray waveformChoices = + { + "Sine", "Triangle", "Saw", "Square", + "Pulse", "Noise", "Sub", "Pluck", + "Super", "S&H", "Formant", "PWM" + }; + + auto setupWaveCombo = [&apvts, &waveformChoices] (juce::ComboBox& combo, + const juce::String& paramId) + { + combo.setColour (juce::ComboBox::textColourId, colours::text); + combo.setColour (juce::ComboBox::outlineColourId, colours::gridLight); + combo.setColour (juce::ComboBox::backgroundColourId, colours::grid); + combo.setColour (juce::ComboBox::arrowColourId, colours::accent); + combo.setFont (font (11.0f)); + + for (int i = 0; i < waveformChoices.size(); ++i) + combo.addItem (waveformChoices[i], i + 1); + + combo.selectId (static_cast (apvts.getRawParameterValue (paramId)->load()) + 1); + }; + + setupWaveCombo (wave1Box, "osc1Wave"); + setupWaveCombo (wave2Box, "osc2Wave"); + + wave1Attachment = std::make_unique (apvts, "osc1Wave", wave1Box); + wave2Attachment = std::make_unique (apvts, "osc2Wave", wave2Box); + + content.addAndMakeVisible (wave1Box); + content.addAndMakeVisible (wave2Box); static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; zoomBar.setManualHandler ([this] (int idx) @@ -734,8 +761,8 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g) drawCaption (rateBar, "RATE"); drawCaption (octaveBar, "OCTAVE"); drawCaption (zoomBar, "ZOOM"); - drawCaption (wave1Bar, "OSC 1"); - drawCaption (wave2Bar, "OSC 2"); + drawCaption (wave1Box, "OSC 1"); + drawCaption (wave2Box, "OSC 2"); const int footY = baseHeight - 20; g.setColour (colours::grid); @@ -799,8 +826,8 @@ void MonostepAudioProcessorEditor::resized() int x = 16; const int barW = 112; - wave1Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 8; - wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16; + wave1Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 8; + wave2Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 16; const int knobW = 56; const int knobGap = 60; diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index cf4d3ef..0df31a9 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -216,8 +216,10 @@ private: ChoiceBar rateBar; ChoiceBar octaveBar; ChoiceBar zoomBar; - ChoiceBar wave1Bar; - ChoiceBar wave2Bar; + juce::ComboBox wave1Box; + juce::ComboBox wave2Box; + std::unique_ptr wave1Attachment; + std::unique_ptr wave2Attachment; SequencerMatrix matrix; StepPanel stepPanel; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 090713f..25cde3f 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -62,8 +62,8 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea { juce::AudioProcessorValueTreeState::ParameterLayout layout; - layout.add (std::make_unique (paramId (pOsc1Wave), "Osc 1 Wave", 0, 7, 2)); - layout.add (std::make_unique (paramId (pOsc2Wave), "Osc 2 Wave", 0, 7, 3)); + layout.add (std::make_unique (paramId (pOsc1Wave), "Osc 1 Wave", 0, monostep::numWaveforms - 1, 2)); + layout.add (std::make_unique (paramId (pOsc2Wave), "Osc 2 Wave", 0, monostep::numWaveforms - 1, 3)); layout.add (std::make_unique (paramId (pOsc1Coarse), "Osc 1 Coarse", -12, 12, 0)); layout.add (std::make_unique (paramId (pOsc2Coarse), "Osc 2 Coarse", -12, 12, 0)); @@ -307,8 +307,8 @@ void MonostepAudioProcessor::randomizePattern() void MonostepAudioProcessor::randomizeOsc() { - applyParamValue ("osc1Wave", (float) random.nextInt (4)); - applyParamValue ("osc2Wave", (float) random.nextInt (4)); + applyParamValue ("osc1Wave", (float) random.nextInt (monostep::numWaveforms)); + applyParamValue ("osc2Wave", (float) random.nextInt (monostep::numWaveforms)); applyParamValue ("osc2Coarse", (float) (random.nextInt (25) - 12)); applyParamValue ("detune", (float) (random.nextInt (101) - 50)); applyParamValue ("mix", 0.2f + random.nextFloat() * 0.6f); diff --git a/Source/dsp/WaveTables.h b/Source/dsp/WaveTables.h index 34fce7b..f24532a 100644 --- a/Source/dsp/WaveTables.h +++ b/Source/dsp/WaveTables.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace monostep @@ -16,9 +17,15 @@ enum class Waveform : int pulse = 4, noise = 5, sub = 6, - pluck = 7 + pluck = 7, + super = 8, + sah = 9, + formant = 10, + pwm = 11 }; +static constexpr int numWaveforms = 12; + inline float frac (float x) { return x - std::floor (x); @@ -93,6 +100,57 @@ inline float renderWave (Waveform w, float phase, float inc) const float f = frac (phase); return 2.0f * std::sin (f * 6.283185307179586f * 2.0f) * std::exp (-10.0f * f); } + + case Waveform::super: + { + // A small detuned stack of band-limited saws -> fat, chorus-like lead. + float sum = 0.0f; + const float detunes[3] = { -0.01f, 0.0f, 0.01f }; + + for (const float d : detunes) + { + const float f = frac (phase + d); + sum += (2.0f * f - 1.0f) - polyBlep (f, inc); + } + + return sum / 3.0f; + } + + case Waveform::sah: + { + // Sample & hold: a stepped random level per fraction of the cycle. + // Deterministic (no shared state) so each oscillator is independent. + const int steps = 16; + const float f = frac (phase); + int s = static_cast (std::floor (f * (float) steps)); + if (s >= steps) s = steps - 1; + if (s < 0) s = 0; + + std::uint32_t h = static_cast (s); + h ^= h << 13; h ^= h >> 17; h ^= h << 5; + const float r = static_cast (h & 0xffffff) / 16777215.0f; + + return r * 2.0f - 1.0f; + } + + case Waveform::formant: + { + // Two formant-like bands via slow AM sidebands (fixed ratios). + const float w = phase * 6.283185307179586f; + const float env = 0.5f + 0.3f * std::cos (w * 0.5f) + 0.2f * std::cos (w * 0.25f); + return std::sin (w) * env; + } + + case Waveform::pwm: + { + // Fixed 20 % pulse, band-limited at both edges. + const float width = 0.2f; + const float f = frac (phase); + float v = (f < width) ? 1.0f : -1.0f; + v += polyBlep (f, inc); + v -= polyBlep (frac (f + 1.0f - width), inc); + return v; + } } return 0.0f;