mirror of
https://codeberg.org/armin/chromaflock.git
synced 2026-09-01 04:10:47 +02:00
add arpeggiator, rename presets, move spectrum analyzer into waveform
This commit is contained in:
parent
a806e1a8b5
commit
3331b4cc16
9 changed files with 1126 additions and 395 deletions
225
Source/DSP/Arpeggiator.h
Normal file
225
Source/DSP/Arpeggiator.h
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
// Tempo-synced arpeggiator. Tracks held notes and, on each clock step,
|
||||
// produces the notes to play for the selected pattern / octave range /
|
||||
// direction. Pure note logic — timing is driven from outside (the audio
|
||||
// thread) via step().
|
||||
class Arpeggiator {
|
||||
public:
|
||||
enum Pattern {
|
||||
Up = 0,
|
||||
Down,
|
||||
UpDown,
|
||||
DownUp,
|
||||
Random,
|
||||
AsPlayed,
|
||||
Chord,
|
||||
numPatterns
|
||||
};
|
||||
|
||||
enum Direction {
|
||||
DirectionUp = 0,
|
||||
DirectionDown
|
||||
};
|
||||
|
||||
struct NotePitch {
|
||||
int note;
|
||||
float velocity;
|
||||
};
|
||||
|
||||
void setParameters(bool enabled, int pattern, int octaves, int direction,
|
||||
double rateBeats, double bpm) {
|
||||
this->enabled = enabled;
|
||||
this->pattern = pattern < 0 ? Up : (pattern >= numPatterns ? Chord : pattern);
|
||||
this->octaves = octaves < 1 ? 1 : (octaves > 3 ? 3 : octaves);
|
||||
this->direction = (direction == DirectionDown) ? DirectionDown : DirectionUp;
|
||||
this->rateBeats = rateBeats;
|
||||
this->bpm = bpm > 0.0 ? bpm : 120.0;
|
||||
}
|
||||
|
||||
bool isEnabled() const { return enabled; }
|
||||
|
||||
double getTickSeconds() const {
|
||||
return rateBeats * 60.0 / bpm;
|
||||
}
|
||||
|
||||
void noteOn(int note, float velocity) {
|
||||
if (note < 0 || note > 127) return;
|
||||
if (std::find(heldNotes.begin(), heldNotes.end(), note) != heldNotes.end())
|
||||
return;
|
||||
bool wasIdle = heldNotes.empty();
|
||||
heldNotes.push_back(note);
|
||||
velocities[note] = velocity;
|
||||
// A fresh key press should sound immediately instead of waiting for
|
||||
// the next host-synced tick.
|
||||
if (wasIdle)
|
||||
needsImmediateStep = true;
|
||||
}
|
||||
|
||||
void noteOff(int note) {
|
||||
auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
|
||||
if (it != heldNotes.end())
|
||||
heldNotes.erase(it);
|
||||
}
|
||||
|
||||
bool hasHeldNotes() const { return !heldNotes.empty(); }
|
||||
|
||||
// Returns true if the next step should fire right now (a fresh key press
|
||||
// requested an immediate trigger) and clears the request.
|
||||
bool shouldTriggerNow() {
|
||||
bool v = needsImmediateStep;
|
||||
needsImmediateStep = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Clears held notes and step state. Notes that were sounding when reset
|
||||
// was called are returned so the caller can issue note-offs.
|
||||
void reset(std::vector<int>& notesToStop) {
|
||||
notesToStop.swap(currentNotes);
|
||||
heldNotes.clear();
|
||||
stepIndex = 0;
|
||||
needsImmediateStep = false;
|
||||
}
|
||||
|
||||
// Advances one step. Notes that should stop go into `notesToStop`
|
||||
// (usually the note(s) from the previous step); notes that should start
|
||||
// go into `notesToPlay`.
|
||||
void step(std::vector<int>& notesToStop, std::vector<NotePitch>& notesToPlay) {
|
||||
notesToStop.clear();
|
||||
notesToPlay.clear();
|
||||
|
||||
if (!enabled) return;
|
||||
|
||||
if (heldNotes.empty()) {
|
||||
notesToStop = currentNotes;
|
||||
currentNotes.clear();
|
||||
stepIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<NotePitch> pool;
|
||||
buildPool(pool);
|
||||
|
||||
if (pool.empty()) {
|
||||
notesToStop = currentNotes;
|
||||
currentNotes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pattern == Chord) {
|
||||
notesToStop = currentNotes;
|
||||
notesToPlay = pool;
|
||||
currentNotes.clear();
|
||||
for (const auto& np : pool)
|
||||
currentNotes.push_back(np.note);
|
||||
return;
|
||||
}
|
||||
|
||||
auto order = buildOrder(pool);
|
||||
if (order.empty()) {
|
||||
notesToStop = currentNotes;
|
||||
currentNotes.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = stepIndex % static_cast<int>(order.size());
|
||||
++stepIndex;
|
||||
|
||||
const NotePitch& np = order[idx];
|
||||
|
||||
notesToStop = currentNotes;
|
||||
currentNotes.clear();
|
||||
currentNotes.push_back(np.note);
|
||||
notesToPlay.push_back(np);
|
||||
}
|
||||
|
||||
private:
|
||||
void buildPool(std::vector<NotePitch>& pool) const {
|
||||
bool used[128] = {};
|
||||
auto addPitch = [&](int pitch, float velocity) {
|
||||
pitch = pitch < 0 ? 0 : (pitch > 127 ? 127 : pitch);
|
||||
if (used[pitch]) return;
|
||||
used[pitch] = true;
|
||||
pool.push_back({pitch, velocity});
|
||||
};
|
||||
|
||||
if (pattern == AsPlayed) {
|
||||
for (int n : heldNotes) {
|
||||
for (int k = 0; k < octaves; ++k) {
|
||||
int off = (direction == DirectionUp ? 12 * k : -12 * k);
|
||||
addPitch(n + off, velocities[n]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::vector<int> sorted(heldNotes.begin(), heldNotes.end());
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
for (int n : sorted) {
|
||||
for (int k = 0; k < octaves; ++k) {
|
||||
int off = (direction == DirectionUp ? 12 * k : -12 * k);
|
||||
addPitch(n + off, velocities[n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<NotePitch> buildOrder(const std::vector<NotePitch>& pool) {
|
||||
// Sequential patterns play by ascending pitch regardless of the
|
||||
// octave direction; As Played keeps the key-press order.
|
||||
std::vector<NotePitch> seq(pool);
|
||||
std::sort(seq.begin(), seq.end(),
|
||||
[](const NotePitch& a, const NotePitch& b) { return a.note < b.note; });
|
||||
int n = static_cast<int>(seq.size());
|
||||
|
||||
std::vector<NotePitch> order;
|
||||
auto push = [&](int i) { order.push_back(seq[i]); };
|
||||
|
||||
switch (pattern) {
|
||||
case Down:
|
||||
for (int i = n - 1; i >= 0; --i) push(i);
|
||||
break;
|
||||
case UpDown:
|
||||
for (int i = 0; i < n; ++i) push(i);
|
||||
for (int i = n - 2; i >= 1; --i) push(i);
|
||||
break;
|
||||
case DownUp:
|
||||
for (int i = n - 1; i >= 0; --i) push(i);
|
||||
for (int i = 1; i < n - 1; ++i) push(i);
|
||||
break;
|
||||
case Random:
|
||||
order = seq;
|
||||
for (int i = n - 1; i > 0; --i) {
|
||||
int j = static_cast<int>(randomState % static_cast<uint32_t>(i + 1));
|
||||
std::swap(order[i], order[j]);
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
}
|
||||
break;
|
||||
case AsPlayed:
|
||||
order = pool;
|
||||
break;
|
||||
case Chord:
|
||||
case Up:
|
||||
default:
|
||||
for (int i = 0; i < n; ++i) push(i);
|
||||
break;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
bool enabled = false;
|
||||
int pattern = Up;
|
||||
int octaves = 1;
|
||||
int direction = DirectionUp;
|
||||
double rateBeats = 0.5;
|
||||
double bpm = 120.0;
|
||||
|
||||
std::vector<int> heldNotes;
|
||||
float velocities[128] = {};
|
||||
std::vector<int> currentNotes;
|
||||
int stepIndex = 0;
|
||||
bool needsImmediateStep = false;
|
||||
|
||||
uint32_t randomState = 0x12345678u;
|
||||
};
|
||||
|
|
@ -197,7 +197,7 @@ void VuMeter::paint(juce::Graphics& g) {
|
|||
g.drawText("VU", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred);
|
||||
}
|
||||
|
||||
// --- Waveform Display ---
|
||||
// --- Waveform Display (with semi-transparent spectrum overlay) ---
|
||||
void WaveformDisplay::paint(juce::Graphics& g) {
|
||||
auto bounds = getLocalBounds().toFloat().reduced(0.5f);
|
||||
|
||||
|
|
@ -209,6 +209,116 @@ void WaveformDisplay::paint(juce::Graphics& g) {
|
|||
g.setColour(juce::Colour(0xff333333));
|
||||
g.drawRoundedRectangle(bounds, 9.0f, 1.0f);
|
||||
|
||||
// ---- Spectrum overlay (half-transparent, drawn behind the waveform) ----
|
||||
if (fft != nullptr) {
|
||||
std::array<float, ChromaFlockProcessor::fftSize * 2> fftData{};
|
||||
int writePos = processor.fftWritePos.load(std::memory_order_acquire);
|
||||
int fftSize = ChromaFlockProcessor::fftSize;
|
||||
|
||||
for (int i = 0; i < fftSize; ++i) {
|
||||
int idx = (writePos + i) % fftSize;
|
||||
fftData[i] = processor.fftInput[idx];
|
||||
}
|
||||
|
||||
for (int i = 0; i < fftSize; ++i) {
|
||||
float window = 0.5f - 0.5f * std::cos(2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(fftSize));
|
||||
fftData[i] *= window;
|
||||
}
|
||||
|
||||
fft->performFrequencyOnlyForwardTransform(fftData.data());
|
||||
|
||||
int numPoints = 128;
|
||||
float w = bounds.getWidth();
|
||||
float h = bounds.getHeight() - 22.0f;
|
||||
float bottom = bounds.getBottom() - 2.0f;
|
||||
int maxBin = fftSize / 4;
|
||||
float sampleRate = static_cast<float>(processor.getSampleRate());
|
||||
float binHz = sampleRate / static_cast<float>(fftSize);
|
||||
const float dbFloor = -48.0f;
|
||||
|
||||
// Log-frequency mapping so the low end (sub bass) spreads across the
|
||||
// display instead of bunching up in the leftmost ~10%.
|
||||
const float fLow = 20.0f;
|
||||
const float fHigh = static_cast<float>(maxBin) * binHz;
|
||||
const float logRange = std::log(fHigh / fLow);
|
||||
|
||||
if (specSmooth.size() != static_cast<size_t>(numPoints + 1))
|
||||
specSmooth.assign(numPoints + 1, 0.0f);
|
||||
|
||||
std::vector<float> mags(numPoints + 1);
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float t = static_cast<float>(i) / static_cast<float>(numPoints);
|
||||
float tN = juce::jmin(t + 1.0f / static_cast<float>(numPoints), 1.0f);
|
||||
float fL = fLow * std::exp(logRange * t);
|
||||
float fN = fLow * std::exp(logRange * tN);
|
||||
int binStart = juce::jmax(1, static_cast<int>(fL / binHz));
|
||||
int binEnd = static_cast<int>(fN / binHz) + 1;
|
||||
if (binEnd <= binStart) binEnd = binStart + 1;
|
||||
if (binEnd > maxBin) binEnd = maxBin;
|
||||
|
||||
float mag = 0.0f;
|
||||
int count = 0;
|
||||
for (int b = binStart; b < binEnd; ++b) {
|
||||
mag += fftData[b];
|
||||
++count;
|
||||
}
|
||||
mag = count > 0 ? mag / static_cast<float>(count) : 0.0f;
|
||||
|
||||
// dB scale: 0 dB reference ≈ full-scale sine peak, floor at dbFloor.
|
||||
float lin = mag / static_cast<float>(fftSize) * 4.0f;
|
||||
float db = 20.0f * std::log10(lin + 1.0e-6f);
|
||||
mags[i] = juce::jlimit(0.0f, 1.0f, (db - dbFloor) / -dbFloor);
|
||||
}
|
||||
|
||||
// Spatial smoothing between adjacent points (rolling-hill look).
|
||||
std::vector<float> blurred = mags;
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float a = mags[static_cast<size_t>(juce::jmax(0, i - 1))];
|
||||
float c = mags[static_cast<size_t>(juce::jmin(numPoints, i + 1))];
|
||||
blurred[static_cast<size_t>(i)] = (a + 2.0f * mags[static_cast<size_t>(i)] + c) * 0.25f;
|
||||
}
|
||||
mags = blurred;
|
||||
}
|
||||
|
||||
// Time smoothing (EMA) so the curve glides instead of jumping.
|
||||
// Asymmetric: fast attack, slower fall — a released note's spectrum
|
||||
// decays away instead of being held up.
|
||||
const float emaUp = 0.7f;
|
||||
const float emaDown = 0.6f;
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float& s = specSmooth[static_cast<size_t>(i)];
|
||||
float m = mags[static_cast<size_t>(i)];
|
||||
float coeff = m > s ? emaUp : emaDown;
|
||||
s = coeff * s + (1.0f - coeff) * m;
|
||||
}
|
||||
|
||||
juce::Path specPath;
|
||||
specPath.startNewSubPath(bounds.getX(), bottom);
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float x = bounds.getX() + (static_cast<float>(i) / static_cast<float>(numPoints)) * w;
|
||||
float y = bottom - specSmooth[static_cast<size_t>(i)] * h;
|
||||
specPath.lineTo(x, y);
|
||||
}
|
||||
specPath.lineTo(bounds.getRight(), bottom);
|
||||
specPath.closeSubPath();
|
||||
|
||||
{
|
||||
juce::Graphics::ScopedSaveState saved(g);
|
||||
juce::Path clipPath;
|
||||
clipPath.addRoundedRectangle(bounds, 9.0f);
|
||||
g.reduceClipRegion(clipPath);
|
||||
|
||||
juce::ColourGradient specGrad(juce::Colour(0xff7b94b5).withAlpha(0.5f), 0.0f, bottom,
|
||||
juce::Colour(0xff2a3a4a).withAlpha(0.4f), 0.0f, bottom - h, false);
|
||||
g.setGradientFill(specGrad);
|
||||
g.fillPath(specPath);
|
||||
|
||||
g.setColour(juce::Colour(0xff9db8d8).withAlpha(0.55f));
|
||||
g.strokePath(specPath, juce::PathStrokeType(1.2f));
|
||||
}
|
||||
}
|
||||
|
||||
g.setColour(juce::Colour(0xff333333));
|
||||
g.drawLine(bounds.getX(), bounds.getCentreY(), bounds.getRight(), bounds.getCentreY(), 1.0f);
|
||||
|
||||
|
|
@ -245,12 +355,12 @@ void WaveformDisplay::paint(juce::Graphics& g) {
|
|||
clipPath.addRoundedRectangle(bounds, 9.0f);
|
||||
g.reduceClipRegion(clipPath);
|
||||
|
||||
juce::ColourGradient waveGrad(juce::Colour(0xffc59c07).withAlpha(0.5f), 0.0f, bounds.getY(),
|
||||
juce::Colour(0xff3d2e02).withAlpha(0.4f), 0.0f, bounds.getBottom(), false);
|
||||
juce::ColourGradient waveGrad(juce::Colour(0xff8fa35a).withAlpha(0.5f), 0.0f, bounds.getY(),
|
||||
juce::Colour(0xff2e3a18).withAlpha(0.4f), 0.0f, bounds.getBottom(), false);
|
||||
g.setGradientFill(waveGrad);
|
||||
g.fillPath(filledPath);
|
||||
|
||||
g.setColour(juce::Colour(0xffc59c07).withAlpha(0.9f));
|
||||
g.setColour(juce::Colour(0xff8fa35a).withAlpha(0.9f));
|
||||
g.strokePath(wavePath, juce::PathStrokeType(1.5f));
|
||||
}
|
||||
|
||||
|
|
@ -259,102 +369,6 @@ void WaveformDisplay::paint(juce::Graphics& g) {
|
|||
g.drawText("WAVE", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred);
|
||||
}
|
||||
|
||||
// --- Spectrum Analyzer ---
|
||||
void SpectrumAnalyzer::paint(juce::Graphics& g) {
|
||||
auto bounds = getLocalBounds().toFloat().reduced(0.7f);
|
||||
|
||||
juce::ColourGradient bgGrad(juce::Colour(0xBB222222), bounds.getCentreX(), bounds.getY(),
|
||||
juce::Colour(0xBB111111), bounds.getCentreX(), bounds.getBottom(), false);
|
||||
g.setGradientFill(bgGrad);
|
||||
g.fillRoundedRectangle(bounds, 9.0f);
|
||||
|
||||
g.setColour(juce::Colour(0xff333333));
|
||||
g.drawRoundedRectangle(bounds, 9.0f, 1.0f);
|
||||
|
||||
std::array<float, ChromaFlockProcessor::fftSize * 2> fftData{};
|
||||
int writePos = processor.fftWritePos.load(std::memory_order_acquire);
|
||||
int fftSize = ChromaFlockProcessor::fftSize;
|
||||
|
||||
for (int i = 0; i < fftSize; ++i) {
|
||||
int idx = (writePos + i) % fftSize;
|
||||
fftData[i] = processor.fftInput[idx];
|
||||
}
|
||||
|
||||
for (int i = 0; i < fftSize; ++i) {
|
||||
float window = 0.5f - 0.5f * std::cos(2.0f * 3.14159265f * static_cast<float>(i) / static_cast<float>(fftSize));
|
||||
fftData[i] *= window;
|
||||
}
|
||||
|
||||
fft->performFrequencyOnlyForwardTransform(fftData.data());
|
||||
|
||||
int numPoints = 128;
|
||||
float w = bounds.getWidth();
|
||||
float h = bounds.getHeight() - 22.0f;
|
||||
float bottom = bounds.getBottom() - 2.0f;
|
||||
int maxBin = fftSize / 4;
|
||||
float sampleRate = static_cast<float>(processor.getSampleRate());
|
||||
float binHz = sampleRate / static_cast<float>(fftSize);
|
||||
float lowCut = 80.0f;
|
||||
float lowPass = 250.0f;
|
||||
|
||||
juce::Graphics::ScopedSaveState savedClip(g);
|
||||
juce::Path clipPath;
|
||||
clipPath.addRoundedRectangle(bounds, 9.0f);
|
||||
g.reduceClipRegion(clipPath);
|
||||
|
||||
std::vector<float> mags(numPoints + 1);
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float t = static_cast<float>(i) / static_cast<float>(numPoints);
|
||||
int binStart = static_cast<int>(std::pow(t, 2.0f) * static_cast<float>(maxBin));
|
||||
int binEnd = static_cast<int>(std::pow(t + 1.0f / static_cast<float>(numPoints), 2.0f) * static_cast<float>(maxBin));
|
||||
if (binEnd <= binStart) binEnd = binStart + 1;
|
||||
if (binEnd > maxBin) binEnd = maxBin;
|
||||
|
||||
float mag = 0.0f;
|
||||
int count = 0;
|
||||
for (int b = binStart; b < binEnd; ++b) {
|
||||
mag += fftData[b];
|
||||
++count;
|
||||
}
|
||||
mag = count > 0 ? mag / static_cast<float>(count) : 0.0f;
|
||||
mag = mag / static_cast<float>(fftSize);
|
||||
mag = std::sqrt(mag) * 6.0f;
|
||||
|
||||
float centerHz = static_cast<float>((binStart + binEnd) / 2) * binHz;
|
||||
float rolloff = juce::jlimit(0.0f, 1.0f, (centerHz - lowCut) / (lowPass - lowCut));
|
||||
mag *= rolloff;
|
||||
mags[i] = juce::jlimit(0.0f, 1.0f, mag);
|
||||
}
|
||||
|
||||
juce::Path wavePath;
|
||||
wavePath.startNewSubPath(bounds.getX(), bottom);
|
||||
juce::Path strokePath;
|
||||
strokePath.startNewSubPath(bounds.getX(), bottom - mags[0] * h);
|
||||
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
float t = static_cast<float>(i) / static_cast<float>(numPoints);
|
||||
float x = bounds.getX() + t * w;
|
||||
float y = bottom - mags[i] * h;
|
||||
wavePath.lineTo(x, y);
|
||||
if (i > 0) strokePath.lineTo(x, y);
|
||||
}
|
||||
|
||||
wavePath.lineTo(bounds.getRight(), bottom);
|
||||
wavePath.closeSubPath();
|
||||
|
||||
juce::ColourGradient fillGrad(juce::Colour(0xffc59c07).withAlpha(0.35f), 0.0f, bottom,
|
||||
juce::Colour(0xffc59c07).withAlpha(0.02f), 0.0f, bottom - h, false);
|
||||
g.setGradientFill(fillGrad);
|
||||
g.fillPath(wavePath);
|
||||
|
||||
g.setColour(juce::Colour(0xffc59c07).withAlpha(0.9f));
|
||||
g.strokePath(strokePath, juce::PathStrokeType(1.5f));
|
||||
|
||||
g.setColour(juce::Colour(0xffccaa44));
|
||||
g.setFont(juce::Font(juce::FontOptions(13.0f).withStyle("Bold")));
|
||||
g.drawText("SPECTRUM", bounds.withBottom(bounds.getY() + 22.0f).translated(0, 6), juce::Justification::centred);
|
||||
}
|
||||
|
||||
// --- Patch LCD (amber dot-matrix) ---
|
||||
namespace {
|
||||
struct Glyph { char c; unsigned char rows[7]; };
|
||||
|
|
@ -476,7 +490,7 @@ void MidiLed::paint(juce::Graphics& g) {
|
|||
}
|
||||
|
||||
MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
|
||||
: processorRef(p), vuMeter(p), waveformDisplay(p), spectrumAnalyzer(p), pianoRoll(p), midiLed(p) {
|
||||
: processorRef(p), vuMeter(p), waveformDisplay(p), pianoRoll(p), midiLed(p) {
|
||||
|
||||
auto setupParam = [&](juce::Slider& knob, std::unique_ptr<SliderAttachment>& attach,
|
||||
const juce::String& paramId, const juce::String& name) {
|
||||
|
|
@ -667,9 +681,29 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
|
|||
|
||||
addAndMakeVisible(vuMeter);
|
||||
addAndMakeVisible(waveformDisplay);
|
||||
addAndMakeVisible(spectrumAnalyzer);
|
||||
addAndMakeVisible(pianoRoll);
|
||||
|
||||
// Arpeggiator section
|
||||
auto setupArpLabel = [&](juce::Label& label, const juce::String& text) {
|
||||
label.setText(text, juce::dontSendNotification);
|
||||
label.setFont(juce::Font(juce::FontOptions(11.0f).withStyle("Bold")));
|
||||
label.setColour(juce::Label::textColourId, juce::Colour(0xff888888));
|
||||
label.setJustificationType(juce::Justification::centred);
|
||||
addAndMakeVisible(label);
|
||||
};
|
||||
setupArpLabel(arpEnabledLabel, "ON");
|
||||
setupArpLabel(arpPatternLabel, "PATTERN");
|
||||
setupArpLabel(arpOctavesLabel, "OCTAVES");
|
||||
setupArpLabel(arpDirectionLabel, "DIR");
|
||||
setupArpLabel(arpRateLabel, "RATE");
|
||||
|
||||
setupCB(arpEnabledBox, arpEnabledAttach, "arpEnabled", {"Off", "On"});
|
||||
setupCB(arpPatternBox, arpPatternAttach, "arpPattern",
|
||||
{"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"});
|
||||
setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"});
|
||||
setupCB(arpDirectionBox, arpDirectionAttach, "arpDirection", {"Up", "Down"});
|
||||
setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/16", "1/8", "1/4", "1/2", "1", "2"});
|
||||
|
||||
setupCombo(uiScaleBox);
|
||||
uiScaleBox.addItem("100%", 1);
|
||||
uiScaleBox.addItem("125%", 2);
|
||||
|
|
@ -1050,6 +1084,9 @@ void MainContentComponent::paint(juce::Graphics& g) {
|
|||
// FX2 sub-sections
|
||||
drawSubSection(1015, 586, 375, 150, "DELAY", 4);
|
||||
drawSubSection(1395, 586, 255, 150, "REVERB", 4);
|
||||
|
||||
// Arpeggiator section (replaces the removed spectrum analyzer)
|
||||
drawSubSection(860, 746, 790, 128, "ARPEGGIATOR", 6);
|
||||
}
|
||||
|
||||
void MainContentComponent::resized() {
|
||||
|
|
@ -1242,7 +1279,39 @@ void MainContentComponent::resized() {
|
|||
int vizH = 128;
|
||||
vuMeter.setBounds(10, vizY, 40, vizH);
|
||||
waveformDisplay.setBounds(60, vizY, 790, vizH);
|
||||
spectrumAnalyzer.setBounds(860, vizY, 790, vizH);
|
||||
|
||||
// Arpeggiator controls (right of the waveform display)
|
||||
{
|
||||
int arpX = 860, arpY = vizY, arpW = 790;
|
||||
int comboH = 28;
|
||||
int labelH = 16;
|
||||
int labelY = arpY + 26;
|
||||
int comboY = labelY + labelH + 8;
|
||||
|
||||
int widths[] = {90, 170, 90, 90, 110};
|
||||
int gap = 22;
|
||||
int totalW = widths[0] + widths[1] + widths[2] + widths[3] + widths[4] + gap * 4;
|
||||
int x = arpX + (arpW - totalW) / 2;
|
||||
|
||||
arpEnabledLabel.setBounds(x, labelY, widths[0], labelH);
|
||||
arpEnabledBox.setBounds(x, comboY, widths[0], comboH);
|
||||
x += widths[0] + gap;
|
||||
|
||||
arpPatternLabel.setBounds(x, labelY, widths[1], labelH);
|
||||
arpPatternBox.setBounds(x, comboY, widths[1], comboH);
|
||||
x += widths[1] + gap;
|
||||
|
||||
arpOctavesLabel.setBounds(x, labelY, widths[2], labelH);
|
||||
arpOctavesBox.setBounds(x, comboY, widths[2], comboH);
|
||||
x += widths[2] + gap;
|
||||
|
||||
arpDirectionLabel.setBounds(x, labelY, widths[3], labelH);
|
||||
arpDirectionBox.setBounds(x, comboY, widths[3], comboH);
|
||||
x += widths[3] + gap;
|
||||
|
||||
arpRateLabel.setBounds(x, labelY, widths[4], labelH);
|
||||
arpRateBox.setBounds(x, comboY, widths[4], comboH);
|
||||
}
|
||||
|
||||
// Piano roll
|
||||
pianoRoll.setBounds(10, 884, 1640, 136);
|
||||
|
|
|
|||
|
|
@ -46,16 +46,7 @@ private:
|
|||
|
||||
class WaveformDisplay : public juce::Component, public juce::Timer {
|
||||
public:
|
||||
explicit WaveformDisplay(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); }
|
||||
void paint(juce::Graphics& g) override;
|
||||
void timerCallback() override { repaint(); }
|
||||
private:
|
||||
ChromaFlockProcessor& processor;
|
||||
};
|
||||
|
||||
class SpectrumAnalyzer : public juce::Component, public juce::Timer {
|
||||
public:
|
||||
explicit SpectrumAnalyzer(ChromaFlockProcessor& p) : processor(p) {
|
||||
explicit WaveformDisplay(ChromaFlockProcessor& p) : processor(p) {
|
||||
fft = std::make_unique<juce::dsp::FFT>(ChromaFlockProcessor::fftOrder);
|
||||
startTimerHz(30);
|
||||
}
|
||||
|
|
@ -64,6 +55,7 @@ public:
|
|||
private:
|
||||
ChromaFlockProcessor& processor;
|
||||
std::unique_ptr<juce::dsp::FFT> fft;
|
||||
std::vector<float> specSmooth;
|
||||
};
|
||||
|
||||
class PatchLCD : public juce::Component {
|
||||
|
|
@ -143,7 +135,6 @@ private:
|
|||
|
||||
VuMeter vuMeter;
|
||||
WaveformDisplay waveformDisplay;
|
||||
SpectrumAnalyzer spectrumAnalyzer;
|
||||
PianoRollComponent pianoRoll;
|
||||
|
||||
juce::Label osc1Label, osc2Label, filterLabel, envLabel, fEnvLabel, globalLabel;
|
||||
|
|
@ -185,6 +176,10 @@ private:
|
|||
juce::ComboBox delaySyncBox;
|
||||
KnobSlider reverbSizeKnob, reverbDampKnob, reverbMixKnob;
|
||||
|
||||
// Arpeggiator controls
|
||||
juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel;
|
||||
juce::ComboBox arpEnabledBox, arpPatternBox, arpOctavesBox, arpDirectionBox, arpRateBox;
|
||||
|
||||
// Preset menu
|
||||
juce::TextButton presetButton{"PRESET"};
|
||||
PatchLCD patchLCD;
|
||||
|
|
@ -227,6 +222,10 @@ private:
|
|||
std::unique_ptr<SliderAttachment> delayPingPongAttach;
|
||||
std::unique_ptr<SliderAttachment> reverbSizeAttach, reverbDampAttach, reverbMixAttach;
|
||||
|
||||
// Arpeggiator attachments
|
||||
std::unique_ptr<ComboBoxAttachment> arpEnabledAttach, arpPatternAttach, arpOctavesAttach;
|
||||
std::unique_ptr<ComboBoxAttachment> arpDirectionAttach, arpRateAttach;
|
||||
|
||||
void setupLabel(juce::Label& label, const juce::String& text);
|
||||
void setupKnob(juce::Slider& knob, const juce::String& name);
|
||||
void setupCombo(juce::ComboBox& combo);
|
||||
|
|
|
|||
|
|
@ -274,6 +274,23 @@ juce::AudioProcessorValueTreeState::ParameterLayout ChromaFlockProcessor::create
|
|||
layout.add(std::make_unique<juce::AudioParameterFloat>(
|
||||
juce::ParameterID{"reverbMix", 1}, "Reverb Mix", zeroOne, 0.2f));
|
||||
|
||||
// ARPEGGIATOR
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"arpEnabled", 1}, "Arp On",
|
||||
juce::StringArray{"Off", "On"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"arpPattern", 1}, "Arp Pattern",
|
||||
juce::StringArray{"Up", "Down", "UpDown", "DownUp", "Random", "As Played", "Chord"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"arpOctaves", 1}, "Arp Octaves",
|
||||
juce::StringArray{"1", "2", "3"}, 1));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"arpDirection", 1}, "Arp Direction",
|
||||
juce::StringArray{"Up", "Down"}, 0));
|
||||
layout.add(std::make_unique<juce::AudioParameterChoice>(
|
||||
juce::ParameterID{"arpRate", 1}, "Arp Rate",
|
||||
juce::StringArray{"1/16", "1/8", "1/4", "1/2", "1", "2"}, 1));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +303,9 @@ void ChromaFlockProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
|||
delay.prepare(sampleRate);
|
||||
reverbFX.prepare(sampleRate);
|
||||
autoPanPhase = 0.0f;
|
||||
arpSampleCount = 0.0;
|
||||
std::vector<int> arpStop;
|
||||
arp.reset(arpStop);
|
||||
updateVoiceParameters();
|
||||
}
|
||||
|
||||
|
|
@ -383,19 +403,85 @@ void ChromaFlockProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::
|
|||
buffer.clear();
|
||||
updateVoiceParameters();
|
||||
|
||||
for (const auto metadata : midiMessages)
|
||||
if (metadata.getMessage().isNoteOn())
|
||||
activeNotes[metadata.getMessage().getNoteNumber()].store(true, std::memory_order_relaxed);
|
||||
else if (metadata.getMessage().isNoteOff())
|
||||
activeNotes[metadata.getMessage().getNoteNumber()].store(false, std::memory_order_relaxed);
|
||||
auto getRaw = [&](const juce::String& id) -> float {
|
||||
auto* p = apvts.getRawParameterValue(id);
|
||||
return p != nullptr ? p->load() : 0.0f;
|
||||
};
|
||||
|
||||
bool arpOn = getRaw("arpEnabled") > 0.5f;
|
||||
if (arpOn) {
|
||||
static const double arpRateBeats[] = {0.25, 0.5, 1.0, 2.0, 4.0, 8.0};
|
||||
int rateIdx = juce::jlimit(0, 5, juce::roundToInt(getRaw("arpRate")));
|
||||
arp.setParameters(true,
|
||||
juce::roundToInt(getRaw("arpPattern")),
|
||||
juce::roundToInt(getRaw("arpOctaves")) + 1,
|
||||
juce::roundToInt(getRaw("arpDirection")),
|
||||
arpRateBeats[rateIdx],
|
||||
currentBpm);
|
||||
} else {
|
||||
arp.setParameters(false, 0, 1, 0, 0.5, currentBpm);
|
||||
}
|
||||
|
||||
int transpose = getTransposeSemitones();
|
||||
juce::MidiBuffer transposed;
|
||||
for (const auto metadata : midiMessages) {
|
||||
auto msg = metadata.getMessage();
|
||||
if (msg.isNoteOn() || msg.isNoteOff())
|
||||
msg.setNoteNumber(juce::jlimit(0, 127, msg.getNoteNumber() + transpose));
|
||||
transposed.addEvent(msg, metadata.samplePosition);
|
||||
if (msg.isNoteOn()) {
|
||||
int note = msg.getNoteNumber();
|
||||
activeNotes[note].store(true, std::memory_order_relaxed);
|
||||
if (arpOn)
|
||||
arp.noteOn(note, msg.getFloatVelocity());
|
||||
else {
|
||||
msg.setNoteNumber(juce::jlimit(0, 127, note + transpose));
|
||||
transposed.addEvent(msg, metadata.samplePosition);
|
||||
}
|
||||
} else if (msg.isNoteOff()) {
|
||||
int note = msg.getNoteNumber();
|
||||
activeNotes[note].store(false, std::memory_order_relaxed);
|
||||
if (arpOn)
|
||||
arp.noteOff(note);
|
||||
else {
|
||||
msg.setNoteNumber(juce::jlimit(0, 127, note + transpose));
|
||||
transposed.addEvent(msg, metadata.samplePosition);
|
||||
}
|
||||
} else {
|
||||
transposed.addEvent(msg, metadata.samplePosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (arpOn) {
|
||||
// A freshly pressed key triggers the first note immediately; the
|
||||
// clock then starts from zero so the subsequent ticks stay synced.
|
||||
if (arp.shouldTriggerNow()) {
|
||||
std::vector<int> stopNotes;
|
||||
std::vector<Arpeggiator::NotePitch> playNotes;
|
||||
arp.step(stopNotes, playNotes);
|
||||
for (int n : stopNotes)
|
||||
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
||||
for (const auto& np : playNotes)
|
||||
synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity);
|
||||
arpSampleCount = 0.0;
|
||||
}
|
||||
|
||||
double tickSamples = arp.getTickSeconds() * currentSampleRate;
|
||||
if (tickSamples < 1.0) tickSamples = 1.0;
|
||||
arpSampleCount += buffer.getNumSamples();
|
||||
while (arpSampleCount >= tickSamples) {
|
||||
arpSampleCount -= tickSamples;
|
||||
std::vector<int> stopNotes;
|
||||
std::vector<Arpeggiator::NotePitch> playNotes;
|
||||
arp.step(stopNotes, playNotes);
|
||||
for (int n : stopNotes)
|
||||
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
||||
for (const auto& np : playNotes)
|
||||
synth.noteOn(1, juce::jlimit(0, 127, np.note + transpose), np.velocity);
|
||||
}
|
||||
} else {
|
||||
arpSampleCount = 0.0;
|
||||
std::vector<int> stopNotes;
|
||||
arp.reset(stopNotes);
|
||||
for (int n : stopNotes)
|
||||
synth.noteOff(1, juce::jlimit(0, 127, n + transpose), 0.0f, true);
|
||||
}
|
||||
|
||||
synth.renderNextBlock(buffer, transposed, 0, buffer.getNumSamples());
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_audio_basics/juce_audio_basics.h>
|
||||
#include <juce_dsp/juce_dsp.h>
|
||||
#include "DSP/Voice.h"
|
||||
#include "DSP/Arpeggiator.h"
|
||||
#include "DSP/Distortion.h"
|
||||
#include "DSP/Compressor.h"
|
||||
#include "DSP/Limiter.h"
|
||||
|
|
@ -53,17 +53,30 @@ public:
|
|||
std::array<float, fftSize> fftInput{};
|
||||
std::atomic<int> fftWritePos{0};
|
||||
|
||||
Arpeggiator arp;
|
||||
|
||||
float getRmsLevel() const { return rmsLevel.load(); }
|
||||
|
||||
bool isArpEnabled() const {
|
||||
auto* p = apvts.getRawParameterValue("arpEnabled");
|
||||
return p != nullptr && p->load() > 0.5f;
|
||||
}
|
||||
|
||||
void noteOn(int midiNote, float velocity) {
|
||||
if (midiNote >= 0 && midiNote < 128)
|
||||
activeNotes[midiNote].store(true, std::memory_order_relaxed);
|
||||
synth.noteOn(1, juce::jlimit(0, 127, midiNote + getTransposeSemitones()), velocity);
|
||||
if (isArpEnabled())
|
||||
arp.noteOn(midiNote, velocity);
|
||||
else
|
||||
synth.noteOn(1, juce::jlimit(0, 127, midiNote + getTransposeSemitones()), velocity);
|
||||
}
|
||||
void noteOff(int midiNote) {
|
||||
if (midiNote >= 0 && midiNote < 128)
|
||||
activeNotes[midiNote].store(false, std::memory_order_relaxed);
|
||||
synth.noteOff(1, juce::jlimit(0, 127, midiNote + getTransposeSemitones()), 0.0f, true);
|
||||
if (isArpEnabled())
|
||||
arp.noteOff(midiNote);
|
||||
else
|
||||
synth.noteOff(1, juce::jlimit(0, 127, midiNote + getTransposeSemitones()), 0.0f, true);
|
||||
}
|
||||
bool isNoteActive(int midiNote) const {
|
||||
return midiNote >= 0 && midiNote < 128
|
||||
|
|
@ -102,6 +115,7 @@ private:
|
|||
|
||||
double currentSampleRate = 44100.0;
|
||||
double currentBpm = 120.0;
|
||||
double arpSampleCount = 0.0;
|
||||
|
||||
std::atomic<float> rmsLevel{0.0f};
|
||||
std::array<std::atomic<bool>, 128> activeNotes{};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue