mirror of
https://codeberg.org/armin/sm-26.git
synced 2026-10-11 17:01:53 +02:00
SM-26 synthesizer: initial implementation
This commit is contained in:
commit
8734ac25f1
18 changed files with 2619 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
build/
|
||||
deps/
|
||||
*.o
|
||||
*.d
|
||||
*.dylib
|
||||
.DS_Store
|
||||
*.dSYM/
|
||||
*.vst3/
|
||||
*.component/
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
Makefile.cmake
|
||||
JuceLibraryCode/
|
||||
88
CMakeLists.txt
Normal file
88
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
cmake_minimum_required(VERSION 3.22)
|
||||
project(SM26 VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
if(NOT DEFINED JUCE_ROOT)
|
||||
set(JUCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/deps/juce")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${JUCE_ROOT}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "JUCE not found at ${JUCE_ROOT}. Run 'make deps' first.")
|
||||
endif()
|
||||
|
||||
add_subdirectory("${JUCE_ROOT}" juce_build)
|
||||
|
||||
if(APPLE)
|
||||
set(SM26_FORMATS AU VST3)
|
||||
elseif(UNIX)
|
||||
set(SM26_FORMATS VST3)
|
||||
else()
|
||||
set(SM26_FORMATS VST3)
|
||||
endif()
|
||||
|
||||
juce_add_plugin(SM-26
|
||||
VENDOR_NAME "SM"
|
||||
VENDOR_WEBSITE "https://example.org/sm-26"
|
||||
VENDOR_EMAIL "dev@example.org"
|
||||
VENDOR_VERSION_CODE 0x0100
|
||||
PLUGIN_MANUFACTURER_CODE "SM26"
|
||||
PLUGIN_CODE "SM26"
|
||||
PRODUCT_NAME "SM-26"
|
||||
PRODUCT_DESCRIPTION "SM-26 synthesizer"
|
||||
VERSION "1.0.0"
|
||||
IS_SYNTH TRUE
|
||||
NEEDS_MIDI_INPUT TRUE
|
||||
NEEDS_MIDI_OUTPUT FALSE
|
||||
IS_MIDI_EFFECT FALSE
|
||||
EDITOR_NAME "SM26AudioProcessorEditor"
|
||||
EDITOR_NEEDS_KEYBOARD_FOCUS TRUE
|
||||
FORMATS ${SM26_FORMATS}
|
||||
)
|
||||
|
||||
target_sources(SM-26 PRIVATE
|
||||
Source/PluginProcessor.cpp
|
||||
Source/PluginEditor.cpp
|
||||
Source/SynthParams.cpp
|
||||
Source/SynthEngine.cpp
|
||||
Source/Presets.cpp
|
||||
Source/LcdDisplay.cpp
|
||||
Source/MenuSystem.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SM-26 PRIVATE Source)
|
||||
|
||||
target_compile_definitions(SM-26 PRIVATE
|
||||
JUCE_WEB_BROWSER=0
|
||||
JUCE_USE_CURL=0
|
||||
JUCE_ENABLE_GUI_TELNET_CONSOLE=0
|
||||
JUCE_VST3_CAN_REPLACE_VST2=0
|
||||
JUCE_MODAL_LOOPS_PERMITTED=1
|
||||
)
|
||||
|
||||
juce_generate_juce_header(SM-26)
|
||||
|
||||
add_custom_target(gen_buildinfo ALL
|
||||
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_buildinfo.sh"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/gen/BuildInfo_generated.h"
|
||||
COMMENT "Generating build info header (git version + build time)"
|
||||
)
|
||||
add_dependencies(SM-26 gen_buildinfo)
|
||||
|
||||
target_include_directories(SM-26 PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/gen")
|
||||
|
||||
set_target_properties(SM-26 PROPERTIES OUTPUT_NAME "SM-26")
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(SM-26 PROPERTIES
|
||||
MICROPHONE_PERMISSION_ENABLED FALSE
|
||||
WEBKIT_PERMISSION_ENABLED FALSE
|
||||
)
|
||||
endif()
|
||||
100
Makefile
Normal file
100
Makefile
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
SHELL := /bin/sh
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
JUCE_ROOT ?= deps/juce
|
||||
BUILD_DIR ?= build
|
||||
CONFIG := Release
|
||||
JOBS := $(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
|
||||
|
||||
BUNDLE := SM-26
|
||||
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
OSX_ARCH := arm64
|
||||
FOLLOW_BRANCH := 1
|
||||
endif
|
||||
|
||||
.PHONY: all deps configure build install uninstall clean distclean
|
||||
|
||||
all: build
|
||||
|
||||
deps:
|
||||
@if [ ! -f "$(JUCE_ROOT)/CMakeLists.txt" ]; then \
|
||||
echo ">> Cloning JUCE into $(JUCE_ROOT) ..."; \
|
||||
mkdir -p "$(JUCE_ROOT)"; \
|
||||
git clone --depth 1 https://github.com/juce-framework/JUCE.git "$(JUCE_ROOT)"; \
|
||||
fi
|
||||
@echo ">> JUCE present."
|
||||
|
||||
configure: deps
|
||||
@mkdir -p "$(BUILD_DIR)"
|
||||
@if [ "$(UNAME_S)" = "Darwin" ]; then \
|
||||
cmake -S . -B "$(BUILD_DIR)" \
|
||||
-DCMAKE_BUILD_TYPE="$(CONFIG)" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="$(OSX_ARCH)" \
|
||||
-DJUCE_ROOT="$(abspath $(JUCE_ROOT))"; \
|
||||
else \
|
||||
cmake -S . -B "$(BUILD_DIR)" \
|
||||
-DCMAKE_BUILD_TYPE="$(CONFIG)" \
|
||||
-DJUCE_ROOT="$(abspath $(JUCE_ROOT))"; \
|
||||
fi
|
||||
|
||||
build: configure
|
||||
cmake --build "$(BUILD_DIR)" --config "$(CONFIG)" -j "$(JOBS)"
|
||||
|
||||
# After install the plugin gets ad-hoc code-signed (required for AU hosts and
|
||||
# VST3 sandboxing on macOS) and any quarantine xattr is stripped.
|
||||
DEST_VST3 ?=
|
||||
DEST_AU ?=
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
DEST_VST3 := "$(HOME)/Library/Audio/Plug-Ins/VST3/$(BUNDLE).vst3"
|
||||
DEST_AU := "$(HOME)/Library/Audio/Plug-Ins/Components/$(BUNDLE).component"
|
||||
else ifeq ($(UNAME_S),Linux)
|
||||
DEST_VST3 := "$(HOME)/.vst3/$(BUNDLE).vst3"
|
||||
endif
|
||||
|
||||
help_install_sources := $(shell find "$(BUILD_DIR)" -type d -name "*.vst3" 2>/dev/null; find "$(BUILD_DIR)" -type d -name "*.component" 2>/dev/null)
|
||||
|
||||
install: build
|
||||
@if [ "$(UNAME_S)" = "Darwin" ]; then \
|
||||
mkdir -p "$(HOME)/Library/Audio/Plug-Ins/VST3" "$(HOME)/Library/Audio/Plug-Ins/Components"; \
|
||||
VST3=$$(find "$(BUILD_DIR)" -type d -name "$(BUNDLE).vst3" | head -n1); \
|
||||
AU=$$(find "$(BUILD_DIR)" -type d -name "$(BUNDLE).component" | head -n1); \
|
||||
if [ -n "$$VST3" ]; then \
|
||||
echo ">> Installing VST3"; \
|
||||
rm -rf "$(DEST_VST3)"; cp -R "$$VST3" "$(HOME)/Library/Audio/Plug-Ins/VST3/"; \
|
||||
codesign --force --deep --sign - "$(DEST_VST3)" && echo ">> Signed VST3 (ad-hoc)"; \
|
||||
codesign --verify --deep "$(DEST_VST3)" 2>/dev/null && echo ">> VST3 signature verified"; \
|
||||
xattr -dr com.apple.quarantine "$(DEST_VST3)" 2>/dev/null || true; \
|
||||
echo ">> Quarantine removed."; \
|
||||
fi; \
|
||||
if [ -n "$$AU" ]; then \
|
||||
echo ">> Installing AU"; \
|
||||
rm -rf "$(DEST_AU)"; cp -R "$$AU" "$(HOME)/Library/Audio/Plug-Ins/Components/"; \
|
||||
codesign --force --deep --sign - "$(DEST_AU)" && echo ">> Signed AU (ad-hoc)"; \
|
||||
codesign --verify --deep "$(DEST_AU)" 2>/dev/null && echo ">> AU signature verified"; \
|
||||
xattr -dr com.apple.quarantine "$(DEST_AU)" 2>/dev/null || true; \
|
||||
echo ">> Quarantine removed."; \
|
||||
fi; \
|
||||
elif [ "$(UNAME_S)" = "Linux" ]; then \
|
||||
echo ">> Installing VST3"; \
|
||||
mkdir -p "$(HOME)/.vst3"; \
|
||||
VST3=$$(find "$(BUILD_DIR)" -type d -name "$(BUNDLE).vst3" | head -n1); \
|
||||
if [ -n "$$VST3" ]; then rm -rf "$(DEST_VST3)"; cp -R "$$VST3" "$(HOME)/.vst3/"; else echo "!! VST3 not found in build dir"; exit 1; fi; \
|
||||
fi
|
||||
@echo ">> SM-26 installed."
|
||||
|
||||
uninstall:
|
||||
@if [ "$(UNAME_S)" = "Darwin" ]; then \
|
||||
rm -rf "$(DEST_VST3)" "$(DEST_AU)"; \
|
||||
echo ">> Removed $(BUNDLE) AU + VST3"; \
|
||||
elif [ "$(UNAME_S)" = "Linux" ]; then \
|
||||
rm -rf "$(DEST_VST3)"; \
|
||||
echo ">> Removed $(BUNDLE) VST3"; \
|
||||
fi
|
||||
|
||||
clean:
|
||||
rm -rf "$(BUILD_DIR)"/SM-26_artefacts
|
||||
|
||||
distclean:
|
||||
rm -rf "$(BUILD_DIR)"
|
||||
155
Source/LcdDisplay.cpp
Normal file
155
Source/LcdDisplay.cpp
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
#include "LcdDisplay.h"
|
||||
|
||||
LcdDisplay::LcdDisplay()
|
||||
{
|
||||
clear();
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
juce::Font LcdDisplay::getMonoFont() const
|
||||
{
|
||||
return juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(),
|
||||
(float) kCharH - 1.0f, juce::Font::plain });
|
||||
}
|
||||
|
||||
void LcdDisplay::rasterize (juce::uint32 c)
|
||||
{
|
||||
if (c >= 256 || glyphCache[c].cached) return;
|
||||
|
||||
juce::Image img (juce::Image::ARGB, kCharW * 2, kCharH * 2, true);
|
||||
{
|
||||
juce::Graphics g (img);
|
||||
g.setColour (juce::Colours::white);
|
||||
g.setFont (getMonoFont());
|
||||
juce::String s;
|
||||
s << (juce_wchar) c;
|
||||
g.drawText (s, img.getBounds().removeFromBottom (kCharH * 2 - 2),
|
||||
juce::Justification::centredBottom, false);
|
||||
}
|
||||
|
||||
const float sx = (float) img.getWidth() / (float) kCharW;
|
||||
const float sy = (float) img.getHeight() / (float) kCharH;
|
||||
|
||||
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), img.getWidth() - 1);
|
||||
int py1 = juce::jmin ((int) ((row + 1) * sy), img.getHeight() - 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)
|
||||
lines[row] = s;
|
||||
}
|
||||
|
||||
void LcdDisplay::showCursor (bool show) { cursorVisible = show; }
|
||||
|
||||
void LcdDisplay::clear()
|
||||
{
|
||||
for (int r = 0; r < kRows; ++r)
|
||||
lines[r] = "";
|
||||
cursorCol = 0;
|
||||
cursorRow = 0;
|
||||
cursorVisible = false;
|
||||
}
|
||||
|
||||
void LcdDisplay::paint (juce::Graphics& g)
|
||||
{
|
||||
const float cw = (float) kCharW;
|
||||
const float ch = (float) kCharH;
|
||||
const float w = (float) getWidth();
|
||||
const float h = (float) getHeight();
|
||||
|
||||
const juce::Colour bgCol (0xFF0D1A05);
|
||||
const juce::Colour gridCol (0xFF15260A);
|
||||
const juce::Colour pixOn (0xFF4AE038);
|
||||
const juce::Colour pixOnDim (0xFF1E3A12);
|
||||
const juce::Colour pixOff (0xFF0F1C07);
|
||||
|
||||
g.fillAll (bgCol);
|
||||
|
||||
g.setColour (gridCol);
|
||||
float totalW = (float) kCols * cw;
|
||||
float totalH = (float) kRows * ch;
|
||||
float ox = (w - totalW) / 2.0f;
|
||||
float oy = (h - totalH) / 2.0f;
|
||||
g.fillRect (ox - 1.0f, oy - 1.0f, totalW + 2.0f, totalH + 2.0f);
|
||||
|
||||
bool blinkOn = cursorVisible &&
|
||||
((int) (juce::Time::getMillisecondCounterHiRes() / 400.0) % 2 == 0);
|
||||
|
||||
for (int row = 0; row < kRows; ++row)
|
||||
{
|
||||
const juce::String& line = lines[row];
|
||||
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);
|
||||
|
||||
float cx = ox + (float) col * cw;
|
||||
float cy = oy + (float) row * ch;
|
||||
|
||||
g.setColour (pixOff);
|
||||
g.fillRect (cx + 0.5f, cy + 0.5f, cw - 1.0f, ch - 1.0f);
|
||||
|
||||
for (int gRow = 0; gRow < 8; ++gRow)
|
||||
{
|
||||
uint8_t bits = glyph[gRow];
|
||||
if (bits == 0) continue;
|
||||
for (int gCol = 0; gCol < 5; ++gCol)
|
||||
{
|
||||
if (bits & (1u << (4 - gCol)))
|
||||
{
|
||||
float px = cx + 1.0f + (float) gCol * (cw - 2.0f) / 5.0f;
|
||||
float py = cy + 1.0f + (float) gRow * (ch - 2.0f) / 9.0f;
|
||||
float pw = (cw - 2.0f) / 5.0f - 0.5f;
|
||||
float ph = (ch - 2.0f) / 9.0f - 0.3f;
|
||||
|
||||
g.setColour (pixOnDim);
|
||||
g.fillRect (px - 0.4f, py - 0.3f, pw + 0.8f, ph + 0.6f);
|
||||
g.setColour (pixOn);
|
||||
g.fillRect (px, py, pw, ph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blinkOn && col == cursorCol && row == cursorRow)
|
||||
{
|
||||
g.setColour (pixOn);
|
||||
g.fillRect (cx + 1.0f, cy + ch - 3.0f, cw - 2.0f, 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LcdDisplay::timerCallback() { repaint(); }
|
||||
42
Source/LcdDisplay.h
Normal file
42
Source/LcdDisplay.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class LcdDisplay : public juce::Component,
|
||||
private juce::Timer
|
||||
{
|
||||
public:
|
||||
static constexpr int kCols = 80;
|
||||
static constexpr int kRows = 4;
|
||||
|
||||
LcdDisplay();
|
||||
|
||||
void setLine (int row, const juce::String& s);
|
||||
void showCursor (bool show);
|
||||
void clear();
|
||||
void paint (juce::Graphics&) override;
|
||||
|
||||
private:
|
||||
void timerCallback() override;
|
||||
|
||||
static constexpr int kCharW = 6;
|
||||
static constexpr int kCharH = 9;
|
||||
|
||||
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;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LcdDisplay)
|
||||
};
|
||||
124
Source/MenuSystem.cpp
Normal file
124
Source/MenuSystem.cpp
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#include "MenuSystem.h"
|
||||
|
||||
MenuSystem::MenuSystem() {}
|
||||
|
||||
void MenuSystem::setRootScreenBuilder (ScreenBuilder fn) { rootBuilder = fn; }
|
||||
|
||||
void MenuSystem::rebuildRoot()
|
||||
{
|
||||
if (rootBuilder)
|
||||
{
|
||||
MenuScreen root = rootBuilder();
|
||||
stack.clear();
|
||||
stack.push_back (root);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuSystem::open()
|
||||
{
|
||||
wasOpen = true;
|
||||
rebuildRoot();
|
||||
}
|
||||
|
||||
void MenuSystem::close()
|
||||
{
|
||||
stack.clear();
|
||||
wasOpen = false;
|
||||
}
|
||||
|
||||
void MenuSystem::pushScreen (const MenuScreen& screen) { stack.push_back (screen); }
|
||||
|
||||
const MenuScreen& MenuSystem::current() const
|
||||
{
|
||||
if (stack.empty())
|
||||
{
|
||||
static MenuScreen empty;
|
||||
empty.title = "";
|
||||
empty.selected = 0;
|
||||
return empty;
|
||||
}
|
||||
return stack.back();
|
||||
}
|
||||
|
||||
void MenuSystem::refreshCurrent()
|
||||
{
|
||||
if (! stack.empty())
|
||||
{
|
||||
int sel = stack.back().selected;
|
||||
stack.pop_back();
|
||||
if (stack.empty() && rootBuilder)
|
||||
{
|
||||
MenuScreen root = rootBuilder();
|
||||
root.selected = sel;
|
||||
stack.push_back (root);
|
||||
}
|
||||
else if (! stack.empty())
|
||||
{
|
||||
stack.back().selected = sel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuSystem::up()
|
||||
{
|
||||
if (stack.empty()) return;
|
||||
auto& s = stack.back();
|
||||
if (s.items.empty()) return;
|
||||
|
||||
int newSel = s.selected - 1;
|
||||
while (newSel >= 0 && s.items[(size_t) newSel].kind == MenuItem::Separator)
|
||||
--newSel;
|
||||
if (newSel >= 0)
|
||||
s.selected = newSel;
|
||||
}
|
||||
|
||||
void MenuSystem::down()
|
||||
{
|
||||
if (stack.empty()) return;
|
||||
auto& s = stack.back();
|
||||
if (s.items.empty()) return;
|
||||
|
||||
int newSel = s.selected + 1;
|
||||
while (newSel < (int) s.items.size() && s.items[(size_t) newSel].kind == MenuItem::Separator)
|
||||
++newSel;
|
||||
if (newSel < (int) s.items.size())
|
||||
s.selected = newSel;
|
||||
}
|
||||
|
||||
void MenuSystem::left()
|
||||
{
|
||||
if (stack.empty()) return;
|
||||
auto& s = stack.back();
|
||||
if (s.items.empty()) return;
|
||||
auto& item = s.items[(size_t) s.selected];
|
||||
if (item.kind == MenuItem::Value && item.onAdjust)
|
||||
item.onAdjust (-0.02f);
|
||||
}
|
||||
|
||||
void MenuSystem::right()
|
||||
{
|
||||
if (stack.empty()) return;
|
||||
auto& s = stack.back();
|
||||
if (s.items.empty()) return;
|
||||
auto& item = s.items[(size_t) s.selected];
|
||||
if (item.kind == MenuItem::Value && item.onAdjust)
|
||||
item.onAdjust (0.02f);
|
||||
}
|
||||
|
||||
void MenuSystem::enter()
|
||||
{
|
||||
if (stack.empty()) return;
|
||||
auto& s = stack.back();
|
||||
if (s.items.empty()) return;
|
||||
auto& item = s.items[(size_t) s.selected];
|
||||
if (item.onEnter)
|
||||
item.onEnter();
|
||||
}
|
||||
|
||||
void MenuSystem::escape()
|
||||
{
|
||||
if (stack.size() > 1)
|
||||
stack.pop_back();
|
||||
else if (stack.size() == 1)
|
||||
close();
|
||||
}
|
||||
66
Source/MenuSystem.h
Normal file
66
Source/MenuSystem.h
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "SynthParams.h"
|
||||
|
||||
class MenuSystem;
|
||||
|
||||
struct MenuItem
|
||||
{
|
||||
enum Kind { Action, Value, Submenu, Separator };
|
||||
|
||||
juce::String label;
|
||||
Kind kind = Action;
|
||||
int paramId = -1;
|
||||
|
||||
std::function<void()> onEnter;
|
||||
std::function<void(float delta)> onAdjust;
|
||||
std::function<juce::String()> valueText;
|
||||
|
||||
MenuItem& action (std::function<void()> fn) { onEnter = fn; return *this; }
|
||||
MenuItem& param (int id, float lo, float hi) { paramId = id; return *this; }
|
||||
MenuItem& value (std::function<juce::String()> fn) { valueText = fn; kind = Value; return *this; }
|
||||
MenuItem& submenu() { kind = Submenu; return *this; }
|
||||
MenuItem& sep() { kind = Separator; return *this; }
|
||||
};
|
||||
|
||||
struct MenuScreen
|
||||
{
|
||||
juce::String title;
|
||||
std::vector<MenuItem> items;
|
||||
int selected = 0;
|
||||
};
|
||||
|
||||
class MenuSystem
|
||||
{
|
||||
public:
|
||||
MenuSystem();
|
||||
|
||||
void open();
|
||||
void close();
|
||||
bool isOpen() const { return ! stack.empty(); }
|
||||
|
||||
void up();
|
||||
void down();
|
||||
void left();
|
||||
void right();
|
||||
void enter();
|
||||
void escape();
|
||||
|
||||
const MenuScreen& current() const;
|
||||
|
||||
void pushScreen (const MenuScreen& screen);
|
||||
|
||||
using ScreenBuilder = std::function<MenuScreen()>;
|
||||
|
||||
void setRootScreenBuilder (ScreenBuilder fn);
|
||||
|
||||
void refreshCurrent();
|
||||
|
||||
private:
|
||||
std::vector<MenuScreen> stack;
|
||||
ScreenBuilder rootBuilder;
|
||||
bool wasOpen = false;
|
||||
|
||||
void rebuildRoot();
|
||||
};
|
||||
588
Source/PluginEditor.cpp
Normal file
588
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,588 @@
|
|||
#include "PluginEditor.h"
|
||||
#include "BuildInfo_generated.h"
|
||||
|
||||
static juce::String padRight (const juce::String& s, int width)
|
||||
{
|
||||
if (s.length() >= width) return s.substring (0, width);
|
||||
return s + juce::String::repeatedString (" ", width - s.length());
|
||||
}
|
||||
|
||||
SM26AudioProcessorEditor::SM26AudioProcessorEditor (SM26AudioProcessor& p)
|
||||
: AudioProcessorEditor (p), proc (p)
|
||||
{
|
||||
setSize (920, 540);
|
||||
setResizable (true, false);
|
||||
setResizeLimits (720, 400, 1400, 900);
|
||||
|
||||
addAndMakeVisible (lcd);
|
||||
setupKnobs();
|
||||
setupNavButtons();
|
||||
|
||||
footer.setText (buildGitFooter(), juce::dontSendNotification);
|
||||
footer.setJustificationType (juce::Justification::centredLeft);
|
||||
footer.setColour (juce::Label::textColourId, juce::Colour (0xFF90B080));
|
||||
footer.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 11.0f, juce::Font::plain }));
|
||||
addAndMakeVisible (footer);
|
||||
|
||||
buildRootMenu();
|
||||
startTimerHz (20);
|
||||
}
|
||||
|
||||
SM26AudioProcessorEditor::~SM26AudioProcessorEditor()
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
juce::String SM26AudioProcessorEditor::buildGitFooter()
|
||||
{
|
||||
return juce::String ("SM-26 v") + SM26_GIT_VERSION
|
||||
+ " | branch " + SM26_GIT_BRANCH
|
||||
+ " | built " + SM26_BUILD_TIME
|
||||
+ " | arm64";
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::setupKnobs()
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
knobSliders[i].setSliderStyle (juce::Slider::RotaryVerticalDrag);
|
||||
knobSliders[i].setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
||||
knobSliders[i].setRange (0.0, 1.0, 0.001);
|
||||
knobSliders[i].setColour (juce::Slider::rotarySliderFillColourId, juce::Colour (0xFF3A8A28));
|
||||
knobSliders[i].setColour (juce::Slider::rotarySliderOutlineColourId, juce::Colour (0xFF1A2E0A));
|
||||
knobSliders[i].setColour (juce::Slider::thumbColourId, juce::Colour (0xFFE0E0C0));
|
||||
knobSliders[i].addListener (this);
|
||||
addAndMakeVisible (knobSliders[i]);
|
||||
|
||||
knobLabels[i].setJustificationType (juce::Justification::centred);
|
||||
knobLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFF90B080));
|
||||
knobLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 11.0f, juce::Font::plain }));
|
||||
addAndMakeVisible (knobLabels[i]);
|
||||
|
||||
knobValueLabels[i].setJustificationType (juce::Justification::centred);
|
||||
knobValueLabels[i].setColour (juce::Label::textColourId, juce::Colour (0xFFC0E0A0));
|
||||
knobValueLabels[i].setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 12.0f, juce::Font::bold }));
|
||||
addAndMakeVisible (knobValueLabels[i]);
|
||||
|
||||
knobSliders[i].setValue (0.5, juce::dontSendNotification);
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::setupNavButtons()
|
||||
{
|
||||
auto setup = [this](juce::TextButton& btn, juce::Colour bg)
|
||||
{
|
||||
btn.setColour (juce::TextButton::buttonColourId, bg);
|
||||
btn.setColour (juce::TextButton::textColourOffId, juce::Colour (0xFFE0E0C0));
|
||||
btn.setColour (juce::ComboBox::outlineColourId, juce::Colour (0xFF2A4A18));
|
||||
addAndMakeVisible (btn);
|
||||
};
|
||||
|
||||
setup (navUp, juce::Colour (0xFF2A4A18));
|
||||
setup (navDown, juce::Colour (0xFF2A4A18));
|
||||
setup (navLeft, juce::Colour (0xFF2A4A18));
|
||||
setup (navRight, juce::Colour (0xFF2A4A18));
|
||||
setup (navEnter, juce::Colour (0xFF3A6A28));
|
||||
setup (navEsc, juce::Colour (0xFF3A2A18));
|
||||
|
||||
navUp.onClick = [this] { menu.up(); };
|
||||
navDown.onClick = [this] { menu.down(); };
|
||||
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::buildRootMenu()
|
||||
{
|
||||
auto builder = [this]() -> MenuScreen
|
||||
{
|
||||
MenuScreen s;
|
||||
s.title = "SM-26 MAIN MENU";
|
||||
s.selected = 0;
|
||||
|
||||
auto addPreset = [&](const char* name, int idx) -> MenuItem
|
||||
{
|
||||
MenuItem item;
|
||||
item.label = name;
|
||||
item.kind = MenuItem::Action;
|
||||
item.onEnter = [this, idx]() {
|
||||
proc.setPresetByIndex (idx);
|
||||
};
|
||||
return item;
|
||||
};
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "PRESETS (" + juce::String (sm26::PresetStore::count()) + ")";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this, addPreset]() {
|
||||
MenuScreen ps;
|
||||
ps.title = "SELECT PRESET";
|
||||
ps.selected = proc.engine.presetIndex();
|
||||
for (int i = 0; i < sm26::PresetStore::count(); ++i)
|
||||
ps.items.push_back (addPreset (
|
||||
sm26::PresetStore::get(i)->name, i));
|
||||
menu.pushScreen (ps);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
s.items.push_back (MenuItem().sep());
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "OSCILLATORS";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this]() {
|
||||
MenuScreen os;
|
||||
os.title = "OSCILLATORS";
|
||||
os.selected = 0;
|
||||
|
||||
auto addItem = [&](const char* n, int pid) {
|
||||
MenuItem mi;
|
||||
mi.label = n;
|
||||
mi.kind = MenuItem::Value;
|
||||
mi.paramId = pid;
|
||||
mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); };
|
||||
mi.onAdjust = [this, pid](float d) {
|
||||
float cur = proc.engine.getParam (pid);
|
||||
proc.engine.setParam (pid, sm26::clamp01 (cur + d));
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = proc.engine.knobView (k);
|
||||
if (kv.param == pid)
|
||||
proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f)));
|
||||
}
|
||||
};
|
||||
os.items.push_back (mi);
|
||||
};
|
||||
|
||||
addItem ("OSC1 WAVE", sm26::PO1WAVE);
|
||||
addItem ("OSC2 WAVE", sm26::PO2WAVE);
|
||||
addItem ("O1 OCTAVE", sm26::PO1OCT);
|
||||
addItem ("O1 DETUNE", sm26::PO1CT);
|
||||
addItem ("O2 OCTAVE", sm26::PO2OCT);
|
||||
addItem ("O2 DETUNE", sm26::PO2CT);
|
||||
addItem ("MIX", sm26::PMIX);
|
||||
addItem ("NOISE", sm26::PNOISE);
|
||||
menu.pushScreen (os);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "FILTER";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this]() {
|
||||
MenuScreen fs;
|
||||
fs.title = "FILTER";
|
||||
fs.selected = 0;
|
||||
|
||||
auto addItem = [&](const char* n, int pid) {
|
||||
MenuItem mi;
|
||||
mi.label = n;
|
||||
mi.kind = MenuItem::Value;
|
||||
mi.paramId = pid;
|
||||
mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); };
|
||||
mi.onAdjust = [this, pid](float d) {
|
||||
float cur = proc.engine.getParam (pid);
|
||||
proc.engine.setParam (pid, sm26::clamp01 (cur + d));
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = proc.engine.knobView (k);
|
||||
if (kv.param == pid)
|
||||
proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f)));
|
||||
}
|
||||
};
|
||||
fs.items.push_back (mi);
|
||||
};
|
||||
|
||||
addItem ("CUTOFF", sm26::PCUTOFF);
|
||||
addItem ("RES", sm26::PRES);
|
||||
addItem ("F-ENV", sm26::PFENV);
|
||||
addItem ("F-TYPE", sm26::PFTYPE);
|
||||
menu.pushScreen (fs);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "ENVELOPE";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this]() {
|
||||
MenuScreen es;
|
||||
es.title = "ENVELOPE";
|
||||
es.selected = 0;
|
||||
|
||||
auto addItem = [&](const char* n, int pid) {
|
||||
MenuItem mi;
|
||||
mi.label = n;
|
||||
mi.kind = MenuItem::Value;
|
||||
mi.paramId = pid;
|
||||
mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); };
|
||||
mi.onAdjust = [this, pid](float d) {
|
||||
float cur = proc.engine.getParam (pid);
|
||||
proc.engine.setParam (pid, sm26::clamp01 (cur + d));
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = proc.engine.knobView (k);
|
||||
if (kv.param == pid)
|
||||
proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f)));
|
||||
}
|
||||
};
|
||||
es.items.push_back (mi);
|
||||
};
|
||||
|
||||
addItem ("ATTACK", sm26::PATK);
|
||||
addItem ("DECAY", sm26::PDEC);
|
||||
addItem ("SUSTAIN", sm26::PSUS);
|
||||
addItem ("RELEASE", sm26::PREL);
|
||||
menu.pushScreen (es);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "DELAY";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this]() {
|
||||
MenuScreen ds;
|
||||
ds.title = "DELAY";
|
||||
ds.selected = 0;
|
||||
|
||||
auto addItem = [&](const char* n, int pid) {
|
||||
MenuItem mi;
|
||||
mi.label = n;
|
||||
mi.kind = MenuItem::Value;
|
||||
mi.paramId = pid;
|
||||
mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); };
|
||||
mi.onAdjust = [this, pid](float d) {
|
||||
float cur = proc.engine.getParam (pid);
|
||||
proc.engine.setParam (pid, sm26::clamp01 (cur + d));
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = proc.engine.knobView (k);
|
||||
if (kv.param == pid)
|
||||
proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f)));
|
||||
}
|
||||
};
|
||||
ds.items.push_back (mi);
|
||||
};
|
||||
|
||||
addItem ("TIME/STEPS", sm26::PDTIME);
|
||||
addItem ("STEPS", sm26::PDSTEPS);
|
||||
addItem ("SYNC", sm26::PDSYNC);
|
||||
addItem ("FEEDBACK", sm26::PDFB);
|
||||
addItem ("MIX", sm26::PDMIX);
|
||||
menu.pushScreen (ds);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
{
|
||||
MenuItem m;
|
||||
m.label = "SYSTEM";
|
||||
m.kind = MenuItem::Submenu;
|
||||
m.onEnter = [this]() {
|
||||
MenuScreen ss;
|
||||
ss.title = "SYSTEM";
|
||||
ss.selected = 0;
|
||||
|
||||
auto addItem = [&](const char* n, int pid) {
|
||||
MenuItem mi;
|
||||
mi.label = n;
|
||||
mi.kind = MenuItem::Value;
|
||||
mi.paramId = pid;
|
||||
mi.valueText = [this, pid]() { return sm26::formatParamReal (pid, proc.engine.getParam (pid)); };
|
||||
mi.onAdjust = [this, pid](float d) {
|
||||
float cur = proc.engine.getParam (pid);
|
||||
proc.engine.setParam (pid, sm26::clamp01 (cur + d));
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = proc.engine.knobView (k);
|
||||
if (kv.param == pid)
|
||||
proc.setKnob (k, sm26::clamp01 ((proc.engine.getParam (pid) - kv.lo) / (kv.hi - kv.lo + 1e-9f)));
|
||||
}
|
||||
};
|
||||
ss.items.push_back (mi);
|
||||
};
|
||||
|
||||
addItem ("DRIVE", sm26::PDRIVE);
|
||||
addItem ("GLIDE", sm26::PGLIDE);
|
||||
addItem ("MASTER", sm26::PMASTER);
|
||||
|
||||
{
|
||||
MenuItem mi; mi.label = "---"; mi.kind = MenuItem::Separator;
|
||||
ss.items.push_back (mi);
|
||||
}
|
||||
|
||||
{
|
||||
MenuItem mi;
|
||||
mi.label = "ABOUT";
|
||||
mi.kind = MenuItem::Action;
|
||||
mi.onEnter = [this]() {
|
||||
MenuScreen as;
|
||||
as.title = "ABOUT";
|
||||
as.selected = 0;
|
||||
{
|
||||
MenuItem m2; m2.label = "SM-26 Synthesizer"; m2.kind = MenuItem::Action;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
{
|
||||
MenuItem m2;
|
||||
m2.label = "Version: " + juce::String (SM26_GIT_VERSION);
|
||||
m2.kind = MenuItem::Action;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
{
|
||||
MenuItem m2;
|
||||
m2.label = "Built: " + juce::String (SM26_BUILD_TIME);
|
||||
m2.kind = MenuItem::Action;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
{
|
||||
MenuItem m2;
|
||||
m2.label = "Branch: " + juce::String (SM26_GIT_BRANCH);
|
||||
m2.kind = MenuItem::Action;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
{
|
||||
MenuItem m2; m2.label = ""; m2.kind = MenuItem::Separator;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
{
|
||||
MenuItem m2;
|
||||
m2.label = "48 presets | VST3 + AU";
|
||||
m2.kind = MenuItem::Action;
|
||||
as.items.push_back (m2);
|
||||
}
|
||||
menu.pushScreen (as);
|
||||
};
|
||||
ss.items.push_back (mi);
|
||||
}
|
||||
|
||||
menu.pushScreen (ss);
|
||||
};
|
||||
s.items.push_back (m);
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
menu.setRootScreenBuilder (builder);
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (juce::Colour (0xFF0A0F06));
|
||||
|
||||
const float totalW = (float) getWidth();
|
||||
const float totalH = (float) getHeight();
|
||||
|
||||
g.setColour (juce::Colour (0xFF1A2E0A));
|
||||
g.fillRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f);
|
||||
|
||||
g.setColour (juce::Colour (0xFF2A4A18));
|
||||
g.drawRoundedRectangle (8.0f, 8.0f, totalW - 16.0f, totalH - 16.0f, 6.0f, 1.5f);
|
||||
|
||||
if (! menu.isOpen())
|
||||
{
|
||||
g.setColour (juce::Colour (0xFF6AE050));
|
||||
g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 14.0f, juce::Font::bold }));
|
||||
g.drawText ("SM-26", 20.0f, 16.0f, 100.0f, 22.0f, juce::Justification::centredLeft);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setColour (juce::Colour (0xFFE0E0C0));
|
||||
g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 14.0f, juce::Font::bold }));
|
||||
g.drawText (menu.current().title, 20.0f, 16.0f, getWidth() - 40.0f, 22.0f,
|
||||
juce::Justification::centred);
|
||||
}
|
||||
|
||||
if (menu.isOpen())
|
||||
{
|
||||
const auto& ms = menu.current();
|
||||
float y = 44.0f;
|
||||
float itemH = 18.0f;
|
||||
float menuLeft = 20.0f;
|
||||
float menuW = getWidth() - 40.0f;
|
||||
|
||||
for (int i = 0; i < (int) ms.items.size() && y < (float) getHeight() - 60.0f; ++i)
|
||||
{
|
||||
const auto& item = ms.items[i];
|
||||
|
||||
if (item.kind == MenuItem::Separator)
|
||||
{
|
||||
g.setColour (juce::Colour (0xFF2A4A18));
|
||||
g.drawHorizontalLine ((int) y, menuLeft, menuLeft + menuW);
|
||||
y += 4.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool selected = (i == ms.selected);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
g.setColour (juce::Colour (0xFF3A6A28));
|
||||
g.fillRoundedRectangle (menuLeft, y, menuW, itemH, 3.0f);
|
||||
}
|
||||
|
||||
g.setColour (selected ? juce::Colour (0xFFE0E0C0) : juce::Colour (0xFF709060));
|
||||
g.setFont (juce::Font (juce::FontOptions { juce::Font::getDefaultMonospacedFontName(), 12.0f, juce::Font::plain }));
|
||||
g.drawText (item.label, menuLeft + 8.0f, y, menuW * 0.6f, itemH,
|
||||
juce::Justification::centredLeft);
|
||||
|
||||
if (item.kind == MenuItem::Value && item.valueText)
|
||||
{
|
||||
g.setColour (juce::Colour (0xFFA0E080));
|
||||
g.drawText (item.valueText(), menuLeft + menuW * 0.55f, y,
|
||||
menuW * 0.4f, itemH, juce::Justification::centredRight);
|
||||
}
|
||||
else if (item.kind == MenuItem::Submenu)
|
||||
{
|
||||
g.setColour (juce::Colour (0xFF90B080));
|
||||
g.drawText (">", menuLeft + menuW - 16.0f, y, 16.0f, itemH,
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
|
||||
y += itemH + 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::resized()
|
||||
{
|
||||
const float totalW = (float) getWidth();
|
||||
const float totalH = (float) getHeight();
|
||||
|
||||
float knobSize = 62.0f;
|
||||
float knobGap = 18.0f;
|
||||
float knobsW = 4.0f * knobSize + 3.0f * knobGap;
|
||||
float knobsX = (totalW - knobsW) / 2.0f;
|
||||
float knobsY = totalH - 135.0f;
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
float x = knobsX + (float) i * (knobSize + knobGap);
|
||||
knobSliders[i].setBounds ((int) x, (int) knobsY, (int) knobSize, (int) knobSize);
|
||||
knobLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 2), (int) knobSize, 16);
|
||||
knobValueLabels[i].setBounds ((int) x, (int) (knobsY + knobSize + 18), (int) knobSize, 14);
|
||||
}
|
||||
|
||||
float navSize = 36.0f;
|
||||
float navGap = 4.0f;
|
||||
float navCenterX = totalW / 2.0f;
|
||||
float navCenterY = totalH - 48.0f;
|
||||
|
||||
navUp.setBounds ((int) (navCenterX - navSize / 2), (int) (navCenterY - navSize - navGap),
|
||||
(int) navSize, (int) navSize);
|
||||
navDown.setBounds ((int) (navCenterX - navSize / 2), (int) (navCenterY + navGap),
|
||||
(int) navSize, (int) navSize);
|
||||
navLeft.setBounds ((int) (navCenterX - navSize - navGap - navSize / 2), (int) (navCenterY - navSize / 2),
|
||||
(int) navSize, (int) navSize);
|
||||
navRight.setBounds ((int) (navCenterX + navGap + navSize / 2), (int) (navCenterY - navSize / 2),
|
||||
(int) navSize, (int) navSize);
|
||||
|
||||
navEnter.setBounds ((int) (navCenterX - navSize - navGap), (int) (navCenterY + navSize + navGap),
|
||||
(int) navSize, (int) navSize);
|
||||
navEsc.setBounds ((int) (navCenterX + navGap), (int) (navCenterY + navSize + navGap),
|
||||
(int) navSize, (int) navSize);
|
||||
|
||||
lcd.setBounds (14, 36, (int) totalW - 28, (int) totalH - 190);
|
||||
|
||||
footer.setBounds (8, (int) totalH - 22, (int) totalW - 16, 16);
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::timerCallback()
|
||||
{
|
||||
updateLCD();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
auto kv = proc.engine.knobView (i);
|
||||
float val = proc.engine.getParam (kv.param);
|
||||
knobSliders[i].setValue (val, juce::dontSendNotification);
|
||||
knobLabels[i].setText (kv.name, juce::dontSendNotification);
|
||||
knobValueLabels[i].setText (sm26::formatParamReal (kv.param, val), juce::dontSendNotification);
|
||||
}
|
||||
|
||||
if (! menu.isOpen() && menuWasOpen)
|
||||
buildRootMenu();
|
||||
menuWasOpen = menu.isOpen();
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::sliderValueChanged (juce::Slider* s)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
if (s == &knobSliders[i])
|
||||
{
|
||||
float v = (float) s->getValue();
|
||||
auto kv = proc.engine.knobView (i);
|
||||
float paramVal = sm26::clamp01 (kv.lo + v * (kv.hi - kv.lo));
|
||||
proc.engine.setParam (kv.param, paramVal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessorEditor::updateLCD()
|
||||
{
|
||||
if (! menu.isOpen())
|
||||
{
|
||||
int presetIdx = proc.engine.presetIndex();
|
||||
const auto* p = sm26::PresetStore::findByIndex (presetIdx);
|
||||
|
||||
lcd.setLine (0, juce::String (presetIdx + 1) + "/" + juce::String (sm26::PresetStore::count())
|
||||
+ " " + sm26::PresetStore::fullLabel (*p));
|
||||
|
||||
lcd.setLine (1, padRight (proc.engine.knobView (0).name, 12)
|
||||
+ padRight (sm26::formatParamReal (proc.engine.knobView(0).param,
|
||||
proc.engine.getParam(proc.engine.knobView(0).param)), 12)
|
||||
+ " "
|
||||
+ padRight (proc.engine.knobView (2).name, 12)
|
||||
+ sm26::formatParamReal (proc.engine.knobView(2).param,
|
||||
proc.engine.getParam(proc.engine.knobView(2).param)));
|
||||
|
||||
lcd.setLine (2, padRight (proc.engine.knobView (1).name, 12)
|
||||
+ padRight (sm26::formatParamReal (proc.engine.knobView(1).param,
|
||||
proc.engine.getParam(proc.engine.knobView(1).param)), 12)
|
||||
+ " "
|
||||
+ padRight (proc.engine.knobView (3).name, 12)
|
||||
+ sm26::formatParamReal (proc.engine.knobView(3).param,
|
||||
proc.engine.getParam(proc.engine.knobView(3).param)));
|
||||
|
||||
double bpm = proc.engine.lastBpm();
|
||||
bool playing = proc.engine.transport();
|
||||
juce::String bpmStr = playing ? juce::String (bpm, 1) + " BPM" : "--- BPM";
|
||||
int notes = proc.engine.notesPlaying();
|
||||
juce::String notesStr = notes > 0 ? "NOTES:" + juce::String (notes) : "";
|
||||
|
||||
lcd.setLine (3, juce::String ("SM-26") + " " + bpmStr + " " + notesStr);
|
||||
lcd.showCursor (false);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& cs = menu.current();
|
||||
|
||||
lcd.setLine (0, "== " + cs.title + " ==");
|
||||
for (int row = 0; row < 3 && row < (int) cs.items.size(); ++row)
|
||||
{
|
||||
int idx = juce::jlimit (0, (int) cs.items.size() - 1, cs.selected + row);
|
||||
const auto& item = cs.items[idx];
|
||||
juce::String line = (idx == cs.selected ? "> " : " ") + item.label;
|
||||
if (item.kind == MenuItem::Value && item.valueText)
|
||||
line = padRight (line, 50) + item.valueText();
|
||||
else if (item.kind == MenuItem::Submenu)
|
||||
line = line + " [ENTER]";
|
||||
lcd.setLine (1 + row, line);
|
||||
}
|
||||
lcd.setLine (3, "UP/DOWN=SEL L/R=VAL ENTER=OK ESC=BACK");
|
||||
lcd.showCursor (false);
|
||||
}
|
||||
}
|
||||
53
Source/PluginEditor.h
Normal file
53
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "LcdDisplay.h"
|
||||
#include "MenuSystem.h"
|
||||
#include "Presets.h"
|
||||
#include "SynthParams.h"
|
||||
|
||||
class SM26AudioProcessorEditor : public juce::AudioProcessorEditor,
|
||||
private juce::Timer,
|
||||
private juce::Slider::Listener
|
||||
{
|
||||
public:
|
||||
SM26AudioProcessorEditor (SM26AudioProcessor&);
|
||||
~SM26AudioProcessorEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
private:
|
||||
void sliderValueChanged (juce::Slider*) override;
|
||||
|
||||
void setupKnobs();
|
||||
void setupNavButtons();
|
||||
void buildRootMenu();
|
||||
void updateLCD();
|
||||
|
||||
SM26AudioProcessor& proc;
|
||||
MenuSystem menu;
|
||||
|
||||
LcdDisplay lcd;
|
||||
|
||||
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 navEsc { "ESC" };
|
||||
|
||||
juce::Label footer;
|
||||
|
||||
bool menuWasOpen = false;
|
||||
|
||||
static juce::String buildGitFooter();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SM26AudioProcessorEditor)
|
||||
};
|
||||
163
Source/PluginProcessor.cpp
Normal file
163
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
#include "BuildInfo_generated.h"
|
||||
|
||||
SM26AudioProcessor::SM26AudioProcessor()
|
||||
: AudioProcessor (BusesProperties().withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts (*this, nullptr, "SM26State", {
|
||||
std::make_unique<juce::AudioParameterFloat> ("k1", "Knob 1", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
|
||||
std::make_unique<juce::AudioParameterFloat> ("k2", "Knob 2", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
|
||||
std::make_unique<juce::AudioParameterFloat> ("k3", "Knob 3", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
|
||||
std::make_unique<juce::AudioParameterFloat> ("k4", "Knob 4", juce::NormalisableRange<float>(0.f,1.f,0.001f), 0.5f),
|
||||
std::make_unique<juce::AudioParameterInt> ("preset", "Preset",
|
||||
0, sm26::PresetStore::count() - 1, 0)
|
||||
})
|
||||
{
|
||||
engine.setPresetIndex (0);
|
||||
|
||||
apvts.addParameterListener ("k1", this);
|
||||
apvts.addParameterListener ("k2", this);
|
||||
apvts.addParameterListener ("k3", this);
|
||||
apvts.addParameterListener ("k4", this);
|
||||
apvts.addParameterListener ("preset", this);
|
||||
paramsReady.store (true);
|
||||
}
|
||||
|
||||
SM26AudioProcessor::~SM26AudioProcessor()
|
||||
{
|
||||
apvts.removeParameterListener ("k1", this);
|
||||
apvts.removeParameterListener ("k2", this);
|
||||
apvts.removeParameterListener ("k3", this);
|
||||
apvts.removeParameterListener ("k4", this);
|
||||
apvts.removeParameterListener ("preset", this);
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
engine.prepare (sampleRate, samplesPerBlock);
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::releaseResources() {}
|
||||
|
||||
void SM26AudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
const int n = buffer.getNumSamples();
|
||||
|
||||
double bpm = 120.0;
|
||||
bool playing = false;
|
||||
if (auto* ph = getPlayHead())
|
||||
{
|
||||
auto pos = ph->getPosition();
|
||||
if (pos.hasValue())
|
||||
{
|
||||
if (pos->getBpm().hasValue())
|
||||
bpm = *pos->getBpm();
|
||||
playing = pos->getIsPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
for (int ch = 0; ch < buffer.getNumChannels(); ++ch)
|
||||
buffer.clear (ch, 0, n);
|
||||
|
||||
engine.process (buffer, midi, n, bpm, playing);
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* SM26AudioProcessor::createEditor()
|
||||
{
|
||||
return new SM26AudioProcessorEditor (*this);
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto xmlStr = apvts.copyState().createXml()->toString();
|
||||
destData.replaceAll (xmlStr.toRawUTF8(), (size_t) xmlStr.length());
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
auto xml = juce::XmlDocument::parse (juce::String::fromUTF8 ((const char*) data, sizeInBytes));
|
||||
if (xml != nullptr)
|
||||
{
|
||||
auto tree = juce::ValueTree::fromXml (*xml);
|
||||
if (tree.isValid())
|
||||
{
|
||||
paramsReady.store (false);
|
||||
apvts.replaceState (tree);
|
||||
paramsReady.store (true);
|
||||
int idx = (int) *apvts.getRawParameterValue ("preset");
|
||||
engine.setPresetIndex (idx);
|
||||
applyPresetKnobs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::setKnob (int knob, float value01)
|
||||
{
|
||||
if (knob < 0 || knob >= 4) return;
|
||||
auto* p = apvts.getParameter (juce::String ("k") + juce::String (knob + 1));
|
||||
if (p != nullptr)
|
||||
p->setValueNotifyingHost (p->convertTo0to1 (value01));
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::adjustParam (int paramId, float delta01)
|
||||
{
|
||||
float cur = engine.getParam (paramId);
|
||||
engine.setParam (paramId, sm26::clamp01 (cur + delta01));
|
||||
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = engine.knobView (k);
|
||||
if (kv.param == paramId)
|
||||
{
|
||||
float pval = engine.getParam (paramId);
|
||||
float knobVal = sm26::clamp01 ((pval - kv.lo) / (kv.hi - kv.lo + 1e-9f));
|
||||
setKnob (k, knobVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::setPresetByIndex (int i)
|
||||
{
|
||||
auto* p = apvts.getParameter ("preset");
|
||||
if (p != nullptr)
|
||||
p->setValueNotifyingHost (p->convertTo0to1 ((float) i));
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::parameterChanged (const juce::String& parameterID, float newValue)
|
||||
{
|
||||
if (! paramsReady.load()) return;
|
||||
|
||||
if (parameterID == "preset")
|
||||
{
|
||||
int idx = (int) std::round (newValue * (float) (sm26::PresetStore::count() - 1));
|
||||
idx = juce::jlimit (0, sm26::PresetStore::count() - 1, idx);
|
||||
engine.setPresetIndex (idx);
|
||||
applyPresetKnobs();
|
||||
}
|
||||
else if (parameterID == "k1" || parameterID == "k2" || parameterID == "k3" || parameterID == "k4")
|
||||
{
|
||||
int k = parameterID.getIntValue() - 1;
|
||||
auto kv = engine.knobView (k);
|
||||
|
||||
float paramVal = sm26::clamp01 (kv.lo + newValue * (kv.hi - kv.lo));
|
||||
engine.setParam (kv.param, paramVal);
|
||||
}
|
||||
}
|
||||
|
||||
void SM26AudioProcessor::applyPresetKnobs()
|
||||
{
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
auto kv = engine.knobView (k);
|
||||
float paramVal = engine.getParam (kv.param);
|
||||
float knobVal = sm26::clamp01 ((paramVal - kv.lo) / (kv.hi - kv.lo + 1e-9f));
|
||||
setKnob (k, knobVal);
|
||||
}
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new SM26AudioProcessor();
|
||||
}
|
||||
52
Source/PluginProcessor.h
Normal file
52
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "SynthEngine.h"
|
||||
#include "Presets.h"
|
||||
|
||||
class SM26AudioProcessor : public juce::AudioProcessor,
|
||||
public juce::AudioProcessorValueTreeState::Listener
|
||||
{
|
||||
public:
|
||||
SM26AudioProcessor();
|
||||
~SM26AudioProcessor() override;
|
||||
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
const juce::String getName() const override { return "SM-26"; }
|
||||
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const juce::String getProgramName (int) override { return {}; }
|
||||
void changeProgramName (int, const juce::String&) override {}
|
||||
|
||||
void getStateInformation (juce::MemoryBlock& destData) override;
|
||||
void setStateInformation (const void* data, int sizeInBytes) override;
|
||||
|
||||
void setKnob (int knob, float value01);
|
||||
void adjustParam (int paramId, float delta01);
|
||||
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
sm26::SynthEngine engine;
|
||||
|
||||
void setPresetByIndex (int i);
|
||||
int presetCount() const { return sm26::PresetStore::count(); }
|
||||
|
||||
private:
|
||||
void parameterChanged (const juce::String& parameterID, float newValue) override;
|
||||
void applyPresetKnobs();
|
||||
|
||||
std::atomic<bool> paramsReady{false};
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SM26AudioProcessor)
|
||||
};
|
||||
400
Source/Presets.cpp
Normal file
400
Source/Presets.cpp
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
#include "Presets.h"
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Catalog {
|
||||
Preset data[48];
|
||||
int n = 0;
|
||||
};
|
||||
|
||||
Catalog& catalog()
|
||||
{
|
||||
static Catalog cat;
|
||||
|
||||
if (cat.n == 0)
|
||||
{
|
||||
using P = std::pair<int, float>;
|
||||
using C = ControlDef;
|
||||
|
||||
auto K = [](const char* n, int p, float lo, float hi) {
|
||||
C c; c.name = n; c.param = p; c.lo = lo; c.hi = hi; return c;
|
||||
};
|
||||
|
||||
auto add = [&](const char* name, const char* category,
|
||||
const std::initializer_list<P>& delta,
|
||||
const ControlDef (&ctl)[4])
|
||||
{
|
||||
Preset& p = cat.data[cat.n++];
|
||||
p.name = name;
|
||||
p.category = category;
|
||||
p.base = defaultParams();
|
||||
for (auto& d : delta)
|
||||
p.base[d.first] = d.second;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
p.ctl[i] = ctl[i];
|
||||
};
|
||||
|
||||
const C cCff[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("F-ENV",PFENV,0.0f,1.0f), K("DELAY",PDMIX,0.15f,1.0f) };
|
||||
const C cResDcy[4]= { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("DECAY",PDEC,0.15f,0.98f),K("DELAY",PDMIX,0.15f,1.0f) };
|
||||
const C cEnvDcy[4]= { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.15f,0.98f),K("GLIDE",PGLIDE,0.0f,1.0f) };
|
||||
const C cPluck[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("DECAY",PDEC,0.1f,0.9f), K("RES",PRES,0.0f,1.0f), K("DELAY",PDMIX,0.0f,0.8f) };
|
||||
const C cPads[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("ATK",PATK,0.0f,1.0f), K("DECAY",PDEC,0.2f,0.95f), K("D-MIX",PDMIX,0.0f,1.0f) };
|
||||
const C cDrift[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("DETUNE",PO1CT,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) };
|
||||
const C cNoise[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("NOISE",PNOISE,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f), K("DELAY",PDMIX,0.0f,1.0f) };
|
||||
const C cShp[4] = { K("CUTOFF",PCUTOFF,0.05f,1.0f), K("RES",PRES,0.0f,1.0f), K("F-TYPE",PFTYPE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) };
|
||||
const C cKeys[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("DECAY",PDEC,0.1f,0.9f), K("DRIVE",PDRIVE,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) };
|
||||
const C cStab[4] = { K("CUTOFF",PCUTOFF,0.05f,0.95f), K("RES",PRES,0.0f,1.0f), K("F-ENV",PFENV,0.0f,1.0f), K("DRIVE",PDRIVE,0.0f,1.0f) };
|
||||
|
||||
// BASS ------------------------------------------------------------
|
||||
add("Sub Square", "BASS",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PMIX,0.9f},
|
||||
{PCUTOFF,0.4f},{PRES,0.35f},{PATK,0.015f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.25f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.4f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.15f,0.8f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.15f,0.95f),K("D-MIX",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Deep Saw", "BASS",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.317f},{PO2OCT,0.36f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.42f},{PRES,0.25f},{PFENV,0.3f},{PATK,0.03f},{PDEC,0.45f},{PSUS,0.65f},{PREL,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.45f},{PDMIX,0.35f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.1f,0.7f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Acid Bass", "BASS",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1OCT,0.283f},{PMIX,0.98f},{PCUTOFF,0.2f},{PRES,0.55f},
|
||||
{PFENV,0.75f},{PATK,0.008f},{PDEC,0.6f},{PSUS,0.3f},{PREL,0.4f},
|
||||
{PDSYNC,0.0f},{PDTIME,0.2f},{PDFB,0.6f},{PDMIX,0.3f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.04f,0.5f),K("RES",PRES,0.2f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("808 Thump", "BASS",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.25f},{PO2OCT,0.24f},{PMIX,0.85f},
|
||||
{PCUTOFF,0.3f},{PRES,0.7f},{PFENV,0.5f},{PATK,0.004f},{PDEC,0.65f},{PSUS,0.15f},{PREL,0.1f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.85f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.04f,0.6f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DRIVE",PDRIVE,0.0f,1.0f) });
|
||||
|
||||
add("DnB Reeses", "BASS",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.525f},{PO2CT,0.475f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.28f},{PRES,0.5f},{PFENV,0.6f},{PATK,0.01f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.15f},
|
||||
{PDRIVE,0.5f},{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDFB,0.4f},{PDMIX,0.25f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.7f),K("RES",PRES,0.0f,1.0f),K("DETUNE",PO1CT,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f) });
|
||||
|
||||
add("Round Sub", "BASS",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.25f},{PO1OCT,0.25f},{PO2OCT,0.25f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.5f},{PRES,0.2f},{PATK,0.02f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.2f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Filter Zam", "BASS",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.28f},{PO2OCT,0.35f},{PMIX,0.6f},
|
||||
{PCUTOFF,0.18f},{PRES,0.7f},{PFENV,0.8f},{PATK,0.01f},{PDEC,0.35f},{PSUS,0.4f},{PREL,0.3f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.55f},{PDMIX,0.3f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.04f,0.5f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f) });
|
||||
|
||||
add("Dirty Tron", "BASS",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.35f},{PRES,0.4f},{PFENV,0.4f},{PATK,0.02f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.15f},
|
||||
{PDRIVE,0.6f},{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.35f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.85f),K("RES",PRES,0.0f,1.0f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
// LEAD ------------------------------------------------------------
|
||||
add("Plain Sine", "LEAD",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PMIX,1.0f},{PCUTOFF,0.7f},{PRES,0.15f},{PFENV,0.3f},
|
||||
{PATK,0.02f},{PDEC,0.3f},{PSUS,0.65f},{PREL,0.2f},{PGLIDE,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
|
||||
cCff);
|
||||
|
||||
add("Square Lead", "LEAD",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.505f},{PMIX,0.5f},{PCUTOFF,0.55f},{PRES,0.3f},
|
||||
{PFENV,0.35f},{PATK,0.01f},{PDEC,0.3f},{PSUS,0.7f},{PREL,0.15f},{PGLIDE,0.25f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.5f},{PDMIX,0.3f},{PMASTER,0.75f} },
|
||||
cStab);
|
||||
|
||||
add("Saw Lead", "LEAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.5f},{PCUTOFF,0.5f},{PRES,0.25f},
|
||||
{PFENV,0.4f},{PATK,0.01f},{PDEC,0.35f},{PSUS,0.7f},{PREL,0.12f},{PGLIDE,0.3f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.35f},{PMASTER,0.75f} },
|
||||
cCff);
|
||||
|
||||
add("Distant Lead", "LEAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.25f},{PO1CT,0.53f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.3f},{PRES,0.4f},
|
||||
{PFENV,0.55f},{PATK,0.005f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.35f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.8f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("RES",PRES,0.0f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Glass Lead", "LEAD",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.62f},{PO2OCT,0.56f},{PO1CT,0.51f},{PO2CT,0.49f},
|
||||
{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.45f},{PFENV,0.5f},{PATK,0.01f},{PDEC,0.4f},{PSUS,0.6f},{PREL,0.25f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.72f} },
|
||||
cCff);
|
||||
|
||||
add("Supersaw Growl", "LEAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.45f},{PO2CT,0.55f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.3f},
|
||||
{PFENV,0.45f},{PATK,0.02f},{PDEC,0.35f},{PSUS,0.7f},{PREL,0.15f},{PDRIVE,0.35f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
|
||||
cDrift);
|
||||
|
||||
add("Night Rider", "LEAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1OCT,0.4f},{PO2OCT,0.5f},{PMIX,0.6f},{PCUTOFF,0.32f},{PRES,0.5f},
|
||||
{PFENV,0.6f},{PATK,0.015f},{PDEC,0.3f},{PSUS,0.7f},{PREL,0.2f},{PDRIVE,0.4f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.4f},{PMASTER,0.78f} },
|
||||
cShp);
|
||||
|
||||
add("Sine FM Bell", "LEAD",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.45f},{PCUTOFF,0.3f},{PRES,0.35f},{PFENV,0.7f},
|
||||
{PATK,0.002f},{PDEC,0.4f},{PSUS,0.1f},{PREL,0.4f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.75f} },
|
||||
{ K("RATIO",PO2OCT,0.0f,1.0f),K("LEVEL",PMIX,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
// PAD -------------------------------------------------------------
|
||||
add("Analog Pad", "PAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.49f},{PO2CT,0.51f},{PMIX,0.45f},{PCUTOFF,0.35f},{PRES,0.3f},
|
||||
{PFENV,0.45f},{PATK,0.45f},{PDEC,0.5f},{PSUS,0.7f},{PREL,0.6f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDFB,0.45f},{PDMIX,0.4f},{PMASTER,0.7f} },
|
||||
cPads);
|
||||
|
||||
add("Dream Pad", "PAD",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.52f},{PO2CT,0.48f},
|
||||
{PMIX,0.5f},{PCUTOFF,0.38f},{PRES,0.42f},{PFENV,0.5f},{PATK,0.5f},{PDEC,0.45f},{PSUS,0.7f},{PREL,0.7f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.55f},{PDMIX,0.5f},{PMASTER,0.7f} },
|
||||
cPads);
|
||||
|
||||
add("Dark Pad", "PAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.28f},{PO2OCT,0.35f},{PMIX,0.5f},{PCUTOFF,0.22f},{PRES,0.5f},
|
||||
{PFENV,0.35f},{PATK,0.6f},{PDEC,0.45f},{PSUS,0.75f},{PREL,0.8f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.5f},{PDMIX,0.45f},{PMASTER,0.72f} },
|
||||
cPads);
|
||||
|
||||
add("Brass Pad", "PAD",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.4f},{PO2OCT,0.5f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
|
||||
{PFENV,0.6f},{PATK,0.15f},{PDEC,0.4f},{PSUS,0.7f},{PREL,0.5f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.2f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("RES",PRES,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("String Swell", "PAD",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1CT,0.53f},{PO2CT,0.47f},{PMIX,0.45f},{PCUTOFF,0.3f},{PRES,0.35f},
|
||||
{PFENV,0.55f},{PATK,0.8f},{PDEC,0.35f},{PSUS,0.75f},{PREL,0.7f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.72f} },
|
||||
cPads);
|
||||
|
||||
add("Shimmer Pad", "PAD",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.5f},{PO1OCT,0.62f},{PO2OCT,0.5f},{PO1CT,0.53f},{PO2CT,0.47f},
|
||||
{PMIX,0.45f},{PCUTOFF,0.35f},{PRES,0.4f},{PFENV,0.5f},{PATK,0.4f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.8f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.65f},{PDMIX,0.6f},{PMASTER,0.7f} },
|
||||
cPads);
|
||||
|
||||
add("Airy Pad", "PAD",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.47f},{PO2CT,0.52f},{PMIX,0.5f},{PNOISE,0.1f},{PCUTOFF,0.4f},
|
||||
{PRES,0.25f},{PFENV,0.4f},{PATK,0.35f},{PDEC,0.5f},{PSUS,0.6f},{PREL,0.6f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDMIX,0.4f},{PMASTER,0.68f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("NOISE",PNOISE,0.0f,1.0f),K("DECAY",PDEC,0.2f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Vapor Pad", "PAD",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.25f},{PO1CT,0.55f},{PO2CT,0.48f},{PMIX,0.5f},{PCUTOFF,0.28f},{PRES,0.55f},
|
||||
{PFENV,0.6f},{PATK,0.5f},{PDEC,0.5f},{PSUS,0.55f},{PREL,0.85f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.7f} },
|
||||
cPads);
|
||||
|
||||
// KEYS ------------------------------------------------------------
|
||||
add("DX Bell", "KEYS",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.4f},{PCUTOFF,0.5f},{PRES,0.3f},{PFENV,0.3f},
|
||||
{PATK,0.002f},{PDEC,0.35f},{PSUS,0.1f},{PREL,0.5f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
|
||||
cKeys);
|
||||
|
||||
add("Wurly", "KEYS",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.62f},{PMIX,0.5f},{PCUTOFF,0.32f},{PRES,0.45f},
|
||||
{PFENV,0.5f},{PATK,0.004f},{PDEC,0.5f},{PSUS,0.2f},{PREL,0.3f},{PDRIVE,0.5f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.8f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
|
||||
|
||||
add("Rhodes EP", "KEYS",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.5f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.45f},
|
||||
{PCUTOFF,0.42f},{PRES,0.35f},{PFENV,0.35f},{PATK,0.003f},{PDEC,0.45f},{PSUS,0.15f},{PREL,0.35f},
|
||||
{PDRIVE,0.3f},{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.85f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
|
||||
|
||||
add("Clay EP", "KEYS",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.62f},{PMIX,0.6f},{PCUTOFF,0.38f},{PRES,0.4f},
|
||||
{PFENV,0.45f},{PATK,0.006f},{PDEC,0.4f},{PSUS,0.15f},{PREL,0.3f},{PDRIVE,0.35f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.25f},{PMASTER,0.72f} },
|
||||
cKeys);
|
||||
|
||||
add("Harpsi", "KEYS",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
|
||||
{PFENV,0.4f},{PATK,0.001f},{PDEC,0.5f},{PSUS,0.1f},{PREL,0.2f},{PDRIVE,0.3f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("D-MIX",PDMIX,0.0f,0.8f) });
|
||||
|
||||
add("Toy Piano", "KEYS",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PCUTOFF,0.3f},{PRES,0.5f},
|
||||
{PFENV,0.5f},{PATK,0.001f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.15f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDMIX,0.25f},{PMASTER,0.72f} },
|
||||
cKeys);
|
||||
|
||||
add("Warm EP", "KEYS",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO1CT,0.52f},{PO2CT,0.48f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.3f},
|
||||
{PFENV,0.4f},{PATK,0.004f},{PDEC,0.45f},{PSUS,0.3f},{PREL,0.3f},{PDRIVE,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,10.0f},{PDMIX,0.35f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("DRIVE",PDRIVE,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
// PLUCK -----------------------------------------------------------
|
||||
add("Wood Pluck", "PLUCK",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.0f},{PO1OCT,0.5f},{PO2OCT,0.8f},{PMIX,0.6f},{PCUTOFF,0.32f},{PRES,0.55f},
|
||||
{PFENV,0.7f},{PATK,0.001f},{PDEC,0.25f},{PSUS,0.0f},{PREL,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
|
||||
cPluck);
|
||||
|
||||
add("Perc Pluck", "PLUCK",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.5f},{PO2CT,0.58f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.25f},{PRES,0.6f},{PFENV,0.6f},{PATK,0.001f},{PDEC,0.2f},{PSUS,0.0f},{PREL,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.78f} },
|
||||
cPluck);
|
||||
|
||||
add("Guitar Pluck", "PLUCK",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.5f},{PO2CT,0.54f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.45f},
|
||||
{PFENV,0.6f},{PATK,0.002f},{PDEC,0.35f},{PSUS,0.0f},{PREL,0.25f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.72f} },
|
||||
cPluck);
|
||||
|
||||
add("Bubble", "PLUCK",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.25f},{PO2OCT,0.83f},{PMIX,0.4f},{PCUTOFF,0.3f},{PRES,0.6f},{PFENV,0.7f},
|
||||
{PATK,0.003f},{PDEC,0.45f},{PSUS,0.0f},{PREL,0.4f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDFB,0.5f},{PDMIX,0.4f},{PMASTER,0.75f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,0.9f),K("DECAY",PDEC,0.1f,0.95f),K("RES",PRES,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Kalimba", "PLUCK",
|
||||
{ {PO1WAVE,0.0f},{PO2WAVE,0.0f},{PO2OCT,0.83f},{PMIX,0.35f},{PCUTOFF,0.5f},{PRES,0.35f},{PFENV,0.4f},
|
||||
{PATK,0.001f},{PDEC,0.4f},{PSUS,0.0f},{PREL,0.35f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.75f} },
|
||||
{ K("RATIO",PO2OCT,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("RES",PRES,0.0f,1.0f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Karpling", "PLUCK",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.49f},{PO2CT,0.51f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.35f},{PRES,0.5f},{PFENV,0.5f},{PATK,0.001f},{PDEC,0.3f},{PSUS,0.0f},{PREL,0.2f},
|
||||
{PDRIVE,0.2f},{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.75f} },
|
||||
cPluck);
|
||||
|
||||
// FX --------------------------------------------------------------
|
||||
add("Wobble FX", "FX",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.0f},{PO1CT,0.58f},{PMIX,0.5f},{PCUTOFF,0.12f},{PRES,0.7f},{PFENV,0.9f},
|
||||
{PATK,0.01f},{PDEC,0.5f},{PSUS,0.3f},{PREL,0.4f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.7f},{PDMIX,0.5f},{PMASTER,0.8f} },
|
||||
cShp);
|
||||
|
||||
add("Riser", "FX",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.3f},{PO2OCT,0.4f},{PMIX,0.5f},{PNOISE,0.5f},{PCUTOFF,0.2f},
|
||||
{PRES,0.5f},{PFENV,0.7f},{PATK,0.9f},{PDEC,0.1f},{PSUS,0.8f},{PREL,0.2f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.7f} },
|
||||
{ K("RISE",PATK,0.2f,1.0f),K("CUTOFF",PCUTOFF,0.05f,1.0f),K("NOISE",PNOISE,0.0f,1.0f),K("D-MIX",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Laser", "FX",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.0f},{PMIX,1.0f},{PCUTOFF,0.08f},{PRES,0.8f},{PFENV,0.8f},
|
||||
{PATK,0.002f},{PDEC,0.4f},{PSUS,0.0f},{PREL,0.3f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.4f},{PDMIX,0.4f},{PMASTER,0.8f} },
|
||||
cShp);
|
||||
|
||||
add("Sweep", "FX",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.5f},{PO2OCT,0.75f},{PMIX,0.5f},{PNOISE,0.2f},{PCUTOFF,0.35f},
|
||||
{PRES,0.45f},{PFENV,0.6f},{PATK,0.2f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.6f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,12.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.72f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("F-ENV",PFENV,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.95f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Drone FX", "FX",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.5f},{PO1OCT,0.25f},{PO2OCT,0.28f},{PO1CT,0.54f},{PO2CT,0.55f},{PMIX,0.5f},
|
||||
{PNOISE,0.12f},{PCUTOFF,0.25f},{PRES,0.6f},{PFENV,0.2f},{PATK,0.8f},{PDEC,0.3f},{PSUS,0.8f},{PREL,0.8f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,16.0f},{PDFB,0.7f},{PDMIX,0.6f},{PMASTER,0.72f} },
|
||||
cNoise);
|
||||
|
||||
add("Tuned Noise", "FX",
|
||||
{ {PO1WAVE,0.8f},{PO2WAVE,0.8f},{PMIX,0.5f},{PNOISE,0.6f},{PCUTOFF,0.32f},{PRES,0.5f},{PFENV,0.5f},
|
||||
{PATK,0.05f},{PDEC,0.4f},{PSUS,0.4f},{PREL,0.5f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.68f} },
|
||||
cNoise);
|
||||
|
||||
// STAB / ARP ------------------------------------------------------
|
||||
add("Detroit Stab", "STAB",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1OCT,0.5f},{PO2OCT,0.5f},{PO1CT,0.52f},{PO2CT,0.48f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.4f},{PRES,0.45f},{PFENV,0.6f},{PATK,0.004f},{PDEC,0.35f},{PSUS,0.3f},{PREL,0.25f},{PDRIVE,0.3f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.8f} },
|
||||
cStab);
|
||||
|
||||
add("Juno Stab", "STAB",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.48f},{PRES,0.35f},
|
||||
{PFENV,0.5f},{PATK,0.003f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.2f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.3f},{PMASTER,0.78f} },
|
||||
cStab);
|
||||
|
||||
add("Chord Stab", "STAB",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.35f},{PRES,0.5f},
|
||||
{PFENV,0.65f},{PATK,0.004f},{PDEC,0.3f},{PSUS,0.4f},{PREL,0.3f},{PDRIVE,0.4f},
|
||||
{PDSYNC,0.0f},{PMASTER,0.78f} },
|
||||
cStab);
|
||||
|
||||
add("House Stab", "STAB",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.48f},{PO2CT,0.52f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.3f},
|
||||
{PFENV,0.5f},{PATK,0.003f},{PDEC,0.3f},{PSUS,0.3f},{PREL,0.18f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,8.0f},{PDMIX,0.25f},{PMASTER,0.8f} },
|
||||
cStab);
|
||||
|
||||
add("Arp Energy", "ARP",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.5f},{PO1CT,0.52f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.45f},{PRES,0.4f},
|
||||
{PFENV,0.5f},{PATK,0.008f},{PDEC,0.3f},{PSUS,0.5f},{PREL,0.15f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.5f},{PDMIX,0.45f},{PMASTER,0.78f} },
|
||||
cCff);
|
||||
|
||||
add("Trance Gate", "ARP",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.51f},{PO2CT,0.49f},{PMIX,0.5f},{PCUTOFF,0.4f},{PRES,0.3f},
|
||||
{PFENV,0.55f},{PATK,0.01f},{PDEC,0.5f},{PSUS,0.4f},{PREL,0.12f},{PDRIVE,0.25f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.6f},{PDMIX,0.5f},{PMASTER,0.78f} },
|
||||
{ K("CUTOFF",PCUTOFF,0.05f,1.0f),K("RES",PRES,0.0f,1.0f),K("DECAY",PDEC,0.1f,0.9f),K("DELAY",PDMIX,0.0f,1.0f) });
|
||||
|
||||
add("Neon Arp", "ARP",
|
||||
{ {PO1WAVE,0.5f},{PO2WAVE,0.25f},{PO1OCT,0.62f},{PO2OCT,0.5f},{PO1CT,0.55f},{PO2CT,0.5f},{PMIX,0.5f},
|
||||
{PCUTOFF,0.35f},{PRES,0.55f},{PFENV,0.7f},{PATK,0.003f},{PDEC,0.4f},{PSUS,0.2f},{PREL,0.3f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.6f},{PDMIX,0.55f},{PMASTER,0.78f} },
|
||||
cCff);
|
||||
|
||||
add("Retro Arp", "ARP",
|
||||
{ {PO1WAVE,0.75f},{PO2WAVE,0.75f},{PO1CT,0.5f},{PO2CT,0.5f},{PMIX,0.5f},{PCUTOFF,0.42f},{PRES,0.35f},
|
||||
{PFENV,0.5f},{PATK,0.004f},{PDEC,0.5f},{PSUS,0.5f},{PREL,0.1f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,4.0f},{PDFB,0.55f},{PDMIX,0.4f},{PMASTER,0.78f} },
|
||||
cCff);
|
||||
|
||||
add("Dark Arp", "ARP",
|
||||
{ {PO1WAVE,0.25f},{PO2WAVE,0.25f},{PO1CT,0.5f},{PO2CT,0.53f},{PMIX,0.5f},{PCUTOFF,0.25f},{PRES,0.6f},
|
||||
{PFENV,0.8f},{PATK,0.002f},{PDEC,0.3f},{PSUS,0.1f},{PREL,0.3f},
|
||||
{PDSYNC,1.0f},{PDSTEPS,6.0f},{PDFB,0.65f},{PDMIX,0.5f},{PMASTER,0.75f} },
|
||||
cShp);
|
||||
}
|
||||
|
||||
return cat;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const Preset* PresetStore::all() { return catalog().data; }
|
||||
int PresetStore::count() { return catalog().n; }
|
||||
|
||||
int PresetStore::indexOf(const char* name)
|
||||
{
|
||||
const auto& cat = catalog();
|
||||
for (int i = 0; i < cat.n; ++i)
|
||||
if (juce::String (cat.data[i].name) == name)
|
||||
return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Preset* PresetStore::get(int index)
|
||||
{
|
||||
const auto& cat = catalog();
|
||||
return cat.data + juce::jlimit (0, cat.n - 1, index);
|
||||
}
|
||||
|
||||
String PresetStore::fullLabel(const Preset& p)
|
||||
{
|
||||
return String ("[") + p.category + "] " + p.name;
|
||||
}
|
||||
|
||||
const Preset* PresetStore::findByIndex(int clampedIndex) { return get (clampedIndex); }
|
||||
|
||||
} // namespace sm26
|
||||
23
Source/Presets.h
Normal file
23
Source/Presets.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "SynthParams.h"
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
class PresetStore
|
||||
{
|
||||
public:
|
||||
static const Preset* all();
|
||||
static int count();
|
||||
static const Preset* get(int index);
|
||||
|
||||
static int indexOf(const char* name);
|
||||
|
||||
static const Preset* findByIndex(int clampedIndex);
|
||||
static String fullLabel(const Preset& p);
|
||||
|
||||
private:
|
||||
static Preset* table();
|
||||
};
|
||||
|
||||
} // namespace sm26
|
||||
343
Source/SynthEngine.cpp
Normal file
343
Source/SynthEngine.cpp
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
#include "SynthEngine.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
SynthEngine::SynthEngine()
|
||||
{
|
||||
auto def = defaultParams();
|
||||
for (int i = 0; i < kNumParams; ++i)
|
||||
params[i] = def[i];
|
||||
|
||||
knobParams[0] = PCUTOFF;
|
||||
knobParams[1] = PRES;
|
||||
knobParams[2] = PFENV;
|
||||
knobParams[3] = PDMIX;
|
||||
|
||||
knobNames[0] = "CUTOFF";
|
||||
knobNames[1] = "RES";
|
||||
knobNames[2] = "F-ENV";
|
||||
knobNames[3] = "DELAY";
|
||||
|
||||
for (int i = 0; i < kNumKnobs; ++i)
|
||||
{ knobLows[i] = 0.0f; knobHighs[i] = 1.0f; }
|
||||
|
||||
std::srand (12345);
|
||||
}
|
||||
|
||||
void SynthEngine::prepare (double sr, int mb)
|
||||
{
|
||||
sampleRate = sr;
|
||||
maxBlock = mb;
|
||||
delay.prepare (sr);
|
||||
allNotesOff();
|
||||
reset();
|
||||
}
|
||||
|
||||
void SynthEngine::reset()
|
||||
{
|
||||
allNotesOff();
|
||||
delay.clear();
|
||||
}
|
||||
|
||||
void SynthEngine::loadPreset (const Preset& p)
|
||||
{
|
||||
for (int i = 0; i < kNumParams; ++i)
|
||||
params[i] = p.base[i];
|
||||
for (int i = 0; i < kNumKnobs; ++i)
|
||||
{
|
||||
knobParams[i] = p.ctl[i].param;
|
||||
knobNames[i] = p.ctl[i].name;
|
||||
knobLows[i] = p.ctl[i].lo;
|
||||
knobHighs[i] = p.ctl[i].hi;
|
||||
}
|
||||
allNotesOff();
|
||||
}
|
||||
|
||||
void SynthEngine::setPresetIndex (int i)
|
||||
{
|
||||
currentPresetIndex = i;
|
||||
loadPreset (*PresetStore::findByIndex (i));
|
||||
}
|
||||
|
||||
void SynthEngine::setParam (int id, float n01)
|
||||
{
|
||||
if (id < 0 || id >= kNumParams) return;
|
||||
params[id] = juce::jlimit (0.0f, 1.0f, n01);
|
||||
}
|
||||
|
||||
float SynthEngine::getParam (int id) const
|
||||
{
|
||||
if (id < 0 || id >= kNumParams) return 0.0f;
|
||||
return params[id];
|
||||
}
|
||||
|
||||
SynthEngine::KnobView SynthEngine::knobView (int knob) const
|
||||
{
|
||||
if (knob < 0 || knob >= kNumKnobs) return {};
|
||||
KnobView k;
|
||||
k.name = knobNames[knob];
|
||||
k.param = knobParams[knob];
|
||||
k.value01 = getParam (k.param);
|
||||
return k;
|
||||
}
|
||||
|
||||
float SynthEngine::knobValue (int knob) const
|
||||
{
|
||||
if (knob < 0 || knob >= kNumKnobs) return 0.0f;
|
||||
return getParam (knobParams[knob]);
|
||||
}
|
||||
|
||||
const char* SynthEngine::waveNameFor (int index) const
|
||||
{
|
||||
return sm26::waveName (juce::jlimit (0, 4, index));
|
||||
}
|
||||
|
||||
void SynthEngine::noteOn (int note, float vel)
|
||||
{
|
||||
Voice* v = findFreeOrSteal();
|
||||
resetVoice (*v, note, vel);
|
||||
}
|
||||
|
||||
void SynthEngine::noteOff (int note)
|
||||
{
|
||||
for (auto& v : voices)
|
||||
if (v.active && v.note == note)
|
||||
v.envState = 3;
|
||||
}
|
||||
|
||||
void SynthEngine::allNotesOff()
|
||||
{
|
||||
for (auto& v : voices) { v.active = false; v.env = 0.0; v.envState = 0; }
|
||||
activeNotes = 0;
|
||||
}
|
||||
|
||||
SynthEngine::Voice* SynthEngine::findFreeOrSteal()
|
||||
{
|
||||
int freeIdx = -1;
|
||||
int oldest = -1;
|
||||
int oldestAge = -1;
|
||||
for (int i = 0; i < kNumVoices; ++i)
|
||||
{
|
||||
if (! voices[i].active) { freeIdx = i; break; }
|
||||
if (voices[i].age > oldestAge) { oldest = i; oldestAge = voices[i].age; }
|
||||
}
|
||||
if (freeIdx >= 0) { activeNotes++; return &voices[freeIdx]; }
|
||||
voices[oldest].envState = 3;
|
||||
voices[oldest].env = 0.0f;
|
||||
return &voices[oldest];
|
||||
}
|
||||
|
||||
void SynthEngine::resetVoice (Voice& v, int note, float vel)
|
||||
{
|
||||
v.active = true;
|
||||
v.note = note;
|
||||
v.velocity = vel;
|
||||
v.envState = 0;
|
||||
v.env = 0.0f;
|
||||
v.age = 0;
|
||||
v.ic1eq = 0.0;
|
||||
v.ic2eq = 0.0;
|
||||
|
||||
double freq = 440.0 * std::pow (2.0, (note - 69) / 12.0);
|
||||
double inc = freq / sampleRate;
|
||||
|
||||
double oct1 = paramReal (PO1OCT, params[PO1OCT]);
|
||||
double oct2 = paramReal (PO2OCT, params[PO2OCT]);
|
||||
double dt1 = paramReal (PO1CT, params[PO1CT]) / 1200.0;
|
||||
double dt2 = paramReal (PO2CT, params[PO2CT]) / 1200.0;
|
||||
|
||||
double f1 = freq * std::pow (2.0, oct1 + dt1);
|
||||
double f2 = freq * std::pow (2.0, oct2 + dt2);
|
||||
|
||||
v.inc1 = f1 / sampleRate;
|
||||
v.inc2 = f2 / sampleRate;
|
||||
v.glidedInc1 = v.inc1;
|
||||
v.ph1 = 0.0;
|
||||
v.ph2 = 0.0;
|
||||
}
|
||||
|
||||
float SynthEngine::oscWave (int wave, double phase)
|
||||
{
|
||||
double ph = phase - std::floor(phase);
|
||||
switch (wave)
|
||||
{
|
||||
case 0: return (float) std::sin (ph * 6.283185307179586);
|
||||
case 1: return (float) (1.0 - 4.0 * std::fabs (ph - 0.5));
|
||||
case 2: return (float) (2.0 * ph - 1.0);
|
||||
case 3: return (float) (ph < 0.5 ? 1.0 : -1.0);
|
||||
case 4: return (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0);
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static float softClip (float x)
|
||||
{
|
||||
return std::tanh (x * 1.5f) / std::tanh(1.5f);
|
||||
}
|
||||
|
||||
double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, double envLevel)
|
||||
{
|
||||
double baseHz = paramReal (PCUTOFF, (float) baseCutoff);
|
||||
baseHz = std::clamp (baseHz, 20.0, 20000.0);
|
||||
double mod = envAmt * envLevel * 5.0;
|
||||
double hz = baseHz * std::pow (2.0, mod);
|
||||
hz = std::clamp (hz, 20.0, std::min (sampleRate * 0.45, 20000.0));
|
||||
return (float) hz;
|
||||
}
|
||||
|
||||
void SynthEngine::process (AudioBuffer<float>& buffer, MidiBuffer& midi,
|
||||
int n, double bpm, bool transportPlaying)
|
||||
{
|
||||
wasPlaying = transportPlaying;
|
||||
lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0;
|
||||
|
||||
const float cutN01 = params[PCUTOFF];
|
||||
const float resN01 = params[PRES];
|
||||
const float fenvN01 = params[PFENV];
|
||||
const float ftypeN01= params[PFTYPE];
|
||||
const float atkN01 = params[PATK];
|
||||
const float decN01 = params[PDEC];
|
||||
const float susN01 = params[PSUS];
|
||||
const float relN01 = params[PREL];
|
||||
const float driveN01= params[PDRIVE];
|
||||
const float glideN01= params[PGLIDE];
|
||||
const float masterN01= params[PMASTER];
|
||||
const float mixN01 = params[PMIX];
|
||||
const float noiseN01= params[PNOISE];
|
||||
|
||||
const double atkSec = paramReal (PATK, atkN01);
|
||||
const double decSec = paramReal (PDEC, decN01);
|
||||
const double susLevel = susN01;
|
||||
const double relSec = paramReal (PREL, relN01);
|
||||
|
||||
const double atkCoef = 1.0 - std::exp (-1.0 / (atkSec * sampleRate + 1.0));
|
||||
const double decCoef = 1.0 - std::exp (-1.0 / (decSec * sampleRate + 1.0));
|
||||
const double relCoef = 1.0 - std::exp (-1.0 / (relSec * sampleRate + 1.0));
|
||||
|
||||
const double drive = 0.1 + driveN01 * 2.4;
|
||||
const double glideCoef = glideN01 < 0.001 ? 1.0 : 1.0 - std::exp (-1.0 / (glideN01 * sampleRate + 1.0));
|
||||
const double master = masterN01;
|
||||
|
||||
const double fenvScale = fenvN01 * 5.0;
|
||||
(void) fenvScale;
|
||||
|
||||
const float gMix = mixN01;
|
||||
const float g1 = 1.0f - gMix;
|
||||
const float g2 = gMix;
|
||||
const float g1p = (float) std::sqrt (g1);
|
||||
const float g2p = (float) std::sqrt (g2);
|
||||
|
||||
const int filterType = paramFilterType (ftypeN01);
|
||||
|
||||
bool delaySync = params[PDSYNC] > 0.5f;
|
||||
int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS] * 15.0f));
|
||||
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);
|
||||
lastDelaySamples = delaySec * sampleRate;
|
||||
|
||||
const float delayFB = params[PDFB];
|
||||
const float delayMix= params[PDMIX];
|
||||
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())
|
||||
noteOn (msg.getNoteNumber(), msg.getFloatVelocity());
|
||||
else if (sm < n && msg.isNoteOff())
|
||||
noteOff (msg.getNoteNumber());
|
||||
}
|
||||
|
||||
float* outL = buffer.getWritePointer (0);
|
||||
float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr;
|
||||
|
||||
for (int s = 0; s < n; ++s)
|
||||
{
|
||||
float sumL = 0.0f, sumR = 0.0f;
|
||||
|
||||
for (auto& v : voices)
|
||||
{
|
||||
if (! v.active) continue;
|
||||
|
||||
v.age++;
|
||||
v.ph1 += v.inc1;
|
||||
v.ph2 += v.inc2;
|
||||
if (v.ph1 > 1.0) v.ph1 -= 1.0;
|
||||
if (v.ph2 > 1.0) v.ph2 -= 1.0;
|
||||
|
||||
v.glidedInc1 = (float) ((1.0 - glideCoef) * v.glidedInc1 + glideCoef * v.inc1);
|
||||
|
||||
switch (v.envState)
|
||||
{
|
||||
case 0: v.env += (1.0 - v.env) * atkCoef; if (v.env > 0.999) v.envState = 1; break;
|
||||
case 1: v.env += (susLevel - v.env) * decCoef; break;
|
||||
case 2: break;
|
||||
case 3: v.env += (0.0 - v.env) * relCoef; if (v.env < 0.001) { v.active = false; v.env = 0.0; activeNotes--; } break;
|
||||
}
|
||||
|
||||
float o1 = oscWave (paramWaveIndex (params[PO1WAVE]), v.ph1);
|
||||
float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2);
|
||||
|
||||
float raw = g1p * o1 + g2p * o2 + noiseN01 * (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0) * 0.5f;
|
||||
raw *= 0.7f;
|
||||
|
||||
float cutoffHz = cutoffFor (v, cutN01, fenvN01, v.env);
|
||||
float Q = 0.5f + resN01 * 14.0f;
|
||||
float k = 1.0f / Q;
|
||||
double f = cutoffHz / (float) sampleRate;
|
||||
if (f > 0.49) f = 0.49;
|
||||
double g = std::tan (juce::MathConstants<double>::pi * f);
|
||||
double a1 = 1.0 / (1.0 + g * (g + k));
|
||||
double a2 = g * a1;
|
||||
double a3 = g * a2;
|
||||
|
||||
double vin = raw;
|
||||
double v3 = vin - v.ic2eq;
|
||||
double v1 = a1 * v.ic1eq + a2 * v3;
|
||||
double v2 = v.ic2eq + a2 * v.ic1eq + a3 * v3;
|
||||
v.ic1eq = 2.0 * v1 - v.ic1eq;
|
||||
v.ic2eq = 2.0 * v2 - v.ic2eq;
|
||||
|
||||
float filtered;
|
||||
switch (filterType)
|
||||
{
|
||||
case 0: filtered = (float) v2; break;
|
||||
case 1: filtered = (float) v1; break;
|
||||
case 2: filtered = (float) (vin - k * v1 - v2); break;
|
||||
default: filtered = (float) v2; break;
|
||||
}
|
||||
|
||||
float sig = softClip (filtered * (float) drive);
|
||||
sig *= (float) (v.env * v.velocity * master);
|
||||
sumL += sig;
|
||||
sumR += sig;
|
||||
}
|
||||
|
||||
float dry = sumL;
|
||||
int w = delay.writePos[0];
|
||||
float delayed = delay.buf[0][w];
|
||||
float fb = dry + delayed * (float) delayFB;
|
||||
fb = softClip (fb * 0.7f);
|
||||
delay.buf[0][w] = fb;
|
||||
delay.buf[1][w] = fb;
|
||||
delay.writePos[0] = (w + 1) % delay.maxLen;
|
||||
delay.writePos[1] = (w + 1) % delay.maxLen;
|
||||
|
||||
outL[s] = dryMix * dry + wetMix * delayed;
|
||||
if (outR) outR[s] = outL[s];
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sm26
|
||||
106
Source/SynthEngine.h
Normal file
106
Source/SynthEngine.h
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "SynthParams.h"
|
||||
#include "Presets.h"
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
class SynthEngine
|
||||
{
|
||||
public:
|
||||
static constexpr int kNumVoices = 12;
|
||||
|
||||
SynthEngine();
|
||||
|
||||
void prepare (double sampleRate, int maxBlockSize);
|
||||
void reset();
|
||||
|
||||
void loadPreset (const Preset& p);
|
||||
int presetIndex() const { return currentPresetIndex; }
|
||||
void setPresetIndex (int i);
|
||||
|
||||
void setParam (int id, float n01);
|
||||
float getParam (int id) const;
|
||||
|
||||
struct KnobView {
|
||||
const char* name;
|
||||
int param;
|
||||
float value01;
|
||||
float lo;
|
||||
float hi;
|
||||
};
|
||||
KnobView knobView (int knob) const;
|
||||
|
||||
float knobValue (int knob) const;
|
||||
|
||||
void process (AudioBuffer<float>& buffer, MidiBuffer& midi, int n,
|
||||
double bpm, bool transportPlaying);
|
||||
|
||||
int notesPlaying() const { return activeNotes; }
|
||||
float lastBpm() const { return lastBpmValue; }
|
||||
bool transport() const { return wasPlaying; }
|
||||
int delayTimeSamples() const { return lastDelaySamples; }
|
||||
|
||||
const char* waveNameFor (int index) const;
|
||||
|
||||
private:
|
||||
struct Voice {
|
||||
bool active = false;
|
||||
int note = 60;
|
||||
double velocity = 0.8;
|
||||
double ph1 = 0.0, ph2 = 0.0;
|
||||
double inc1 = 0.0, inc2 = 0.0, glidedInc1 = 0.0;
|
||||
double env = 0.0;
|
||||
int envState = 0;
|
||||
int age = 0;
|
||||
double ic1eq = 0.0, ic2eq = 0.0;
|
||||
};
|
||||
|
||||
void noteOn (int note, float vel);
|
||||
void noteOff (int note);
|
||||
void allNotesOff();
|
||||
Voice* findFreeOrSteal();
|
||||
void resetVoice (Voice& v, int note, float vel);
|
||||
float oscWave (int wave, double phase);
|
||||
float computeSingleSample (Voice& v, float& outL, float& outR,
|
||||
double fad, double frel, double fsus, double fcut,
|
||||
double fq, double drive, double glideCoef, double bpm);
|
||||
double cutoffFor (const Voice& v, double baseCutoff, double envAmt, double env);
|
||||
|
||||
double sampleRate = 48000.0;
|
||||
int maxBlock = 512;
|
||||
|
||||
float params[kNumParams];
|
||||
int knobParams[kNumKnobs];
|
||||
const char* knobNames[kNumKnobs];
|
||||
float knobLows[kNumKnobs];
|
||||
float knobHighs[kNumKnobs];
|
||||
|
||||
Voice voices[kNumVoices];
|
||||
int activeNotes = 0;
|
||||
|
||||
struct DelayLine
|
||||
{
|
||||
std::vector<std::vector<float>> buf;
|
||||
std::vector<int> writePos;
|
||||
int maxLen = 1;
|
||||
void prepare (double sr)
|
||||
{
|
||||
maxLen = (int) (sr * 18.0) + 4;
|
||||
buf.assign (2, std::vector<float> (maxLen, 0.0f));
|
||||
writePos.assign (2, 0);
|
||||
}
|
||||
void clear() { for (auto& c : buf) std::fill (c.begin(), c.end(), 0.0f); }
|
||||
};
|
||||
|
||||
DelayLine delay;
|
||||
double lastDelaySamples = 0.0;
|
||||
double lastBpmValue = 120.0;
|
||||
bool wasPlaying = false;
|
||||
|
||||
int currentPresetIndex = 0;
|
||||
mutable juce::SpinLock paramLock;
|
||||
};
|
||||
|
||||
} // namespace sm26
|
||||
191
Source/SynthParams.cpp
Normal file
191
Source/SynthParams.cpp
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#include "SynthParams.h"
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
static const char* const kStepLabels[] = {
|
||||
"1/32", "1/16T", "1/16", "1/8T",
|
||||
"1/8", "1/4T", "1/4", "1/2T",
|
||||
"1/2", "3/4", "1", "3/2",
|
||||
"2", "3", "4", "8",
|
||||
};
|
||||
|
||||
static const float kStepBeats[] = {
|
||||
0.125f, 0.1667f, 0.25f, 0.3333f,
|
||||
0.5f, 0.6667f, 1.0f, 1.3333f,
|
||||
2.0f, 3.0f, 4.0f, 6.0f,
|
||||
8.0f, 12.0f, 16.0f, 32.0f,
|
||||
};
|
||||
|
||||
const char* const* stepsLabels() { return kStepLabels; }
|
||||
const float* stepsBeats() { return kStepBeats; }
|
||||
|
||||
int stepsPerBeatFor(float stepIndex)
|
||||
{
|
||||
const int i = juce::jlimit(0, 15, (int) juce::roundToInt(stepIndex * 15.0f));
|
||||
return juce::roundToInt(kStepBeats[i] * 4.0f);
|
||||
}
|
||||
|
||||
float paramReal(int id, float n01)
|
||||
{
|
||||
n01 = clamp01(n01);
|
||||
switch (id)
|
||||
{
|
||||
case PO1OCT: return -2.0f + n01 * 4.0f;
|
||||
case PO1CT: return -100.0f + n01 * 200.0f;
|
||||
case PO2OCT: return -2.0f + n01 * 4.0f;
|
||||
case PO2CT: return -100.0f + n01 * 200.0f;
|
||||
case PMIX: return n01;
|
||||
case PNOISE: return n01;
|
||||
case PCUTOFF: return (float) std::exp (std::log (20.0) + n01 * (std::log (20000.0) - std::log (20.0)));
|
||||
case PRES: return 0.5f + n01 * 19.5f;
|
||||
case PFENV: return n01;
|
||||
case PATK: return (float) std::exp (std::log (0.001) + n01 * (std::log (8.0) - std::log (0.001)));
|
||||
case PDEC: return (float) std::exp (std::log (0.002) + n01 * (std::log (8.0) - std::log (0.002)));
|
||||
case PSUS: return n01;
|
||||
case PREL: return (float) std::exp (std::log (0.002) + n01 * (std::log (8.0) - std::log (0.002)));
|
||||
case PDTIME: return (float) std::exp (std::log (10.0) + n01 * (std::log (1500.0) - std::log (10.0)));
|
||||
case PDFB: return n01;
|
||||
case PDMIX: return n01;
|
||||
case PDRIVE: return n01;
|
||||
case PGLIDE: return (float) std::exp (std::log (0.001) + n01 * (std::log (2.0) - std::log (0.001)));
|
||||
case PMASTER: return 0.10f + n01 * 0.70f;
|
||||
default: return n01;
|
||||
}
|
||||
}
|
||||
|
||||
float unwrapParameter(int id, float n01)
|
||||
{
|
||||
n01 = clamp01(n01);
|
||||
switch (id)
|
||||
{
|
||||
case PO1OCT: case PO2OCT:
|
||||
return juce::jmax (0.0f, juce::jmin (1.0f, (n01 + 2.0f) / 4.0f));
|
||||
case PO1CT: case PO2CT:
|
||||
return (n01 + 100.0f) / 200.0f;
|
||||
case PCUTOFF:
|
||||
return (float) ((std::log (n01) - std::log (20.0)) / (std::log (20000.0) - std::log (20.0)));
|
||||
case PATK:
|
||||
return (float) ((std::log (n01) - std::log (0.001)) / (std::log (8.0) - std::log (0.001)));
|
||||
case PDEC:
|
||||
return (float) ((std::log (n01) - std::log (0.002)) / (std::log (8.0) - std::log (0.002)));
|
||||
case PREL:
|
||||
return (float) ((std::log (n01) - std::log (0.002)) / (std::log (8.0) - std::log (0.002)));
|
||||
case PDTIME:
|
||||
return (float) ((std::log (n01) - std::log (10.0)) / (std::log (1500.0) - std::log (10.0)));
|
||||
case PGLIDE:
|
||||
return (float) ((std::log (n01) - std::log (0.001)) / (std::log (2.0) - std::log (0.001)));
|
||||
case PMASTER:
|
||||
return (n01 - 0.10f) / 0.70f;
|
||||
default:
|
||||
return n01;
|
||||
}
|
||||
}
|
||||
|
||||
const char* paramShortName(int id)
|
||||
{
|
||||
static const char* const names[] = {
|
||||
"OSC1", "OSC2", "O1-OCT", "O1-DET", "O2-OCT", "O2-DET",
|
||||
"MIX", "NOISE", "CUTOFF", "RES", "F-ENV", "F-TYPE",
|
||||
"ATK", "DEC", "SUS", "REL", "D-TIME", "D-STEPS",
|
||||
"SYNC", "D-FB", "D-MIX", "DRIVE","GLIDE", "MASTER",
|
||||
};
|
||||
if (id >= 0 && id < kNumParams) return names[id];
|
||||
return "----";
|
||||
}
|
||||
|
||||
const char* paramUnit(int id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case PO1OCT: case PO2OCT: return "oct";
|
||||
case PO1CT: case PO2CT: return "ct";
|
||||
case PCUTOFF: return "Hz";
|
||||
case PDTIME: return "ms";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
int paramWaveIndex(float n01)
|
||||
{
|
||||
const int i = (int) juce::roundToInt (n01 * 4.0f);
|
||||
return juce::jlimit(0, 4, i);
|
||||
}
|
||||
|
||||
int paramFilterType(float n01)
|
||||
{
|
||||
return juce::jlimit (0, 2, (int) juce::roundToInt (n01 * 2.0f));
|
||||
}
|
||||
|
||||
const char* waveName(int w)
|
||||
{
|
||||
switch (w)
|
||||
{
|
||||
case 0: return "SINE";
|
||||
case 1: return "TRI";
|
||||
case 2: return "SAW";
|
||||
case 3: return "SQRE";
|
||||
case 4: return "NSE";
|
||||
default: return "----";
|
||||
}
|
||||
}
|
||||
|
||||
const char* filterTypeName(int t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case 0: return "LP";
|
||||
case 1: return "BP";
|
||||
case 2: return "HP";
|
||||
default: return "LP";
|
||||
}
|
||||
}
|
||||
|
||||
String formatTimeMs(float ms)
|
||||
{
|
||||
if (ms >= 1000.0f) return String (ms / 1000.0, 2) + " s";
|
||||
return String ((int) juce::roundToInt (ms)) + " ms";
|
||||
}
|
||||
|
||||
String formatFreq(float hz)
|
||||
{
|
||||
if (hz >= 1000.0f) return String (hz / 1000.0, 2) + " kHz";
|
||||
return String ((int) juce::roundToInt (hz)) + " Hz";
|
||||
}
|
||||
|
||||
String formatPercent(float v)
|
||||
{
|
||||
return String ((int) juce::roundToInt (v * 100.0f)) + "%";
|
||||
}
|
||||
|
||||
String formatParamReal(int id, float n01)
|
||||
{
|
||||
n01 = clamp01 (n01);
|
||||
switch (id)
|
||||
{
|
||||
case PO1WAVE: case PO2WAVE:
|
||||
return waveName (paramWaveIndex (n01));
|
||||
case PO1OCT: case PO2OCT:
|
||||
return String (paramReal (id, n01), 2);
|
||||
case PO1CT: case PO2CT:
|
||||
return String ((int) juce::roundToInt (paramReal (id, n01))) + " ct";
|
||||
case PMIX: case PNOISE: case PFENV: case PSUS:
|
||||
case PDFB: case PDMIX: case PDRIVE: case PMASTER:
|
||||
return formatPercent (n01);
|
||||
case PCUTOFF: return formatFreq (paramReal (id, n01));
|
||||
case PRES: return String (paramReal (id, n01), 1);
|
||||
case PFTYPE: return filterTypeName (paramFilterType (n01));
|
||||
case PATK: case PDEC: case PREL:
|
||||
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))];
|
||||
case PDSYNC:
|
||||
return n01 > 0.5f ? "SYNC" : "MS";
|
||||
case PGLIDE:
|
||||
return formatTimeMs (paramReal (id, n01) * 1000.0f);
|
||||
default:
|
||||
return String ((int) juce::roundToInt (n01 * 100.0f)) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sm26
|
||||
71
Source/SynthParams.h
Normal file
71
Source/SynthParams.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include <array>
|
||||
|
||||
namespace sm26 {
|
||||
|
||||
enum ParamId : int {
|
||||
PO1WAVE = 0, PO2WAVE, PO1OCT, PO1CT, PO2OCT, PO2CT,
|
||||
PMIX, PNOISE, PCUTOFF, PRES, PFENV, PFTYPE,
|
||||
PATK, PDEC, PSUS, PREL,
|
||||
PDTIME, PDSTEPS, PDSYNC, PDFB, PDMIX,
|
||||
PDRIVE, PGLIDE, PMASTER,
|
||||
kNumParams
|
||||
};
|
||||
|
||||
static constexpr int kNumKnobs = 4;
|
||||
|
||||
struct ControlDef {
|
||||
const char* name;
|
||||
int param = 0;
|
||||
float lo = 0.0f;
|
||||
float hi = 1.0f;
|
||||
};
|
||||
|
||||
struct Preset {
|
||||
const char* name;
|
||||
const char* category;
|
||||
std::array<float, kNumParams> base;
|
||||
std::array<ControlDef, kNumKnobs> ctl;
|
||||
};
|
||||
|
||||
inline std::array<float, kNumParams> defaultParams()
|
||||
{
|
||||
std::array<float, kNumParams> d{};
|
||||
d[PO1WAVE] = 0.0f; d[PO2WAVE] = 0.0f;
|
||||
d[PO1OCT] = 0.50f; d[PO1CT] = 0.50f;
|
||||
d[PO2OCT] = 0.50f; d[PO2CT] = 0.54f;
|
||||
d[PMIX] = 0.18f; d[PNOISE] = 0.02f;
|
||||
d[PCUTOFF] = 0.68f; d[PRES] = 0.22f;
|
||||
d[PFENV] = 0.38f; d[PFTYPE] = 0.0f;
|
||||
d[PATK] = 0.12f; d[PDEC] = 0.48f;
|
||||
d[PSUS] = 0.50f; d[PREL] = 0.55f;
|
||||
d[PDTIME] = 0.30f; d[PDSTEPS]= 6.0f;
|
||||
d[PDSYNC] = 1.0f; d[PDFB] = 0.42f;
|
||||
d[PDMIX] = 0.42f; d[PDRIVE] = 0.16f;
|
||||
d[PGLIDE] = 0.0f; d[PMASTER]= 0.70f;
|
||||
return d;
|
||||
}
|
||||
|
||||
const char* paramShortName(int id);
|
||||
const char* paramUnit(int id);
|
||||
float paramReal(int id, float n01);
|
||||
inline float clamp01(float v) { return v < 0.f ? 0.f : (v > 1.f ? 1.f : v); }
|
||||
|
||||
int paramFilterType(float n01);
|
||||
|
||||
String formatParamReal(int id, float n01);
|
||||
int paramWaveIndex(float n01);
|
||||
float unwrapParameter(int id, float n01);
|
||||
|
||||
String formatTimeMs(float ms);
|
||||
String formatFreq(float hz);
|
||||
String formatPercent(float v);
|
||||
const char* waveName(int w);
|
||||
|
||||
const char* const* stepsLabels();
|
||||
const float* stepsBeats();
|
||||
int stepsPerBeatFor(float stepIndex);
|
||||
|
||||
} // namespace sm26
|
||||
41
tools/gen_buildinfo.sh
Executable file
41
tools/gen_buildinfo.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/sh
|
||||
set -u
|
||||
|
||||
SRC_DIR="$1"
|
||||
OUT_FILE="$2"
|
||||
|
||||
git_ver="unknown"
|
||||
git_branch="unknown"
|
||||
|
||||
if command -v git >/dev/null 2>&1 && [ -d "$SRC_DIR/.git" ]; then
|
||||
ver=$(git -C "$SRC_DIR" describe --always --dirty --tags 2>/dev/null | tr -d '\n\r')
|
||||
if [ $? -eq 0 ] && [ -n "$ver" ]; then
|
||||
git_ver="$ver"
|
||||
fi
|
||||
|
||||
branch=$(git -C "$SRC_DIR" symbolic-ref --short -q HEAD 2>/dev/null | tr -d '\n\r')
|
||||
if [ -z "$branch" ]; then
|
||||
branch="detached"
|
||||
fi
|
||||
git_branch="$branch"
|
||||
fi
|
||||
|
||||
build_time=$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
mkdir -p "$(dirname "$OUT_FILE")"
|
||||
TMP="$OUT_FILE.tmp.$$"
|
||||
|
||||
cat > "$TMP" <<EOF
|
||||
#pragma once
|
||||
static constexpr const char* const SM26_GIT_VERSION = "$git_ver";
|
||||
static constexpr const char* const SM26_GIT_BRANCH = "$git_branch";
|
||||
static constexpr const char* const SM26_BUILD_TIME = "$build_time";
|
||||
EOF
|
||||
|
||||
if [ ! -f "$OUT_FILE" ] || ! cmp -s "$TMP" "$OUT_FILE"; then
|
||||
mv "$TMP" "$OUT_FILE"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
|
||||
echo "buildinfo: v$git_ver ($git_branch) @ $build_time"
|
||||
Loading…
Add table
Add a link
Reference in a new issue