This commit is contained in:
2026-02-01 18:56:19 -06:00
23 changed files with 1217 additions and 93 deletions

View File

@@ -77,7 +77,6 @@ qt_add_executable(metabolus
src/synth/Voice.h
src/synth/WavetableController.cpp
src/synth/WavetableController.h
resources/resources.qrc
src/ui/widgets/SmartSlider/SmartSlider.cpp
src/ui/widgets/SmartSlider/SmartSlider.h
src/ui/widgets/SmartSlider/SmartSlider.ui

View File

@@ -20,7 +20,7 @@ This synthesizer isn't very good, but it's neat :3
- [x] Create oscillator class where the actual tone generation occurs. Multiple
oscillators increase the sound complexity considerably
- [x] Create a UI scope to visualize the synthesized composite waveform
- [ ] Create wavetables for more complex tone generation. Needs to be selectable from ui
- [x] Create wavetables for more complex tone generation. Needs to be selectable from ui
- [ ] Wavetable file loading
- [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance)
- [x] Add polyphony somewhere. Probably involves a voice class. If processing power
@@ -30,9 +30,9 @@ This synthesizer isn't very good, but it's neat :3
- [ ] Filter upgrades including some more complex solving techniques (State Variable Filter),
better key tracking, more natural envelope curves, filter drive, etc.
- [ ] Reverb (quite a few ways to do this, needs more research)
- [ ] Pitch tuning with the multiple oscillators (Octave > Semitone > Fine-tune)
- [x] Pitch tuning with the multiple oscillators (Octave > Semitone > Fine-tune)
- [ ] Frequency Modulation (huge)
- [ ] Profile saving and loading, also includes loading configurations like keymaps, audio
- [x] Profile saving and loading, also includes loading configurations like keymaps, audio
engine config, etc. from a yaml instead of being hardcoded
- [ ] Noise
- [ ] LFO modulation

View File

@@ -3,7 +3,7 @@
# Configures properties for the RtAudio engine
# Number of samples per second
sampleRate: 44100
sampleRate: 44100
# unconfigurable: sampleFormat; [-1, 1] float
# number of audio channels

338
config/keymap.yaml Normal file
View File

@@ -0,0 +1,338 @@
# keymap.yaml
# Configures a computer keyboard mapping of keys to midi notes for when you don't have a midi device available
# (there's probably tools available that are able to make your keyboard act as a midi device but this was easier)
keymap:
Key_Shift: B_2
Key_Z: C_3
Key_S: C#3
Key_X: D_3
Key_D: D#3
Key_C: E_3
Key_V: F_3
Key_G: F#3
Key_B: G_3
Key_H: G#3
Key_N: A_3
Key_J: A#3
Key_M: B_3
Key_Q: C_4
Key_2: C#4
Key_W: D_4
Key_3: D#4
Key_E: E_4
Key_R: F_4
Key_5: F#4
Key_T: G_4
Key_6: G#4
Key_Y: A_4
Key_7: A#4
Key_U: B_4
Key_I: C_5
Key_9: C#5
Key_O: D_5
# below are translations from strings to numbers that both Qt and the Oscillators can understand
# do not touch them unless you like the program not running
# note strings to midi ids, do not touch
# only has sharps for now, you can figure the rest out
notes:
A_0: 21
A#0: 22
B_0: 23
C_1: 24
C#1: 25
D_1: 26
D#1: 27
E_1: 28
F_1: 29
F#1: 30
G_1: 31
G#1: 32
A_1: 33
A#1: 34
B_1: 35
C_2: 36
C#2: 37
D_2: 38
D#2: 39
E_2: 40
F_2: 41
F#2: 42
G_2: 43
G#2: 44
A_2: 45
A#2: 46
B_2: 47
C_3: 48
C#3: 49
D_3: 50
D#3: 51
E_3: 52
F_3: 53
F#3: 54
G_3: 55
G#3: 56
A_3: 57
A#3: 58
B_3: 59
C_4: 60
C#4: 61
D_4: 62
D#4: 63
E_4: 64
F_4: 65
F#4: 66
G_4: 67
G#4: 68
A_4: 69
A#4: 70
B_4: 71
C_5: 72
C#5: 73
D_5: 74
D#5: 75
E_5: 76
F_5: 77
F#5: 78
G_5: 79
G#5: 80
A_5: 81
A#5: 82
B_5: 83
C_6: 84
C#6: 85
D_6: 86
D#6: 87
E_6: 88
F_6: 89
F#6: 90
G_6: 91
G#6: 92
A_6: 93
A#6: 94
B_6: 95
C_7: 96
C#7: 97
D_7: 98
D#7: 99
E_7: 100
F_7: 101
F#7: 102
G_7: 103
G#7: 104
A_7: 105
A#7: 106
B_7: 107
C_8: 108
# key strings to qt-key-ids, do not touch
keys:
Key_Space: 0x20
Key_Any: 0x20
Key_Exclam: 0x21
Key_QuoteDbl: 0x22
Key_NumberSign: 0x23
Key_Dollar: 0x24
Key_Percent: 0x25
Key_Ampersand: 0x26
Key_Apostrophe: 0x27
Key_ParenLeft: 0x28
Key_ParenRight: 0x29
Key_Asterisk: 0x2a
Key_Plus: 0x2b
Key_Comma: 0x2c
Key_Minus: 0x2d
Key_Period: 0x2e
Key_Slash: 0x2f
Key_0: 0x30
Key_1: 0x31
Key_2: 0x32
Key_3: 0x33
Key_4: 0x34
Key_5: 0x35
Key_6: 0x36
Key_7: 0x37
Key_8: 0x38
Key_9: 0x39
Key_Colon: 0x3a
Key_Semicolon: 0x3b
Key_Less: 0x3c
Key_Equal: 0x3d
Key_Greater: 0x3e
Key_Question: 0x3f
Key_At: 0x40
Key_A: 0x41
Key_B: 0x42
Key_C: 0x43
Key_D: 0x44
Key_E: 0x45
Key_F: 0x46
Key_G: 0x47
Key_H: 0x48
Key_I: 0x49
Key_J: 0x4a
Key_K: 0x4b
Key_L: 0x4c
Key_M: 0x4d
Key_N: 0x4e
Key_O: 0x4f
Key_P: 0x50
Key_Q: 0x51
Key_R: 0x52
Key_S: 0x53
Key_T: 0x54
Key_U: 0x55
Key_V: 0x56
Key_W: 0x57
Key_X: 0x58
Key_Y: 0x59
Key_Z: 0x5a
Key_BracketLeft: 0x5b
Key_Backslash: 0x5c
Key_BracketRight: 0x5d
Key_AsciiCircum: 0x5e
Key_Underscore: 0x5f
Key_QuoteLeft: 0x60
Key_BraceLeft: 0x7b
Key_Bar: 0x7c
Key_BraceRight: 0x7d
Key_AsciiTilde: 0x7e
Key_nobreakspace: 0x0a0
Key_exclamdown: 0x0a1
Key_cent: 0x0a2
Key_sterling: 0x0a3
Key_currency: 0x0a4
Key_yen: 0x0a5
Key_brokenbar: 0x0a6
Key_section: 0x0a7
Key_diaeresis: 0x0a8
Key_copyright: 0x0a9
Key_ordfeminine: 0x0aa
Key_guillemotleft: 0x0ab
Key_notsign: 0x0ac
Key_hyphen: 0x0ad
Key_registered: 0x0ae
Key_macron: 0x0af
Key_degree: 0x0b0
Key_plusminus: 0x0b1
Key_twosuperior: 0x0b2
Key_threesuperior: 0x0b3
Key_acute: 0x0b4
Key_micro: 0x0b5
Key_paragraph: 0x0b6
Key_periodcentered: 0x0b7
Key_cedilla: 0x0b8
Key_onesuperior: 0x0b9
Key_masculine: 0x0ba
Key_guillemotright: 0x0bb
Key_onequarter: 0x0bc
Key_onehalf: 0x0bd
Key_threequarters: 0x0be
Key_questiondown: 0x0bf
Key_Agrave: 0x0c0
Key_Aacute: 0x0c1
Key_Acircumflex: 0x0c2
Key_Atilde: 0x0c3
Key_Adiaeresis: 0x0c4
Key_Aring: 0x0c5
Key_AE: 0x0c6
Key_Ccedilla: 0x0c7
Key_Egrave: 0x0c8
Key_Eacute: 0x0c9
Key_Ecircumflex: 0x0ca
Key_Ediaeresis: 0x0cb
Key_Igrave: 0x0cc
Key_Iacute: 0x0cd
Key_Icircumflex: 0x0ce
Key_Idiaeresis: 0x0cf
Key_ETH: 0x0d0
Key_Ntilde: 0x0d1
Key_Ograve: 0x0d2
Key_Oacute: 0x0d3
Key_Ocircumflex: 0x0d4
Key_Otilde: 0x0d5
Key_Odiaeresis: 0x0d6
Key_multiply: 0x0d7
Key_Ooblique: 0x0d8
Key_Ugrave: 0x0d9
Key_Uacute: 0x0da
Key_Ucircumflex: 0x0db
Key_Udiaeresis: 0x0dc
Key_Yacute: 0x0dd
Key_THORN: 0x0de
Key_ssharp: 0x0df
Key_division: 0x0f7
Key_ydiaeresis: 0x0ff
Key_Escape: 0x01000000
Key_Tab: 0x01000001
Key_Backtab: 0x01000002
Key_Backspace: 0x01000003
Key_Return: 0x01000004
Key_Enter: 0x01000005
Key_Insert: 0x01000006
Key_Delete: 0x01000007
Key_Pause: 0x01000008
Key_Print: 0x01000009
Key_SysReq: 0x0100000a
Key_Clear: 0x0100000b
Key_Home: 0x01000010
Key_End: 0x01000011
Key_Left: 0x01000012
Key_Up: 0x01000013
Key_Right: 0x01000014
Key_Down: 0x01000015
Key_PageUp: 0x01000016
Key_PageDown: 0x01000017
Key_Shift: 0x01000020
Key_Control: 0x01000021
Key_Meta: 0x01000022
Key_Alt: 0x01000023
Key_CapsLock: 0x01000024
Key_NumLock: 0x01000025
Key_ScrollLock: 0x01000026
Key_F1: 0x01000030
Key_F2: 0x01000031
Key_F3: 0x01000032
Key_F4: 0x01000033
Key_F5: 0x01000034
Key_F6: 0x01000035
Key_F7: 0x01000036
Key_F8: 0x01000037
Key_F9: 0x01000038
Key_F10: 0x01000039
Key_F11: 0x0100003a
Key_F12: 0x0100003b
Key_F13: 0x0100003c
Key_F14: 0x0100003d
Key_F15: 0x0100003e
Key_F16: 0x0100003f
Key_F17: 0x01000040
Key_F18: 0x01000041
Key_F19: 0x01000042
Key_F20: 0x01000043
Key_F21: 0x01000044
Key_F22: 0x01000045
Key_F23: 0x01000046
Key_F24: 0x01000047
Key_F25: 0x01000048
Key_F26: 0x01000049
Key_F27: 0x0100004a
Key_F28: 0x0100004b
Key_F29: 0x0100004c
Key_F30: 0x0100004d
Key_F31: 0x0100004e
Key_F32: 0x0100004f
Key_F33: 0x01000050
Key_F34: 0x01000051
Key_F35: 0x01000052
Key_Super_L: 0x01000053
Key_Super_R: 0x01000054
Key_Menu: 0x01000055
Key_Hyper_L: 0x01000056
Key_Hyper_R: 0x01000057
Key_Help: 0x01000058
Key_Direction_L: 0x01000059
Key_Direction_R: 0x01000060

View File

@@ -4,29 +4,38 @@
# sequences in the form [x, x, x] denote [setValue, sliderMinimum, sliderMaximum]
version: 0x0002
version: 0x0003
# deprecated, useless
Osc1Freq: [100, 20, 600]
# wavetable selections
OscWaveSelector1: 2
OscWaveSelector2: 1
OscWaveSelector2: 3
# Oscillator frequency parameters
# Frequency parameters
MasterOctaveOffset: [0, -5, 5]
MasterSemitoneOffset: [0, -12, 12]
MasterPitchOffset: [0, -100, 100]
Osc1OctaveOffset: [0, -5, 5]
Osc1SemitoneOffset: [0, -12, 12]
Osc1PitchOffset: [0, -100, 100]
Osc1PitchOffset: [1.34, -100, 100]
Osc2OctaveOffset: [1, -5, 5]
Osc2SemitoneOffset: [0, -12, 12]
Osc2PitchOffset: [0, -100, 100]
Osc2PitchOffset: [12.86, -100, 100]
Osc3OctaveOffset: [1, -5, 5]
Osc3SemitoneOffset: [7, -12, 12]
Osc3PitchOffset: [1.96, -100, 100]
Osc3PitchOffset: [-8.79, -100, 100]
# gonna have something like this:
#MasterPitchOffset:
# - [0, -5, 5] # Octave
# - [0, -12, -12] # Semitone
# - [0, -100, 100] # Pitch
# Envelope generator parameters
Osc1Volume:
- [1, 0, 2] # Depth
- [1, 0, 10] # Depth
- [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay
- [0.7, 0, 1] # Sustain

View File

@@ -47,7 +47,7 @@ int ConfigInterface::getValue(ConfigFile file, std::string key, int defaultVal)
}
// ugly but if it works it works
void ConfigInterface::loadProfile(std::string filename) {
YAML::Node ConfigInterface::loadProfile(std::string filename) {
// load file
std::string filepath = "config/profiles/" + filename + ".yaml";
@@ -57,16 +57,16 @@ void ConfigInterface::loadProfile(std::string filename) {
config = YAML::LoadFile(filepath);
} catch(const std::exception& e) {
std::cerr << e.what() << std::endl;
return;
return config;
}
// check version
int version = config["version"].as<int>(); // yaml-cpp parses unquoted hex as integers
if(version < CONFIG_VERSION) {
std::cout << "Parameter profile version " << version << "is outdated below the compatible version " << CONFIG_VERSION << std::endl;
return;
return config;
} else {
std::cout << version << std::endl;
std::cout << "Parameter profile version " << version << std::endl;
}
// extract values from the config file
@@ -74,6 +74,27 @@ void ConfigInterface::loadProfile(std::string filename) {
std::array<ParamDefault, 5> fCutoffProfile = loadEnvProfile(&config, "FilterCutoff");
std::array<ParamDefault, 5> fResonanceProfile = loadEnvProfile(&config, "FilterResonance");
std::array<ParamDefault, 3> masterPitchOffsets = {{
{ config["MasterOctaveOffset"][0].as<float>(), config["MasterOctaveOffset"][1].as<float>(), config["MasterOctaveOffset"][2].as<float>() },
{ config["MasterSemitoneOffset"][0].as<float>(), config["MasterSemitoneOffset"][1].as<float>(), config["MasterSemitoneOffset"][2].as<float>() },
{ config["MasterPitchOffset"][0].as<float>(), config["MasterPitchOffset"][1].as<float>(), config["MasterPitchOffset"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc1PitchOffsets = {{
{ config["Osc1OctaveOffset"][0].as<float>(), config["Osc1OctaveOffset"][1].as<float>(), config["Osc1OctaveOffset"][2].as<float>() },
{ config["Osc1SemitoneOffset"][0].as<float>(), config["Osc1SemitoneOffset"][1].as<float>(), config["Osc1SemitoneOffset"][2].as<float>() },
{ config["Osc1PitchOffset"][0].as<float>(), config["Osc1PitchOffset"][1].as<float>(), config["Osc1PitchOffset"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc2PitchOffsets = {{
{ config["Osc2OctaveOffset"][0].as<float>(), config["Osc2OctaveOffset"][1].as<float>(), config["Osc2OctaveOffset"][2].as<float>() },
{ config["Osc2SemitoneOffset"][0].as<float>(), config["Osc2SemitoneOffset"][1].as<float>(), config["Osc2SemitoneOffset"][2].as<float>() },
{ config["Osc2PitchOffset"][0].as<float>(), config["Osc2PitchOffset"][1].as<float>(), config["Osc2PitchOffset"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc3PitchOffsets = {{
{ config["Osc3OctaveOffset"][0].as<float>(), config["Osc3OctaveOffset"][1].as<float>(), config["Osc3OctaveOffset"][2].as<float>() },
{ config["Osc3SemitoneOffset"][0].as<float>(), config["Osc3SemitoneOffset"][1].as<float>(), config["Osc3SemitoneOffset"][2].as<float>() },
{ config["Osc3PitchOffset"][0].as<float>(), config["Osc3PitchOffset"][1].as<float>(), config["Osc3PitchOffset"][2].as<float>() },
}};
// TODO: remove this once all the parameters are set properly
params_->resetToDefaults();
@@ -81,13 +102,26 @@ void ConfigInterface::loadProfile(std::string filename) {
params_->set(EnvelopeId::Osc1Volume, osc1VolumeProfile[0].def, osc1VolumeProfile[1].def, osc1VolumeProfile[2].def, osc1VolumeProfile[3].def, osc1VolumeProfile[4].def);
params_->set(EnvelopeId::FilterCutoff, fCutoffProfile[0].def, fCutoffProfile[1].def, fCutoffProfile[2].def, fCutoffProfile[3].def, fCutoffProfile[4].def);
params_->set(EnvelopeId::FilterResonance, fResonanceProfile[0].def, fResonanceProfile[1].def, fResonanceProfile[2].def, fResonanceProfile[3].def, fResonanceProfile[4].def);
// TODO: why do I bother passing in 5 values independently when I can just do an array ?
// VVV look down there its so easy
// TODO: why do I bother passing in 5 values independently when I can just do an array like in loadEnvProfile ?
params_->set(ParamId::MasterOctaveOffset, masterPitchOffsets[0].def);
params_->set(ParamId::MasterSemitoneOffset, masterPitchOffsets[1].def);
params_->set(ParamId::MasterPitchOffset, masterPitchOffsets[2].def);
params_->set(ParamId::Osc1OctaveOffset, osc1PitchOffsets[0].def);
params_->set(ParamId::Osc1SemitoneOffset, osc1PitchOffsets[1].def);
params_->set(ParamId::Osc1PitchOffset, osc1PitchOffsets[2].def);
params_->set(ParamId::Osc2OctaveOffset, osc2PitchOffsets[0].def);
params_->set(ParamId::Osc2SemitoneOffset, osc2PitchOffsets[1].def);
params_->set(ParamId::Osc2PitchOffset, osc2PitchOffsets[2].def);
params_->set(ParamId::Osc3OctaveOffset, osc3PitchOffsets[0].def);
params_->set(ParamId::Osc3SemitoneOffset, osc3PitchOffsets[1].def);
params_->set(ParamId::Osc3PitchOffset, osc3PitchOffsets[2].def);
// TODO:
// load wavetable settings
// load oscillator pitch settings
return config;
}
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) {

View File

@@ -32,7 +32,7 @@ public:
int getValue(ConfigFile file, std::string key, int defaultVal);
void loadProfile(std::string filename);
YAML::Node loadProfile(std::string filename);
std::array<ParamDefault, 5> loadEnvProfile(YAML::Node* node, std::string profile);
std::array<ParamDefault, 5> loadEnvProfile(std::string filename, std::string profile);

View File

@@ -2,38 +2,40 @@
#include "KeyboardController.h"
#include <iostream>
#include <yaml-cpp/yaml.h>
#include <filesystem>
KeyboardController::KeyboardController(NoteQueue& queue) : queue_(queue) {
KeyboardController::KeyboardController(NoteQueue& queue, ConfigInterface* config) : queue_(queue), config_(config) {
// load keymap from config file
std::string filepath = "config/keymap.yaml";
filepath = std::filesystem::absolute(filepath).string();
YAML::Node file;
try {
file = YAML::LoadFile(filepath);
} catch(const std::exception& e) {
std::cerr << e.what() << std::endl;
return;
}
YAML::Node keymapNode = file["keymap"]; // node for string to string mappings
YAML::Node notesNode = file["notes"]; // string to midi int mappings
YAML::Node keysNode = file["keys"]; // string to qt key id mappings
// for each element in the keymap
for (const auto& entry : keymapNode) {
std::string keyString = entry.first.as<std::string>();
std::string noteString = entry.second.as<std::string>();
// match the strings to ints
uint8_t noteValue = notesNode[noteString].as<uint8_t>();
uint32_t keyValue = keysNode[keyString].as<uint32_t>();
// insert into map
keymap_.emplace(keyValue, noteValue);
}
// TODO: also configurable via a yml
keymap_ = {
{ Qt::Key_Shift, 47 }, // B 2
{ Qt::Key_Z, 48 }, // C 3
{ Qt::Key_S, 49 }, // C#
{ Qt::Key_X, 50 }, // D
{ Qt::Key_D, 51 }, // D#
{ Qt::Key_C, 52 }, // E
{ Qt::Key_V, 53 }, // F
{ Qt::Key_G, 54 }, // F#
{ Qt::Key_B, 55 }, // G
{ Qt::Key_H, 56 }, // G#
{ Qt::Key_N, 57 }, // A
{ Qt::Key_J, 58 }, // A#
{ Qt::Key_M, 59 }, // B 3
{ Qt::Key_Q, 60 }, // C 4
{ Qt::Key_2, 61 }, // C#
{ Qt::Key_W, 62 }, // D
{ Qt::Key_3, 63 }, // D#
{ Qt::Key_E, 64 }, // E
{ Qt::Key_R, 65 }, // F
{ Qt::Key_5, 66 }, // F#
{ Qt::Key_T, 67 }, // G
{ Qt::Key_6, 68 }, // G#
{ Qt::Key_Y, 69 }, // A
{ Qt::Key_7, 70 }, // A#
{ Qt::Key_U, 71 }, // B 4
{ Qt::Key_I, 72 }, // C 5
};
}
void KeyboardController::handleKeyPress(QKeyEvent* e) {

View File

@@ -5,12 +5,13 @@
#include <unordered_map>
#include "NoteQueue.h"
#include "ConfigInterface.h"
// The keyboardcontroller handles user inputs from a keyboard and maps them to note events
class KeyboardController {
public:
explicit KeyboardController(NoteQueue& queue);
explicit KeyboardController(NoteQueue& queue, ConfigInterface* config);
~KeyboardController() = default;
void handleKeyPress(QKeyEvent* e);
@@ -19,6 +20,7 @@ public:
private:
NoteQueue& queue_;
ConfigInterface* config_;
// keymap is key -> midi note id
std::unordered_map<int32_t, uint8_t> keymap_;

View File

@@ -9,6 +9,9 @@ enum class ParamId : uint16_t {
Osc1Frequency,
Osc1WaveSelector1,
Osc1WaveSelector2,
MasterOctaveOffset,
MasterSemitoneOffset,
MasterPitchOffset,
Osc1OctaveOffset,
Osc1SemitoneOffset,
Osc1PitchOffset,
@@ -69,12 +72,13 @@ struct ParamDefault {
float max;
};
// TODO: make these configurable via yml file too
// later TODO: and then when I have full on profile saving there will be a default profile to load from
constexpr std::array<ParamDefault, static_cast<size_t>(ParamId::Count)> PARAM_DEFS {{
{ 100.0f, 20.0f, 600.0f}, // Osc1Freq
{ 2.0f, 0.0f, 0.0f}, // OscWaveSelector1
{ 1.0f, 0.0f, 0.0f}, // OscWaveSelector2
{ 0.0f, -5.0f, 5.0f}, // MasterOctaveOffset
{ 0.0f, -12.0f, 12.0f}, // MasterSemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // MasterPitchOffset
{ 0.0f, -5.0f, 5.0f}, // Osc1OctaveOffset
{ 0.0f, -12.0f, 12.0f}, // Osc1SemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // Osc1PitchOffset

View File

@@ -35,9 +35,17 @@ bool AudioEngine::start() {
RtAudio::StreamOptions options;
options.flags = RTAUDIO_MINIMIZE_LATENCY;
// TODO: error check this please
audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
audio_.startStream();
RtAudioErrorType status = audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
if(status != RTAUDIO_NO_ERROR) {
std::cout << "Error opening RtAudio stream" << std::endl;
return false;
}
status = audio_.startStream();
if(status != RTAUDIO_NO_ERROR) {
std::cout << "Error starting RtAudio stream" << std::endl;
return false;
}
// sanity check
std::cout << "sample rate: " << sampleRate_ << " buffer frames: " << bufferFrames_ << std::endl;

View File

@@ -49,7 +49,8 @@ private:
ScopeBuffer scope_ { 1024 }; // stores audio samples for visualization
RtAudio audio_{AUDIO_API}; // audio device
// TODO: id like a yml config file or something for these
// Configurable in the audio.yaml file, assigned defaults if the file is not found
uint32_t sampleRate_ = 44100;
uint32_t bufferFrames_ = 512; // time per buffer = BF/SR (256/44100 = 5.8ms)
uint32_t channels_ = 2; // stereo

View File

@@ -2,6 +2,7 @@
#include "Voice.h"
#include <cmath>
#include <iostream>
#include <random>
Voice::Voice(SmoothedParam* params, WavetableController* wavetable) : params_(params), wavetable_(wavetable) {
@@ -89,16 +90,22 @@ float Voice::process(float* params, bool& scopeTrigger) {
// calculate the note/pitch of the oscillators
bool temp = false;
uint8_t osc1NoteOffset = static_cast<uint8_t>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc1OctaveOffset) + getParam(ParamId::Osc1SemitoneOffset));
uint8_t osc2NoteOffset = static_cast<uint8_t>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc2OctaveOffset) + getParam(ParamId::Osc2SemitoneOffset));
uint8_t osc3NoteOffset = static_cast<uint8_t>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc3OctaveOffset) + getParam(ParamId::Osc3SemitoneOffset));
uint8_t osc1NoteOffset = static_cast<uint8_t>(std::round((SYNTH_NOTES_PER_OCTAVE) * (getParam(ParamId::Osc1OctaveOffset) + getParam(ParamId::MasterOctaveOffset)) + getParam(ParamId::Osc1SemitoneOffset) + getParam(ParamId::MasterSemitoneOffset)));
uint8_t osc2NoteOffset = static_cast<uint8_t>(std::round((SYNTH_NOTES_PER_OCTAVE) * (getParam(ParamId::Osc2OctaveOffset) + getParam(ParamId::MasterOctaveOffset)) + getParam(ParamId::Osc2SemitoneOffset) + getParam(ParamId::MasterSemitoneOffset)));
uint8_t osc3NoteOffset = static_cast<uint8_t>(std::round((SYNTH_NOTES_PER_OCTAVE) * (getParam(ParamId::Osc3OctaveOffset) + getParam(ParamId::MasterOctaveOffset)) + getParam(ParamId::Osc3SemitoneOffset) + getParam(ParamId::MasterSemitoneOffset)));
// sample oscillators
float osc1 = oscillators_[0].process(osc1NoteOffset + note_, getParam(ParamId::Osc1PitchOffset)/100.0f, scopeTrigger);
float osc2 = oscillators_[1].process(osc2NoteOffset + note_, getParam(ParamId::Osc2PitchOffset)/100.0f, temp);
float osc3 = oscillators_[2].process(osc3NoteOffset + note_, getParam(ParamId::Osc3PitchOffset)/100.0f, temp);
float osc1 = oscillators_[0].process(osc1NoteOffset + note_, (getParam(ParamId::Osc1PitchOffset) + getParam(ParamId::MasterPitchOffset))/100.0f, scopeTrigger);
float osc2 = oscillators_[1].process(osc2NoteOffset + note_, (getParam(ParamId::Osc2PitchOffset) + getParam(ParamId::MasterPitchOffset))/100.0f, temp);
float osc3 = oscillators_[2].process(osc3NoteOffset + note_, (getParam(ParamId::Osc3PitchOffset) + getParam(ParamId::MasterPitchOffset))/100.0f, temp);
// TODO: implement controls for noise
//float scale = static_cast<float>(rand()) / static_cast<float>(RAND_MAX); // Range [0.0, 1.0]
//float noise = -1.0f + 2.0f * scale;
// these values didn't sound good so I commented them out before I get controls for them
// mix oscillators
float sampleOut = (osc1 + osc2*0.25f + osc3*0.125f) * gain;
// TODO: implement gain controls for the other oscillators
float sampleOut = (osc1 + osc2*0.4f + osc3*0.15f) * gain; // pre-filtered noise
// filter sample
float baseFreq = oscillators_[0].frequency();
@@ -109,5 +116,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
float filteredSample = filter1_.biquadProcess(sampleOut);
// sampleOut = filter2_.biquadProcess(sampleOut); // TODO: for some reason second filter is unstable only on windows 🤷
//filteredSample += noise*0.125f; // post-filtered noise
return filteredSample;
}

View File

@@ -12,6 +12,7 @@
// TODO: make configurable
#define SYNTH_OSCILLATOR_COUNT 3
// if there's different oscillator amounts then we need to be able to dynamically create the ui for each of them
struct SmoothedParam {
float current = 0.0f;
@@ -63,7 +64,6 @@ private:
// filters
Filter filter1_;
Filter filter2_;
// TODO: I think the filter's state being uninitialized is what's causing popping when a voice starts for the first time
// paramstore pointer
SmoothedParam* params_;

View File

@@ -17,6 +17,11 @@ void WavetableController::init() {
wavetables_.resize(4); // resize for however many files we find
// don't really know how the files are gonna work
// but I'd like two files- a yaml that contains metadata like name, length, range, datatype, etc.
// and the main data be just a big array of that data type in a binary file
// although having it all in a single bin makes the most sense with the metadata being in the header
float phase = 0.0f;
float phaseInc = 2.0f * M_PI / static_cast<float>(SYNTH_WAVETABLE_SIZE);

View File

@@ -10,7 +10,7 @@ MainWindow::MainWindow(QWidget *parent) :
ui_(new Ui::MainWindow),
config_(ConfigInterface(&params_)),
audio_(new AudioEngine(&config_, &params_)),
keyboard_(audio_->noteQueue()),
keyboard_(audio_->noteQueue(), &config_),
midi_(audio_->noteQueue()) {
// initialize ui
@@ -49,6 +49,67 @@ MainWindow::MainWindow(QWidget *parent) :
audio_->parameters()->set(ParamId::Osc1WaveSelector2, index);
});
// rogue sliders, TODO: clean these up in a package
connect(ui_->sliderMasterOctave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterOctaveOffset, value);
ui_->sliderMasterOctave->setResolution();
});
connect(ui_->sliderMasterSemitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterSemitoneOffset, value);
ui_->sliderMasterSemitone->setResolution();
});
connect(ui_->sliderMasterPitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterPitchOffset, value);
});
connect(ui_->sliderOsc1Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1OctaveOffset, value);
ui_->sliderOsc1Octave->setResolution();
});
connect(ui_->sliderOsc1Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1SemitoneOffset, value);
ui_->sliderOsc1Semitone->setResolution();
});
connect(ui_->sliderOsc1Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1PitchOffset, value);
});
connect(ui_->sliderOsc2Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2OctaveOffset, value);
ui_->sliderOsc2Octave->setResolution();
});
connect(ui_->sliderOsc2Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2SemitoneOffset, value);
ui_->sliderOsc2Semitone->setResolution();
});
connect(ui_->sliderOsc2Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2PitchOffset, value);
});
connect(ui_->sliderOsc3Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3OctaveOffset, value);
ui_->sliderOsc3Octave->setResolution();
});
connect(ui_->sliderOsc3Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3SemitoneOffset, value);
ui_->sliderOsc3Semitone->setResolution();
});
connect(ui_->sliderOsc3Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3PitchOffset, value);
});
// synth business
audio_->start();
@@ -75,14 +136,60 @@ void MainWindow::onResetClicked() {
// initialize to defaults
config_.loadProfile("default");
YAML::Node configRoot = config_.loadProfile("default");
// update ui from the paramstore
ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume, config_.loadEnvProfile("default", "Osc1Volume"));
ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff, config_.loadEnvProfile("default", "FilterCutoff"));
ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance, config_.loadEnvProfile("default", "FilterResonance"));
ui_->comboOsc1WaveSelector1->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector1)].def));
ui_->comboOsc1WaveSelector2->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector2)].def));
// TODO: clean these up, maybe put them in a package like the envelope generators (it'll help encapsulate the int-snapping business)
// what I might do is make a variable-length slider-package object
ui_->sliderMasterOctave->setResolution(configRoot["MasterOctaveOffset"][2].as<int>() - configRoot["MasterOctaveOffset"][1].as<int>());
ui_->sliderMasterOctave->setRange(configRoot["MasterOctaveOffset"][1].as<int>(), configRoot["MasterOctaveOffset"][2].as<int>());
ui_->sliderMasterOctave->setValue(configRoot["MasterOctaveOffset"][0].as<int>());
ui_->sliderMasterSemitone->setResolution(configRoot["MasterSemitoneOffset"][2].as<int>() - configRoot["MasterSemitoneOffset"][1].as<int>());
ui_->sliderMasterSemitone->setRange(configRoot["MasterSemitoneOffset"][1].as<int>(), configRoot["MasterSemitoneOffset"][2].as<int>());
ui_->sliderMasterSemitone->setValue(configRoot["MasterSemitoneOffset"][0].as<int>());
ui_->sliderMasterPitch->setRange(configRoot["MasterPitchOffset"][1].as<float>(), configRoot["MasterPitchOffset"][2].as<float>());
ui_->sliderMasterPitch->setValue(configRoot["MasterPitchOffset"][0].as<float>());
ui_->sliderOsc1Octave->setResolution(configRoot["Osc1OctaveOffset"][2].as<int>() - configRoot["Osc1OctaveOffset"][1].as<int>());
ui_->sliderOsc1Octave->setRange(configRoot["Osc1OctaveOffset"][1].as<int>(), configRoot["Osc1OctaveOffset"][2].as<int>());
ui_->sliderOsc1Octave->setValue(configRoot["Osc1OctaveOffset"][0].as<int>());
ui_->sliderOsc1Semitone->setResolution(configRoot["Osc1SemitoneOffset"][2].as<int>() - configRoot["Osc1SemitoneOffset"][1].as<int>());
ui_->sliderOsc1Semitone->setRange(configRoot["Osc1SemitoneOffset"][1].as<int>(), configRoot["Osc1SemitoneOffset"][2].as<int>());
ui_->sliderOsc1Semitone->setValue(configRoot["Osc1SemitoneOffset"][0].as<int>());
ui_->sliderOsc1Pitch->setRange(configRoot["Osc1PitchOffset"][1].as<float>(), configRoot["Osc1PitchOffset"][2].as<float>());
ui_->sliderOsc1Pitch->setValue(configRoot["Osc1PitchOffset"][0].as<float>());
ui_->sliderOsc2Octave->setResolution(configRoot["Osc2OctaveOffset"][2].as<int>() - configRoot["Osc2OctaveOffset"][1].as<int>());
ui_->sliderOsc2Octave->setRange(configRoot["Osc2OctaveOffset"][1].as<int>(), configRoot["Osc2OctaveOffset"][2].as<int>());
ui_->sliderOsc2Octave->setValue(configRoot["Osc2OctaveOffset"][0].as<int>());
ui_->sliderOsc2Semitone->setResolution(configRoot["Osc2SemitoneOffset"][2].as<int>() - configRoot["Osc2SemitoneOffset"][1].as<int>());
ui_->sliderOsc2Semitone->setRange(configRoot["Osc2SemitoneOffset"][1].as<int>(), configRoot["Osc2SemitoneOffset"][2].as<int>());
ui_->sliderOsc2Semitone->setValue(configRoot["Osc2SemitoneOffset"][0].as<int>());
ui_->sliderOsc2Pitch->setRange(configRoot["Osc2PitchOffset"][1].as<float>(), configRoot["Osc2PitchOffset"][2].as<float>());
ui_->sliderOsc2Pitch->setValue(configRoot["Osc2PitchOffset"][0].as<float>());
ui_->sliderOsc3Octave->setResolution(configRoot["Osc3OctaveOffset"][2].as<int>() - configRoot["Osc3OctaveOffset"][1].as<int>());
ui_->sliderOsc3Octave->setRange(configRoot["Osc3OctaveOffset"][1].as<int>(), configRoot["Osc3OctaveOffset"][2].as<int>());
ui_->sliderOsc3Octave->setValue(configRoot["Osc3OctaveOffset"][0].as<int>());
ui_->sliderOsc3Semitone->setResolution(configRoot["Osc3SemitoneOffset"][2].as<int>() - configRoot["Osc3SemitoneOffset"][1].as<int>());
ui_->sliderOsc3Semitone->setRange(configRoot["Osc3SemitoneOffset"][1].as<int>(), configRoot["Osc3SemitoneOffset"][2].as<int>());
ui_->sliderOsc3Semitone->setValue(configRoot["Osc3SemitoneOffset"][0].as<int>());
ui_->sliderOsc3Pitch->setRange(configRoot["Osc3PitchOffset"][1].as<float>(), configRoot["Osc3PitchOffset"][2].as<float>());
ui_->sliderOsc3Pitch->setValue(configRoot["Osc3PitchOffset"][0].as<float>());
ui_->comboOsc1WaveSelector1->setCurrentIndex(configRoot["OscWaveSelector1"].as<int>());
ui_->comboOsc1WaveSelector2->setCurrentIndex(configRoot["OscWaveSelector2"].as<int>());
}

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1102</width>
<height>564</height>
<width>1100</width>
<height>900</height>
</rect>
</property>
<property name="windowTitle">
@@ -261,6 +261,602 @@
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
<widget class="Line" name="line_3">
<property name="geometry">
<rect>
<x>340</x>
<y>880</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc1Semitone" native="true">
<property name="geometry">
<rect>
<x>410</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_8">
<property name="geometry">
<rect>
<x>490</x>
<y>580</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Pitch</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc1Octave" native="true">
<property name="geometry">
<rect>
<x>340</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_9">
<property name="geometry">
<rect>
<x>410</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Semitone</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="Line" name="line_4">
<property name="geometry">
<rect>
<x>340</x>
<y>600</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc1Pitch" native="true">
<property name="geometry">
<rect>
<x>480</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_6">
<property name="geometry">
<rect>
<x>340</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Octave</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_10">
<property name="geometry">
<rect>
<x>630</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Semitone</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="Line" name="line_5">
<property name="geometry">
<rect>
<x>560</x>
<y>600</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="label_7">
<property name="geometry">
<rect>
<x>560</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Octave</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="Line" name="line_6">
<property name="geometry">
<rect>
<x>560</x>
<y>880</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc2Semitone" native="true">
<property name="geometry">
<rect>
<x>630</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_11">
<property name="geometry">
<rect>
<x>710</x>
<y>580</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Pitch</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc2Octave" native="true">
<property name="geometry">
<rect>
<x>560</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc2Pitch" native="true">
<property name="geometry">
<rect>
<x>700</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc3Semitone" native="true">
<property name="geometry">
<rect>
<x>850</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc3Pitch" native="true">
<property name="geometry">
<rect>
<x>920</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_12">
<property name="geometry">
<rect>
<x>780</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Octave</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="SmartSlider" name="sliderOsc3Octave" native="true">
<property name="geometry">
<rect>
<x>780</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_13">
<property name="geometry">
<rect>
<x>930</x>
<y>580</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Pitch</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="Line" name="line_7">
<property name="geometry">
<rect>
<x>780</x>
<y>600</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="Line" name="line_8">
<property name="geometry">
<rect>
<x>780</x>
<y>880</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="label_14">
<property name="geometry">
<rect>
<x>850</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Semitone</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_15">
<property name="geometry">
<rect>
<x>390</x>
<y>567</y>
<width>111</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Oscillator 1</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_16">
<property name="geometry">
<rect>
<x>611</x>
<y>567</y>
<width>111</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Oscillator 2</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_17">
<property name="geometry">
<rect>
<x>830</x>
<y>569</y>
<width>111</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Oscillator 3</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="Line" name="line_9">
<property name="geometry">
<rect>
<x>120</x>
<y>880</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="label_18">
<property name="geometry">
<rect>
<x>170</x>
<y>567</y>
<width>111</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Master</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_19">
<property name="geometry">
<rect>
<x>270</x>
<y>580</y>
<width>41</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Pitch</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="SmartSlider" name="sliderMasterOctave" native="true">
<property name="geometry">
<rect>
<x>120</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="SmartSlider" name="sliderMasterPitch" native="true">
<property name="geometry">
<rect>
<x>260</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="Line" name="line_10">
<property name="geometry">
<rect>
<x>120</x>
<y>600</y>
<width>211</width>
<height>20</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="label_20">
<property name="geometry">
<rect>
<x>190</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Semitone</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="SmartSlider" name="sliderMasterSemitone" native="true">
<property name="geometry">
<rect>
<x>190</x>
<y>610</y>
<width>65</width>
<height>280</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="label_21">
<property name="geometry">
<rect>
<x>120</x>
<y>580</y>
<width>71</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Octave</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</widget>
</widget>
<customwidgets>
@@ -276,6 +872,12 @@
<header>Scope/Scope.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>SmartSlider</class>
<extends>QWidget</extends>
<header>SmartSlider/SmartSlider.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -75,24 +75,6 @@ void EnvelopeGenerator::emitEnvelope() {
);
}
void EnvelopeGenerator::init(EnvelopeId id) {
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];
ui_->sliderDepth->setRange(PARAM_DEFS[static_cast<size_t>(params.depth)].min, PARAM_DEFS[static_cast<size_t>(params.depth)].max);
ui_->sliderAttack->setRange(PARAM_DEFS[static_cast<size_t>(params.a)].min, PARAM_DEFS[static_cast<size_t>(params.a)].max);
ui_->sliderDecay->setRange(PARAM_DEFS[static_cast<size_t>(params.d)].min, PARAM_DEFS[static_cast<size_t>(params.d)].max);
ui_->sliderSustain->setRange(PARAM_DEFS[static_cast<size_t>(params.s)].min, PARAM_DEFS[static_cast<size_t>(params.s)].max);
ui_->sliderRelease->setRange(PARAM_DEFS[static_cast<size_t>(params.r)].min, PARAM_DEFS[static_cast<size_t>(params.r)].max);
setDepth(PARAM_DEFS[static_cast<size_t>(params.depth)].def);
setAttack(PARAM_DEFS[static_cast<size_t>(params.a)].def);
setDecay(PARAM_DEFS[static_cast<size_t>(params.d)].def);
setSustain(PARAM_DEFS[static_cast<size_t>(params.s)].def);
setRelease(PARAM_DEFS[static_cast<size_t>(params.r)].def);
}
void EnvelopeGenerator::init(EnvelopeId id, std::array<ParamDefault, 5> profile) {
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];

View File

@@ -16,8 +16,7 @@ public:
explicit EnvelopeGenerator(QWidget* parent = nullptr);
~EnvelopeGenerator();
// connects signals, sets parameters to the defaults defined in paramStore
void init(EnvelopeId id);
// connects signals, sets parameters to a provided profile
void init(EnvelopeId id, std::array<ParamDefault, 5> profile);
// setters

View File

@@ -42,6 +42,7 @@ void SmartSlider::setRange(float min, float max) {
ui_->slider->setValue(sliderValue);
ui_->spinValue->setValue(value);
}
// sets value of the slider and the spinBox, called by other classes
@@ -57,6 +58,10 @@ void SmartSlider::setValue(float value) {
emit valueChanged(value);
}
void SmartSlider::setResolution(int resolution) {
sliderResolution_ = resolution;
}
float SmartSlider::value() {
return ui_->spinValue->value();
}

View File

@@ -9,6 +9,7 @@ QT_END_NAMESPACE
// SmartSlider is the widget including a slider, min/max settings, and a value setting parameter
class SmartSlider : public QWidget {
Q_OBJECT
public:
@@ -18,6 +19,8 @@ public:
// setters
void setRange(float min, float max);
void setValue(float value);
void setResolution(int resolution);
void setResolution() { setResolution(static_cast<int>(max_ - min_)); }
// getters
float value();

View File

@@ -19,6 +19,9 @@
<property name="windowTitle">
<string>Form</string>
</property>
<property name="isInt" stdset="0">
<bool>false</bool>
</property>
<widget class="QSlider" name="slider">
<property name="geometry">
<rect>
@@ -62,6 +65,12 @@
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
<widget class="QDoubleSpinBox" name="spinMax">
<property name="geometry">
@@ -84,8 +93,11 @@
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>40000.000000000000000</double>
<double>100000.000000000000000</double>
</property>
</widget>
<widget class="QDoubleSpinBox" name="spinValue">
@@ -117,8 +129,11 @@
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>80000.000000000000000</double>
<double>100000.000000000000000</double>
</property>
</widget>
<widget class="Line" name="line">