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/Voice.h
src/synth/WavetableController.cpp src/synth/WavetableController.cpp
src/synth/WavetableController.h src/synth/WavetableController.h
resources/resources.qrc
src/ui/widgets/SmartSlider/SmartSlider.cpp src/ui/widgets/SmartSlider/SmartSlider.cpp
src/ui/widgets/SmartSlider/SmartSlider.h src/ui/widgets/SmartSlider/SmartSlider.h
src/ui/widgets/SmartSlider/SmartSlider.ui 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 - [x] Create oscillator class where the actual tone generation occurs. Multiple
oscillators increase the sound complexity considerably oscillators increase the sound complexity considerably
- [x] Create a UI scope to visualize the synthesized composite waveform - [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 - [ ] Wavetable file loading
- [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance) - [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance)
- [x] Add polyphony somewhere. Probably involves a voice class. If processing power - [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), - [ ] Filter upgrades including some more complex solving techniques (State Variable Filter),
better key tracking, more natural envelope curves, filter drive, etc. better key tracking, more natural envelope curves, filter drive, etc.
- [ ] Reverb (quite a few ways to do this, needs more research) - [ ] 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) - [ ] 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 engine config, etc. from a yaml instead of being hardcoded
- [ ] Noise - [ ] Noise
- [ ] LFO modulation - [ ] LFO modulation

View File

@@ -3,7 +3,7 @@
# Configures properties for the RtAudio engine # Configures properties for the RtAudio engine
# Number of samples per second # Number of samples per second
sampleRate: 44100 sampleRate: 44100
# unconfigurable: sampleFormat; [-1, 1] float # unconfigurable: sampleFormat; [-1, 1] float
# number of audio channels # 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] # sequences in the form [x, x, x] denote [setValue, sliderMinimum, sliderMaximum]
version: 0x0002 version: 0x0003
# deprecated, useless # deprecated, useless
Osc1Freq: [100, 20, 600] Osc1Freq: [100, 20, 600]
# wavetable selections # wavetable selections
OscWaveSelector1: 2 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] Osc1OctaveOffset: [0, -5, 5]
Osc1SemitoneOffset: [0, -12, 12] Osc1SemitoneOffset: [0, -12, 12]
Osc1PitchOffset: [0, -100, 100] Osc1PitchOffset: [1.34, -100, 100]
Osc2OctaveOffset: [1, -5, 5] Osc2OctaveOffset: [1, -5, 5]
Osc2SemitoneOffset: [0, -12, 12] Osc2SemitoneOffset: [0, -12, 12]
Osc2PitchOffset: [0, -100, 100] Osc2PitchOffset: [12.86, -100, 100]
Osc3OctaveOffset: [1, -5, 5] Osc3OctaveOffset: [1, -5, 5]
Osc3SemitoneOffset: [7, -12, 12] 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 # Envelope generator parameters
Osc1Volume: Osc1Volume:
- [1, 0, 2] # Depth - [1, 0, 10] # Depth
- [0.05, 0, 2] # Attack - [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay - [0.2, 0, 2] # Decay
- [0.7, 0, 1] # Sustain - [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 // ugly but if it works it works
void ConfigInterface::loadProfile(std::string filename) { YAML::Node ConfigInterface::loadProfile(std::string filename) {
// load file // load file
std::string filepath = "config/profiles/" + filename + ".yaml"; std::string filepath = "config/profiles/" + filename + ".yaml";
@@ -57,16 +57,16 @@ void ConfigInterface::loadProfile(std::string filename) {
config = YAML::LoadFile(filepath); config = YAML::LoadFile(filepath);
} catch(const std::exception& e) { } catch(const std::exception& e) {
std::cerr << e.what() << std::endl; std::cerr << e.what() << std::endl;
return; return config;
} }
// check version // check version
int version = config["version"].as<int>(); // yaml-cpp parses unquoted hex as integers int version = config["version"].as<int>(); // yaml-cpp parses unquoted hex as integers
if(version < CONFIG_VERSION) { if(version < CONFIG_VERSION) {
std::cout << "Parameter profile version " << version << "is outdated below the compatible version " << CONFIG_VERSION << std::endl; std::cout << "Parameter profile version " << version << "is outdated below the compatible version " << CONFIG_VERSION << std::endl;
return; return config;
} else { } else {
std::cout << version << std::endl; std::cout << "Parameter profile version " << version << std::endl;
} }
// extract values from the config file // 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> fCutoffProfile = loadEnvProfile(&config, "FilterCutoff");
std::array<ParamDefault, 5> fResonanceProfile = loadEnvProfile(&config, "FilterResonance"); 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 // TODO: remove this once all the parameters are set properly
params_->resetToDefaults(); 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::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::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); 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 ? // TODO: why do I bother passing in 5 values independently when I can just do an array like in loadEnvProfile ?
// VVV look down there its so easy 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: // TODO:
// load wavetable settings // load wavetable settings
// load oscillator pitch settings // load oscillator pitch settings
return config;
} }
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) { 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); 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(YAML::Node* node, std::string profile);
std::array<ParamDefault, 5> loadEnvProfile(std::string filename, std::string profile); std::array<ParamDefault, 5> loadEnvProfile(std::string filename, std::string profile);

View File

@@ -2,38 +2,40 @@
#include "KeyboardController.h" #include "KeyboardController.h"
#include <iostream> #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) { void KeyboardController::handleKeyPress(QKeyEvent* e) {

View File

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

View File

@@ -9,6 +9,9 @@ enum class ParamId : uint16_t {
Osc1Frequency, Osc1Frequency,
Osc1WaveSelector1, Osc1WaveSelector1,
Osc1WaveSelector2, Osc1WaveSelector2,
MasterOctaveOffset,
MasterSemitoneOffset,
MasterPitchOffset,
Osc1OctaveOffset, Osc1OctaveOffset,
Osc1SemitoneOffset, Osc1SemitoneOffset,
Osc1PitchOffset, Osc1PitchOffset,
@@ -69,12 +72,13 @@ struct ParamDefault {
float max; 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 {{ constexpr std::array<ParamDefault, static_cast<size_t>(ParamId::Count)> PARAM_DEFS {{
{ 100.0f, 20.0f, 600.0f}, // Osc1Freq { 100.0f, 20.0f, 600.0f}, // Osc1Freq
{ 2.0f, 0.0f, 0.0f}, // OscWaveSelector1 { 2.0f, 0.0f, 0.0f}, // OscWaveSelector1
{ 1.0f, 0.0f, 0.0f}, // OscWaveSelector2 { 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, -5.0f, 5.0f}, // Osc1OctaveOffset
{ 0.0f, -12.0f, 12.0f}, // Osc1SemitoneOffset { 0.0f, -12.0f, 12.0f}, // Osc1SemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // Osc1PitchOffset { 0.0f, -100.0f, 100.0f}, // Osc1PitchOffset

View File

@@ -35,9 +35,17 @@ bool AudioEngine::start() {
RtAudio::StreamOptions options; RtAudio::StreamOptions options;
options.flags = RTAUDIO_MINIMIZE_LATENCY; options.flags = RTAUDIO_MINIMIZE_LATENCY;
// TODO: error check this please RtAudioErrorType status = audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options); if(status != RTAUDIO_NO_ERROR) {
audio_.startStream(); 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 // sanity check
std::cout << "sample rate: " << sampleRate_ << " buffer frames: " << bufferFrames_ << std::endl; 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 ScopeBuffer scope_ { 1024 }; // stores audio samples for visualization
RtAudio audio_{AUDIO_API}; // audio device 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 sampleRate_ = 44100;
uint32_t bufferFrames_ = 512; // time per buffer = BF/SR (256/44100 = 5.8ms) uint32_t bufferFrames_ = 512; // time per buffer = BF/SR (256/44100 = 5.8ms)
uint32_t channels_ = 2; // stereo uint32_t channels_ = 2; // stereo

View File

@@ -2,6 +2,7 @@
#include "Voice.h" #include "Voice.h"
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
#include <random>
Voice::Voice(SmoothedParam* params, WavetableController* wavetable) : params_(params), wavetable_(wavetable) { 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 // calculate the note/pitch of the oscillators
bool temp = false; bool temp = false;
uint8_t osc1NoteOffset = static_cast<uint8_t>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc1OctaveOffset) + getParam(ParamId::Osc1SemitoneOffset)); 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>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc2OctaveOffset) + getParam(ParamId::Osc2SemitoneOffset)); 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>((SYNTH_NOTES_PER_OCTAVE+1) * getParam(ParamId::Osc3OctaveOffset) + getParam(ParamId::Osc3SemitoneOffset)); 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 // sample oscillators
float osc1 = oscillators_[0].process(osc1NoteOffset + note_, getParam(ParamId::Osc1PitchOffset)/100.0f, scopeTrigger); 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)/100.0f, temp); 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)/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 // 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 // filter sample
float baseFreq = oscillators_[0].frequency(); float baseFreq = oscillators_[0].frequency();
@@ -109,5 +116,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
float filteredSample = filter1_.biquadProcess(sampleOut); float filteredSample = filter1_.biquadProcess(sampleOut);
// sampleOut = filter2_.biquadProcess(sampleOut); // TODO: for some reason second filter is unstable only on windows 🤷 // sampleOut = filter2_.biquadProcess(sampleOut); // TODO: for some reason second filter is unstable only on windows 🤷
//filteredSample += noise*0.125f; // post-filtered noise
return filteredSample; return filteredSample;
} }

View File

@@ -12,6 +12,7 @@
// TODO: make configurable // TODO: make configurable
#define SYNTH_OSCILLATOR_COUNT 3 #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 { struct SmoothedParam {
float current = 0.0f; float current = 0.0f;
@@ -63,7 +64,6 @@ private:
// filters // filters
Filter filter1_; Filter filter1_;
Filter filter2_; 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 // paramstore pointer
SmoothedParam* params_; SmoothedParam* params_;

View File

@@ -17,6 +17,11 @@ void WavetableController::init() {
wavetables_.resize(4); // resize for however many files we find 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 phase = 0.0f;
float phaseInc = 2.0f * M_PI / static_cast<float>(SYNTH_WAVETABLE_SIZE); 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), ui_(new Ui::MainWindow),
config_(ConfigInterface(&params_)), config_(ConfigInterface(&params_)),
audio_(new AudioEngine(&config_, &params_)), audio_(new AudioEngine(&config_, &params_)),
keyboard_(audio_->noteQueue()), keyboard_(audio_->noteQueue(), &config_),
midi_(audio_->noteQueue()) { midi_(audio_->noteQueue()) {
// initialize ui // initialize ui
@@ -49,6 +49,67 @@ MainWindow::MainWindow(QWidget *parent) :
audio_->parameters()->set(ParamId::Osc1WaveSelector2, index); 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 // synth business
audio_->start(); audio_->start();
@@ -75,14 +136,60 @@ void MainWindow::onResetClicked() {
// initialize to defaults // initialize to defaults
config_.loadProfile("default"); YAML::Node configRoot = config_.loadProfile("default");
// update ui from the paramstore // update ui from the paramstore
ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume, config_.loadEnvProfile("default", "Osc1Volume")); ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume, config_.loadEnvProfile("default", "Osc1Volume"));
ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff, config_.loadEnvProfile("default", "FilterCutoff")); ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff, config_.loadEnvProfile("default", "FilterCutoff"));
ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance, config_.loadEnvProfile("default", "FilterResonance")); ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance, config_.loadEnvProfile("default", "FilterResonance"));
ui_->comboOsc1WaveSelector1->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector1)].def)); // TODO: clean these up, maybe put them in a package like the envelope generators (it'll help encapsulate the int-snapping business)
ui_->comboOsc1WaveSelector2->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector2)].def)); // 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> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1102</width> <width>1100</width>
<height>564</height> <height>900</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -261,6 +261,602 @@
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set> <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property> </property>
</widget> </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>
</widget> </widget>
<customwidgets> <customwidgets>
@@ -276,6 +872,12 @@
<header>Scope/Scope.h</header> <header>Scope/Scope.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>SmartSlider</class>
<extends>QWidget</extends>
<header>SmartSlider/SmartSlider.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>
<connections/> <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) { void EnvelopeGenerator::init(EnvelopeId id, std::array<ParamDefault, 5> profile) {
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)]; EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];

View File

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

View File

@@ -42,6 +42,7 @@ void SmartSlider::setRange(float min, float max) {
ui_->slider->setValue(sliderValue); ui_->slider->setValue(sliderValue);
ui_->spinValue->setValue(value); ui_->spinValue->setValue(value);
} }
// sets value of the slider and the spinBox, called by other classes // sets value of the slider and the spinBox, called by other classes
@@ -57,6 +58,10 @@ void SmartSlider::setValue(float value) {
emit valueChanged(value); emit valueChanged(value);
} }
void SmartSlider::setResolution(int resolution) {
sliderResolution_ = resolution;
}
float SmartSlider::value() { float SmartSlider::value() {
return ui_->spinValue->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 // SmartSlider is the widget including a slider, min/max settings, and a value setting parameter
class SmartSlider : public QWidget { class SmartSlider : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
@@ -18,6 +19,8 @@ public:
// setters // setters
void setRange(float min, float max); void setRange(float min, float max);
void setValue(float value); void setValue(float value);
void setResolution(int resolution);
void setResolution() { setResolution(static_cast<int>(max_ - min_)); }
// getters // getters
float value(); float value();

View File

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