Fetch JUCE into build/, tidy gitignore, expand README

This commit is contained in:
Armin 2026-09-12 21:53:16 +02:00
commit ab9ea542a7
12 changed files with 350 additions and 149 deletions

View file

@ -1,66 +1,116 @@
#include "LcdDisplay.h"
namespace {
// Classic 5x8 dot-matrix LCD character generator ROM (HD44780-style),
// rows top-to-bottom, bit 4 = leftmost column. Index = ASCII - 32.
constexpr uint8_t kFont[95][8] =
{
{0,0,0,0,0,0,0,0}, // space
{4,4,4,4,4,0,4,0}, // !
{10,10,10,0,0,0,0,0}, // "
{0,10,31,10,31,10,0,0}, // #
{4,15,20,14,5,30,4,0}, // $
{2,4,8,16,14,17,14,0}, // %
{8,20,20,8,21,18,13,0}, // &
{4,4,4,0,0,0,0,0}, // '
{2,4,8,8,8,4,2,0}, // (
{8,4,2,2,2,4,8,0}, // )
{0,10,4,31,4,10,0,0}, // *
{0,4,4,31,4,4,0,0}, // +
{0,0,0,0,6,2,4,0}, // ,
{0,0,0,31,0,0,0,0}, // -
{0,0,0,0,0,0,12,0}, // .
{1,2,4,8,16,0,0,0}, // /
{14,17,19,21,25,17,14,0}, // 0
{4,12,4,4,4,4,14,0}, // 1
{14,17,1,2,4,8,31,0}, // 2
{14,17,1,6,1,17,14,0}, // 3
{2,6,10,18,31,2,2,0}, // 4
{31,16,30,1,1,17,14,0}, // 5
{6,8,16,30,17,17,14,0}, // 6
{31,1,2,4,8,8,8,0}, // 7
{14,17,17,14,17,17,14,0}, // 8
{14,17,17,15,1,2,12,0}, // 9
{0,0,6,0,0,6,0,0}, // :
{0,0,6,0,0,6,2,4}, // ;
{2,4,8,16,8,4,2,0}, // <
{0,0,31,0,31,0,0,0}, // =
{8,4,2,1,2,4,8,0}, // >
{14,17,1,2,4,0,4,0}, // ?
{14,17,1,13,21,17,14,0}, // @
{14,17,17,31,17,17,17,0}, // A
{30,17,17,30,17,17,30,0}, // B
{14,17,16,16,16,17,14,0}, // C
{30,17,17,17,17,17,30,0}, // D
{31,16,16,30,16,16,31,0}, // E
{31,16,16,30,16,16,16,0}, // F
{14,17,16,23,17,17,15,0}, // G
{17,17,17,31,17,17,17,0}, // H
{14,4,4,4,4,4,14,0}, // I
{7,2,2,2,2,18,12,0}, // J
{17,18,20,24,20,18,17,0}, // K
{16,16,16,16,16,16,31,0}, // L
{17,27,21,21,17,17,17,0}, // M
{17,25,21,19,17,17,17,0}, // N
{14,17,17,17,17,17,14,0}, // O
{30,17,17,30,16,16,16,0}, // P
{14,17,17,17,21,18,13,0}, // Q
{30,17,17,30,20,18,17,0}, // R
{15,16,16,14,1,1,30,0}, // S
{31,4,4,4,4,4,4,0}, // T
{17,17,17,17,17,17,14,0}, // U
{17,17,17,17,17,10,4,0}, // V
{17,17,17,21,21,27,17,0}, // W
{17,17,10,4,10,17,17,0}, // X
{17,17,17,10,4,4,4,0}, // Y
{31,1,2,4,8,16,31,0}, // Z
{14,8,8,8,8,8,14,0}, // [
{16,8,4,2,1,0,0,0}, // backslash
{14,2,2,2,2,2,14,0}, // ]
{4,10,17,0,0,0,0,0}, // ^
{0,0,0,0,0,0,31,0}, // _
{8,4,2,0,0,0,0,0}, // `
{0,0,14,1,15,17,15,0}, // a
{16,16,30,17,17,17,30,0}, // b
{0,0,14,16,16,17,14,0}, // c
{1,1,15,17,17,17,15,0}, // d
{0,0,14,17,31,16,14,0}, // e
{2,4,4,14,4,4,4,0}, // f
{0,0,15,17,17,15,1,14}, // g
{16,16,30,17,17,17,17,0}, // h
{0,6,0,4,4,4,4,0}, // i
{0,6,0,4,4,4,8,0}, // j
{0,0,16,18,20,24,20,0}, // k
{0,0,12,4,4,4,4,0}, // l
{0,0,26,21,21,21,21,0}, // m
{0,0,18,29,17,17,17,0}, // n
{0,0,14,17,17,17,14,0}, // o
{0,30,17,17,30,16,16,0}, // p
{0,0,15,17,17,15,1,1}, // q
{0,0,30,17,16,16,16,0}, // r
{0,0,15,16,14,1,30,0}, // s
{0,4,4,14,4,4,2,0}, // t
{0,0,17,17,17,19,13,0}, // u
{0,0,17,17,17,10,4,0}, // v
{0,0,17,17,21,21,10,0}, // w
{0,0,17,10,4,10,17,0}, // x
{0,0,17,17,17,10,12,0}, // y
{0,0,31,2,4,8,31,0}, // z
{4,8,8,16,8,8,4,0}, // {
{4,4,4,4,4,4,4,0}, // |
{8,4,4,2,4,4,8,0}, // }
{13,18,0,0,0,0,0,0}, // ~
};
} // namespace
LcdDisplay::LcdDisplay()
{
clear();
startTimerHz (30);
}
void LcdDisplay::rasterize (juce::uint32 c)
{
if (c >= 256 || glyphCache[c].cached) return;
juce::Image img (juce::Image::ARGB, kRasterW, kRasterH, true);
{
juce::Graphics g (img);
g.setColour (juce::Colours::white);
g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(),
14.0f, juce::Font::plain }));
juce::String s;
s << (juce_wchar) c;
g.drawText (s, img.getBounds().removeFromBottom (kRasterH - 2),
juce::Justification::centredBottom, false);
}
const float sx = (float) kRasterW / 5.0f;
const float sy = (float) kRasterH / 8.0f;
for (int row = 0; row < 8; ++row)
glyphCache[c].rows[row] = 0;
for (int row = 0; row < 8; ++row)
{
uint8_t bits = 0;
for (int col = 0; col < 5; ++col)
{
int count = 0, total = 0;
int px0 = (int) (col * sx);
int py0 = (int) (row * sy);
int px1 = juce::jmin ((int) ((col + 1) * sx) - 1, kRasterW - 1);
int py1 = juce::jmin ((int) ((row + 1) * sy) - 1, kRasterH - 1);
for (int y = py0; y <= py1; ++y)
for (int x = px0; x <= px1; ++x)
{
total++;
if (img.getPixelAt (x, y).getAlpha() > 127)
count++;
}
if (total > 0 && count > total / 3)
bits |= (uint8_t) (1u << (4 - col));
}
glyphCache[c].rows[row] = bits;
}
glyphCache[c].cached = true;
}
uint8_t* LcdDisplay::getGlyph (juce::uint32 c)
{
if (c >= 256) c = '?';
if (! glyphCache[c].cached)
rasterize (c);
return glyphCache[c].rows;
}
void LcdDisplay::setLine (int row, const juce::String& s)
{
if (row >= 0 && row < kRows)
@ -78,6 +128,13 @@ void LcdDisplay::clear()
cursorVisible = false;
}
const uint8_t* LcdDisplay::getGlyph (juce::uint32 c) const
{
if (c < 32 || c > 126)
c = '?';
return kFont[c - 32];
}
void LcdDisplay::paint (juce::Graphics& g)
{
const float w = (float) getWidth();
@ -95,6 +152,22 @@ void LcdDisplay::paint (juce::Graphics& g)
float ox = 0.0f;
float oy = 0.0f;
float padX = cw * 0.18f;
float padY = ch * 0.10f;
float gw = cw - padX * 2.0f;
float gh = ch - padY * 2.0f;
constexpr float kAspect = 2.0f;
constexpr float kGapCol = 0.22f;
constexpr float kGapRow = 0.12f;
const float gridW = 5.0f + 4.0f * kGapCol;
const float gridH = kAspect * (8.0f + 7.0f * kGapRow);
const float cellW = juce::jmax (0.0f, juce::jmin (gw / gridW, gh / gridH));
const float cellH = cellW * kAspect;
const float stepX = cellW * (1.0f + kGapCol);
const float stepY = cellH * (1.0f + kGapRow);
bool blinkOn = cursorVisible &&
((int) (juce::Time::getMillisecondCounterHiRes() / 400.0) % 2 == 0);
@ -104,7 +177,7 @@ void LcdDisplay::paint (juce::Graphics& g)
for (int col = 0; col < kCols; ++col)
{
juce_wchar ch2 = col < line.length() ? line [(size_t) col] : (juce_wchar) ' ';
uint8_t* glyph = getGlyph ((juce::uint32) ch2);
const uint8_t* glyph = getGlyph ((juce::uint32) ch2);
float cx = ox + (float) col * cw;
float cy = oy + (float) row * ch;
@ -112,10 +185,8 @@ void LcdDisplay::paint (juce::Graphics& g)
g.setColour (pixOff);
g.fillRect (cx + 0.5f, cy + 0.5f, cw - 1.0f, ch - 1.0f);
float padX = cw * 0.12f;
float padY = ch * 0.1f;
float gw = cw - padX * 2.0f;
float gh = ch - padY * 2.0f;
float gridX = cx + padX + (gw - cellW * gridW) * 0.5f;
float gridY = cy + padY + (gh - cellW * gridH) * 0.5f;
for (int gRow = 0; gRow < 8; ++gRow)
{
@ -125,15 +196,14 @@ void LcdDisplay::paint (juce::Graphics& g)
{
if (bits & (1u << (4 - gCol)))
{
float px = cx + padX + (float) gCol * gw / 5.0f;
float py = cy + padY + (float) gRow * gh / 8.0f;
float pw = gw / 5.0f - 0.5f;
float ph = gh / 8.0f - 0.4f;
float px = gridX + (float) gCol * stepX;
float py = gridY + (float) gRow * stepY;
g.setColour (pixOnDim);
g.fillRect (px - 0.6f, py - 0.4f, pw + 1.2f, ph + 0.8f);
g.fillRect (px - cellW * 0.10f, py - cellH * 0.05f,
cellW * 1.20f, cellH * 1.10f);
g.setColour (pixOn);
g.fillRect (px, py, pw, ph);
g.fillRect (px, py, cellW, cellH);
}
}
}
@ -147,4 +217,4 @@ void LcdDisplay::paint (juce::Graphics& g)
}
}
void LcdDisplay::timerCallback() { repaint(); }
void LcdDisplay::timerCallback() { repaint(); }

View file

@ -19,24 +19,12 @@ public:
private:
void timerCallback() override;
static constexpr int kRasterW = 12;
static constexpr int kRasterH = 18;
juce::String lines[kRows];
int cursorCol = 0;
int cursorRow = 0;
bool cursorVisible = false;
struct GlyphCache {
uint8_t rows[8] = {};
bool cached = false;
};
GlyphCache glyphCache[256];
void rasterize (juce::uint32 c);
uint8_t* getGlyph (juce::uint32 c);
juce::Font getMonoFont() const;
const uint8_t* getGlyph (juce::uint32 c) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LcdDisplay)
};

View file

@ -68,10 +68,17 @@ void SM26AudioProcessorEditor::setupKnobs()
}
}
juce::Font SM26AudioProcessorEditor::NavButtonLookAndFeel::getTextButtonFont (juce::TextButton&, int)
{
return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(),
10.5f, juce::Font::bold });
}
void SM26AudioProcessorEditor::setupNavButtons()
{
auto setup = [this](juce::TextButton& btn, juce::Colour bg)
{
btn.setLookAndFeel (&navLookAndFeel);
btn.setColour (juce::TextButton::buttonColourId, bg);
btn.setColour (juce::TextButton::textColourOffId, juce::Colour (0xFFE0E0C0));
btn.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A4A18));
@ -85,14 +92,22 @@ void SM26AudioProcessorEditor::setupNavButtons()
setup (navEnter, juce::Colour (0xFF3A6A28));
setup (navEsc, juce::Colour (0xFF3A2A18));
navUp.onClick = [this] { menu.up(); };
navDown.onClick = [this] { menu.down(); };
navUp.onClick = [this] { if (menu.isOpen()) menu.up(); else cyclePreset (-1); };
navDown.onClick = [this] { if (menu.isOpen()) menu.down(); else cyclePreset (+1); };
navLeft.onClick = [this] { menu.left(); };
navRight.onClick = [this] { menu.right(); };
navEnter.onClick = [this] { menu.open(); };
navEsc.onClick = [this] { if (menu.isOpen()) menu.escape(); else menu.close(); };
}
void SM26AudioProcessorEditor::cyclePreset (int delta)
{
const int count = sm26::PresetStore::count();
if (count <= 0) return;
const int idx = proc.engine.presetIndex();
proc.setPresetByIndex ((idx + delta + count) % count);
}
void SM26AudioProcessorEditor::buildRootMenu()
{
auto builder = [this]() -> MenuScreen
@ -356,7 +371,7 @@ void SM26AudioProcessorEditor::buildRootMenu()
}
{
MenuItem m2;
m2.label = "48 presets | VST3 + AU";
m2.label = "52 presets | VST3 + AU";
m2.kind = MenuItem::Action;
as.items.push_back (m2);
}
@ -463,7 +478,7 @@ void SM26AudioProcessorEditor::resized()
float knobGap = 18.0f;
float knobsW = 4.0f * knobSize + 3.0f * knobGap;
float knobsX = (totalW - knobsW) / 2.0f;
float knobsY = totalH - 135.0f;
float knobsY = juce::jmax (190.0f, totalH - 265.0f);
for (int i = 0; i < 4; ++i)
{
@ -504,7 +519,11 @@ void SM26AudioProcessorEditor::timerCallback()
{
auto kv = proc.engine.knobView (i);
float val = proc.engine.getParam (kv.param);
knobSliders[i].setValue (val, juce::dontSendNotification);
if (! knobDragging[i])
{
float pos = sm26::clamp01 ((val - kv.lo) / (kv.hi - kv.lo + 1e-9f));
knobSliders[i].setValue (pos, juce::dontSendNotification);
}
knobLabels[i].setText (kv.name, juce::dontSendNotification);
knobValueLabels[i].setText (sm26::formatParamReal (kv.param, val), juce::dontSendNotification);
}
@ -526,11 +545,27 @@ void SM26AudioProcessorEditor::sliderValueChanged (juce::Slider* s)
auto kv = proc.engine.knobView (i);
float paramVal = sm26::clamp01 (kv.lo + v * (kv.hi - kv.lo));
proc.engine.setParam (kv.param, paramVal);
knobValueLabels[i].setText (sm26::formatParamReal (kv.param, paramVal),
juce::dontSendNotification);
break;
}
}
}
void SM26AudioProcessorEditor::sliderDragStarted (juce::Slider* s)
{
for (int i = 0; i < 4; ++i)
if (s == &knobSliders[i])
knobDragging[i] = true;
}
void SM26AudioProcessorEditor::sliderDragEnded (juce::Slider* s)
{
for (int i = 0; i < 4; ++i)
if (s == &knobSliders[i])
knobDragging[i] = false;
}
void SM26AudioProcessorEditor::updateLCD()
{
if (! menu.isOpen())
@ -563,7 +598,8 @@ void SM26AudioProcessorEditor::updateLCD()
int notes = proc.engine.notesPlaying();
juce::String notesStr = notes > 0 ? "NOTES:" + juce::String (notes) : "";
lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr);
lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr
+ " UP/DN=PATCH OK=MENU");
lcd.showCursor (false);
}
else
@ -582,7 +618,7 @@ void SM26AudioProcessorEditor::updateLCD()
line = line + " [ENTER]";
lcd.setLine (1 + row, line);
}
lcd.setLine (3, "UP/DOWN=SEL L/R=VAL ENTER=OK ESC=BACK");
lcd.setLine (3, "UP/DN=SEL L/R=VAL OK=OK ESC=BACK");
lcd.showCursor (false);
}
}

View file

@ -21,26 +21,37 @@ public:
private:
void sliderValueChanged (juce::Slider*) override;
void sliderDragStarted (juce::Slider*) override;
void sliderDragEnded (juce::Slider*) override;
void setupKnobs();
void setupNavButtons();
void buildRootMenu();
void updateLCD();
void cyclePreset (int delta);
SM26AudioProcessor& proc;
MenuSystem menu;
class NavButtonLookAndFeel : public juce::LookAndFeel_V4
{
public:
juce::Font getTextButtonFont (juce::TextButton&, int) override;
} navLookAndFeel;
LcdDisplay lcd;
bool knobDragging[4] = {};
juce::Slider knobSliders[4];
juce::Label knobLabels[4];
juce::Label knobValueLabels[4];
juce::TextButton navUp { "UP" };
juce::TextButton navDown { "DOWN" };
juce::TextButton navLeft { "LEFT" };
juce::TextButton navRight { "RIGHT" };
juce::TextButton navEnter { "ENTER" };
juce::TextButton navDown { "DN" };
juce::TextButton navLeft { "LT" };
juce::TextButton navRight { "RT" };
juce::TextButton navEnter { "OK" };
juce::TextButton navEsc { "ESC" };
juce::Label footer;

View file

@ -131,7 +131,7 @@ void SM26AudioProcessor::parameterChanged (const juce::String& parameterID, floa
if (parameterID == "preset")
{
int idx = (int) std::round (newValue * (float) (sm26::PresetStore::count() - 1));
int idx = juce::roundToInt (newValue);
idx = juce::jlimit (0, sm26::PresetStore::count() - 1, idx);
engine.setPresetIndex (idx);
applyPresetKnobs();

View file

@ -5,7 +5,7 @@ namespace sm26 {
namespace {
struct Catalog {
Preset data[48];
Preset data[52];
int n = 0;
};

View file

@ -53,6 +53,7 @@ void SynthEngine::loadPreset (const Preset& p)
knobHighs[i] = p.ctl[i].hi;
}
allNotesOff();
delay.clear();
}
void SynthEngine::setPresetIndex (int i)
@ -243,14 +244,14 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
const int filterType = paramFilterType (ftypeN01);
bool delaySync = params[PDSYNC] > 0.5f;
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS] * 15.0f));
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS]));
float delayMsParam = paramReal (PDTIME, params[PDTIME]);
float delaySec;
if (delaySync)
delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue);
else
delaySec = delayMsParam * 0.001f;
delaySec = std::clamp (delaySec, 0.001f, 17.0f);
delaySec = std::clamp (delaySec, 0.001f, 4.0f);
lastDelaySamples = delaySec * sampleRate;
const float delayFB = params[PDFB];
@ -258,18 +259,16 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
const float dryMix = 1.0f - delayMix;
const float wetMix = delayMix;
int delayWrite = (int) delay.writePos[0];
int delayRead = (delayWrite - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
(void) delayRead;
for (const auto& metadata : midi)
{
MidiMessage msg = metadata.getMessage();
int sm = metadata.samplePosition;
if (sm < n && msg.isNoteOn())
int sm = juce::jlimit (0, n - 1, metadata.samplePosition);
if (msg.isNoteOn())
noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
else if (sm < n && msg.isNoteOff())
else if (msg.isNoteOff())
noteOff (msg.getNoteNumber());
else if (msg.isAllNotesOff() || msg.isAllSoundOff())
allNotesOff();
}
float* outL = buffer.getWritePointer (0);
@ -339,7 +338,8 @@ void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
float dry = sumL;
int w = delay.writePos[0];
float delayed = delay.buf[0][w];
int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen;
float delayed = delay.buf[0][r];
float fb = dry + delayed * (float) delayFB;
fb = softClip (fb * 0.7f);
delay.buf[0][w] = fb;

View file

@ -178,7 +178,7 @@ String formatParamReal(int id, float n01)
return formatTimeMs (paramReal (id, n01) * 1000.0f);
case PDTIME: return formatTimeMs (paramReal (id, n01));
case PDSTEPS:
return stepsLabels() [juce::jlimit (0, 15, (int) juce::roundToInt (n01 * 15.0f))];
return stepsLabels() [juce::jlimit (0, 15, (int) juce::roundToInt (n01))];
case PDSYNC:
return n01 > 0.5f ? "SYNC" : "MS";
case PGLIDE: