diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ae6e1f..9567bd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,19 +63,28 @@ target_sources(${PROJECT_NAME} PRIVATE sources/PluginEditor.h sources/PluginProcessor.cpp sources/PluginProcessor.h + sources/SamplerLookAndFeel.cpp + sources/SamplerLookAndFeel.h + sources/Knob.cpp + sources/Knob.h ) juce_generate_juce_header(${PROJECT_NAME}) juce_add_binary_data(BinaryData SOURCES - sounds/c5.wav - sounds/d5.wav - sounds/e5.wav - sounds/f5.wav - sounds/g5.wav - sounds/a5.wav - sounds/b5.wav - sounds/c6.wav + assets/fonts/Inter-Regular.ttf + assets/fonts/Inter-Medium.ttf + assets/fonts/Inter-SemiBold.ttf + assets/images/BG_1376x1032_1.png + assets/images/Knob_152x132_129.png + assets/sounds/c5.wav + assets/sounds/d5.wav + assets/sounds/e5.wav + assets/sounds/f5.wav + assets/sounds/g5.wav + assets/sounds/a5.wav + assets/sounds/b5.wav + assets/sounds/c6.wav ) target_compile_definitions(${PROJECT_NAME} PUBLIC @@ -89,7 +98,7 @@ target_compile_definitions(${PROJECT_NAME} PUBLIC target_link_libraries(${PROJECT_NAME} PRIVATE - + BinaryData juce::juce_animation juce::juce_audio_utils juce::juce_dsp diff --git a/README.md b/README.md index cddbafa..f5187ef 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,164 @@ -## README - -- Directory structure: - - `sources` has the source files - - `sounds` has the samples as WAV files - -- Using CMake instead of Projucer - - Using CPM rather than submodules to bring in JUCE. It's much easier for beginners to use. - - `juce_add_plugin` section: name and properties of the plug-in. - - `IS_SYNTH` must be TRUE - - `NEEDS_MIDI_INPUT` must be TRUE - - `target_sources` has the .h and .cpp files - - It is possible to let CMake find your source files automatically but I like being explicit. It does mean you need to run `cmake` again whenever you add/remove/rename source files (but that's true for Projucer as well). - - `juce_add_binary_data` has the sound files and images - - Mac: `cmake -B build -G Xcode` (if you use Xcode) - - For release builds, do `cmake -B build -G Xcode -D"CMAKE_OSX_ARCHITECTURES=arm64;x86_64"` to enable both Intel and ARM builds. Without this flag, it only builds for your own type of machine (which is faster for debug builds, which is why we don't put the `CMAKE_OSX_ARCHITECTURES` flag inside CMakeLists.txt). - - Windows: `cmake -B build -G "Visual Studio 17 2022"` - - Added the `**/build` folder to the `.gitignore` file so it doesn't get committed. - - A few notable things inside the CMakeLists.txt file: - - `set(CMAKE_MSVC_RUNTIME_LIBRARY ...`: Will link with the static C++ library rather than the dynamic one (on Windows). - - `# generate PDB/dSYM files for the Release build`: This section enables the creation of PDB (on Windows) and dSYM (on Mac) files that allow us to symbolicate release build crash logs. - - `set_property(GLOBAL PROPERTY USE_FOLDERS YES)` and `option(JUCE_ENABLE_MODULE_SOURCE_GROUPS "Enable Module Source Groups" ON)`: These are optional and only change how the project will look in your IDE. - - The `juce_add_binary_data()` section in the CMake file adds the WAV files to the plug-in. - -- Samples: - - We're adding the WAV files as binary data to the plug-in itself. - - If you have many samples or really large ones, they cannot be added to binary data but must be loaded on-demand. This is a lot more complicated. - - Note that Finder/Explorer shows `a5.wav` and before `c5.wav`, since it doesn't understand that these should be musical notes. - - There is only one velocity per note. - - When you make your own samples, make sure there is no silence at the beginning or end and that all samples are roughly equally loud (although we could fix uneven loudness in code). - -- After running `cmake -B build ...` for the first time, if you open the project in your IDE there may be source files in red, notably `JuceHeader.h`, `BinaryData.h`, and `BinaryData1.cpp` and so on. That's because these files are automatically generated during the build process. After the first build, they will appear as normal and you can open them. - -- Some notes about the `PluginProcessor` code: - - This doesn't use the typical code from the JUCE templates like what you get with Projucer (since we're not using Projucer). I've simplified it a little. - - Currently this is stereo only (see `isBusesLayoutSupported`). Mono wouldn't be too hard to add, since `juce::Synthesiser` supports it already. +# JUCE Sampler Audio Plugin + +A simple but functional sampler plugin built with JUCE 8.0.8, featuring chromatic sampling across multiple octaves with decay and reverb controls. + +## Features + +- 8 chromatic samples (C5 through C6) mapped across the full MIDI range +- Adjustable decay envelope +- Built-in reverb effect +- Custom GUI with filmstrip knobs +- VST3, AU, and Standalone formats + +## Directory Structure + +- `sources/` - Source files (.h and .cpp) +- `assets/` - WAV samples, PNG images, and TTF fonts +- `build/` - Generated build files (not in git) + +## Build Instructions + +### Prerequisites + +- CMake 3.22 or higher +- C++20 compatible compiler +- Xcode (macOS) or Visual Studio 2022 (Windows) + +### macOS + +```bash +# Debug build (native architecture only) +cmake -B build -G Xcode + +# Release build (universal binary: Intel + Apple Silicon) +cmake -B build -G Xcode -D"CMAKE_OSX_ARCHITECTURES=arm64;x86_64" + +# Build from command line +cmake --build build --config Release +``` + +After building, plugins will be automatically copied to: +- AU: `~/Library/Audio/Plug-Ins/Components/` +- VST3: `~/Library/Audio/Plug-Ins/VST3/` +- Standalone: `build/Sampler_artefacts/` + +### Windows + +```bash +# Generate Visual Studio project +cmake -B build -G "Visual Studio 17 2022" + +# Build from command line +cmake --build build --config Release +``` + +Open `build/Sampler.sln` in Visual Studio to build and debug. + +### First Build Notes + +- After running CMake for the first time, generated files like `JuceHeader.h` and `BinaryData.h` may appear in red in your IDE +- These files are created during the build process +- After the first successful build, they will be accessible +- If you add/remove/rename source files, you need to re-run CMake + +## Parameters + +### Decay (0-100%) + +Controls the envelope decay time of the sampler voices. + +- **Range:** 0% to 100% +- **Default:** 0% +- **Behavior:** Uses a non-linear (squared) curve to provide finer control at lower values + - 0% = 0.05s decay time (very short) + - 100% = 1.0s decay time (sustained) +- **Attack:** Fixed at 0.0s (instant) +- **Release:** Fixed at 0.05s + +### Reverb (0-100%) + +Controls the reverb room size. + +- **Range:** 0% to 100% +- **Default:** 50% +- **Behavior:** + - Room size scales linearly with percentage + - Damping: Fixed at 0.5 + - Wet level: Fixed at 0.33 + - Dry level: Fixed at 0.4 +- **Note:** Reverb is always active in the signal chain + +## MIDI Note Mapping + +The plugin uses 8 chromatic samples (C5-C6) distributed across the full MIDI range. Each sample covers multiple keys to create a chromatic scale: + +| Sample | Root Note | MIDI Notes Covered | +|--------|-----------|-------------------| +| **C5** | 72 (C5) | 24, 25, 36, 37, 48, 49, 60, 61, 72, 73 (C1-C#1, C2-C#2, C3-C#3, C4-C#4, C5-C#5) | +| **D5** | 74 (D5) | 26, 27, 38, 39, 50, 51, 62, 63, 74, 75, 86, 87, 98, 99 (D1-D#1, D2-D#2, D3-D#3, D4-D#4, D5-D#5, D6-D#6, D7-D#7) | +| **E5** | 76 (E5) | 28, 40, 52, 64, 76, 88, 100 (E1, E2, E3, E4, E5, E6, E7) | +| **F5** | 77 (F5) | 29, 30, 41, 42, 53, 54, 65, 66, 77, 78, 89, 90, 101, 102 (F1-F#1, F2-F#2, F3-F#3, F4-F#4, F5-F#5, F6-F#6, F7-F#7) | +| **G5** | 79 (G5) | 31, 32, 43, 44, 55, 56, 67, 68, 79, 80, 91, 92, 103, 104 (G1-G#1, G2-G#2, G3-G#3, G4-G#4, G5-G#5, G6-G#6, G7-G#7) | +| **A5** | 81 (A5) | 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 94, 105, 106 (A0-A#0, A1-A#1, A2-A#2, A3-A#3, A4-A#4, A5-A#5, A6-A#6, A7-A#7) | +| **B5** | 83 (B5) | 23, 35, 47, 59, 71, 83, 95, 107 (B0, B1, B2, B3, B4, B5, B6, B7) | +| **C6** | 84 (C6) | 84, 85, 96, 97, 108 (C6-C#6, C7-C#7, C8) | + +**Note:** JUCE's sampler engine automatically pitch-shifts each sample to match the played note, using the root note as the reference pitch. + +## Technical Details + +### Audio Processing + +- **Polyphony:** 8 voices +- **Voice Management:** Standard JUCE voice stealing when polyphony limit is reached +- **Stereo Processing:** Stereo output only (mono not currently supported) +- **Sample Format:** 16-bit WAV files included as binary data + +### CMake Configuration + +This project uses CMake instead of Projucer: + +- **CPM:** Downloads JUCE automatically (no submodules needed) +- **JUCE Version:** 8.0.8 +- **Important flags in CMakeLists.txt:** + - `IS_SYNTH TRUE` - Marks plugin as an instrument + - `NEEDS_MIDI_INPUT TRUE` - Enables MIDI input + - `CMAKE_MSVC_RUNTIME_LIBRARY` - Links with static C++ library on Windows + - PDB/dSYM generation enabled for release builds (allows crash log symbolication) +- **Binary Data:** WAV files, PNG images, and TTF fonts are embedded in the plugin binary via `juce_add_binary_data()` + +### Assets + +**Samples:** +- WAV files are embedded as binary data in the plugin executable +- For projects with many or very large samples, on-demand loading would be required (more complex) +- Current samples have no silence at start/end and are normalized to similar loudness +- Single velocity layer (no velocity sensitivity) +- Note: Finder/Explorer may sort `a5.wav` before `c5.wav` (alphabetical, not musical order) + +**Images and Fonts:** +- Background image and Inter fonts are embedded as binary data +- Plugin is fully self-contained with no external file dependencies at runtime +- All artwork is rendered at 2x resolution: + - Background: 1376×1032 pixels (displayed at 688×516) + - Knob filmstrip: 152×132 pixels per frame, 129 frames total + - 2x assets ensure sharpness on Retina/HiDPI displays +- Inter font family is licensed under SIL Open Font License (royalty-free distribution) + +### Code Structure Notes + +- Simplified `PluginProcessor` implementation (not using standard JUCE/Projucer template) +- Stereo-only output (see `isBusesLayoutSupported()`) +- Mono support could be added as `juce::Synthesiser` already supports it + +## Development Notes + +- The `**/build` folder is in `.gitignore` and won't be committed +- When adding/removing/renaming source files, update `target_sources` in `CMakeLists.txt` and re-run CMake +- Explicit file listing (vs. automatic discovery) provides better control and clarity + +## License + +Check individual asset licenses: +- Inter fonts: SIL Open Font License +- JUCE: GPL/Commercial (see JUCE licensing for distribution) diff --git a/assets/fonts/Inter-Medium.ttf b/assets/fonts/Inter-Medium.ttf new file mode 100644 index 0000000..b53fb1c Binary files /dev/null and b/assets/fonts/Inter-Medium.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..8d4eebf Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/assets/fonts/Inter-SemiBold.ttf b/assets/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000..c6aeeb1 Binary files /dev/null and b/assets/fonts/Inter-SemiBold.ttf differ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..b525cbf --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/images/BG_1376x1032_1.png b/assets/images/BG_1376x1032_1.png new file mode 100644 index 0000000..7174f92 Binary files /dev/null and b/assets/images/BG_1376x1032_1.png differ diff --git a/assets/images/Knob_152x132_129.png b/assets/images/Knob_152x132_129.png new file mode 100644 index 0000000..add13af Binary files /dev/null and b/assets/images/Knob_152x132_129.png differ diff --git a/sounds/a5.wav b/assets/sounds/a5.wav similarity index 100% rename from sounds/a5.wav rename to assets/sounds/a5.wav diff --git a/sounds/b5.wav b/assets/sounds/b5.wav similarity index 100% rename from sounds/b5.wav rename to assets/sounds/b5.wav diff --git a/sounds/c5.wav b/assets/sounds/c5.wav similarity index 100% rename from sounds/c5.wav rename to assets/sounds/c5.wav diff --git a/sounds/c6.wav b/assets/sounds/c6.wav similarity index 100% rename from sounds/c6.wav rename to assets/sounds/c6.wav diff --git a/sounds/d5.wav b/assets/sounds/d5.wav similarity index 100% rename from sounds/d5.wav rename to assets/sounds/d5.wav diff --git a/sounds/e5.wav b/assets/sounds/e5.wav similarity index 100% rename from sounds/e5.wav rename to assets/sounds/e5.wav diff --git a/sounds/f5.wav b/assets/sounds/f5.wav similarity index 100% rename from sounds/f5.wav rename to assets/sounds/f5.wav diff --git a/sounds/g5.wav b/assets/sounds/g5.wav similarity index 100% rename from sounds/g5.wav rename to assets/sounds/g5.wav diff --git a/sounds/A#5.wav b/sounds/A#5.wav deleted file mode 100644 index 0aef255..0000000 Binary files a/sounds/A#5.wav and /dev/null differ diff --git a/sounds/C#5.wav b/sounds/C#5.wav deleted file mode 100644 index 13cf583..0000000 Binary files a/sounds/C#5.wav and /dev/null differ diff --git a/sounds/D#5.wav b/sounds/D#5.wav deleted file mode 100644 index f23d628..0000000 Binary files a/sounds/D#5.wav and /dev/null differ diff --git a/sounds/F#5.wav b/sounds/F#5.wav deleted file mode 100644 index dac7e37..0000000 Binary files a/sounds/F#5.wav and /dev/null differ diff --git a/sounds/G#5.wav b/sounds/G#5.wav deleted file mode 100644 index f267dc4..0000000 Binary files a/sounds/G#5.wav and /dev/null differ diff --git a/sources/Knob.cpp b/sources/Knob.cpp new file mode 100644 index 0000000..6d73c62 --- /dev/null +++ b/sources/Knob.cpp @@ -0,0 +1,30 @@ +// +// Created by Joshua Hodge on 6/22/26. +// + +#include "Knob.h" + +Knob::Knob(const juce::String& text, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID) : + sliderLF(Images::getKnob(), 129), attachment(apvts, parameterID.getParamID(), slider) +{ + setSize(76, 107); + + slider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); + slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, getWidth(), 14); + slider.setLookAndFeel(&sliderLF); + addAndMakeVisible(slider); + + label.setText(text, juce::NotificationType::dontSendNotification); + label.setJustificationType(juce::Justification::horizontallyCentred); + label.setFont(Fonts::getBoldFont()); + label.setColour(juce::Label::textColourId, Colors::textMid); + addAndMakeVisible(label); +} + +void Knob::resized() +{ + label.setBounds(0, 0, getWidth(), 19); + slider.setBounds(0, 19, getWidth(), 88); +} diff --git a/sources/Knob.h b/sources/Knob.h new file mode 100644 index 0000000..2fea1b5 --- /dev/null +++ b/sources/Knob.h @@ -0,0 +1,32 @@ +// +// Created by Joshua Hodge on 6/22/26. +// + +#pragma once + +#include "SamplerLookAndFeel.h" +#include "juce_audio_processors/juce_audio_processors.h" + + +class Knob : public juce::Component +{ +public: + Knob (const juce::String& text, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID); + + ~Knob() override = default; + + void resized() override; + + juce::Slider& getSlider() { return slider; } + juce::Label& getLabel() { return label; } + +private: + KnobLookAndFeel sliderLF; + juce::Slider slider; + juce::Label label; + juce::AudioProcessorValueTreeState::SliderAttachment attachment; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Knob) +}; diff --git a/sources/PluginEditor.cpp b/sources/PluginEditor.cpp index 0dbe0e8..390167e 100644 --- a/sources/PluginEditor.cpp +++ b/sources/PluginEditor.cpp @@ -5,18 +5,18 @@ SamplerAudioProcessorEditor::SamplerAudioProcessorEditor(SamplerAudioProcessor& audioProcessor(p) { setOpaque(true); - setSize(400, 300); -} - -SamplerAudioProcessorEditor::~SamplerAudioProcessorEditor() -{ + setSize(windowWidth, windowHeight); + addAndMakeVisible(decayKnob); + addAndMakeVisible(reverbKnob); } void SamplerAudioProcessorEditor::paint(juce::Graphics& g) { - g.fillAll(juce::Colours::darkgrey); + g.drawImage(Images::getBackground(), { 0, 0, windowWidth, windowHeight }); } void SamplerAudioProcessorEditor::resized() { -} \ No newline at end of file + decayKnob.setTopLeftPosition(451, 91); + reverbKnob.setTopLeftPosition(557, 91); +} diff --git a/sources/PluginEditor.h b/sources/PluginEditor.h index efb4119..492fc01 100644 --- a/sources/PluginEditor.h +++ b/sources/PluginEditor.h @@ -1,19 +1,26 @@ #pragma once #include "juce_gui_basics/juce_gui_basics.h" +#include "SamplerLookAndFeel.h" +#include "Knob.h" #include "PluginProcessor.h" class SamplerAudioProcessorEditor : public juce::AudioProcessorEditor { public: explicit SamplerAudioProcessorEditor(SamplerAudioProcessor&); - ~SamplerAudioProcessorEditor() override; + ~SamplerAudioProcessorEditor() override = default; void paint(juce::Graphics&) override; void resized() override; private: + static constexpr int windowWidth = 688; + static constexpr int windowHeight = 516; SamplerAudioProcessor& audioProcessor; + Knob decayKnob { "Decay", audioProcessor.apvts, "decay" }; + Knob reverbKnob { "Reverb", audioProcessor.apvts, "reverb" }; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SamplerAudioProcessorEditor) }; diff --git a/sources/PluginProcessor.cpp b/sources/PluginProcessor.cpp index 2747c28..8596b81 100644 --- a/sources/PluginProcessor.cpp +++ b/sources/PluginProcessor.cpp @@ -1,10 +1,92 @@ #include "PluginProcessor.h" #include "PluginEditor.h" + +juce::SamplerSound* SamplerAudioProcessor::loadSound(const juce::String& name, + int originalMidiNote, + const std::vector& midiNoteSet, + const void* data, + size_t sizeInBytes) +{ + auto inputStream = std::make_unique(data, sizeInBytes, false); + + if (const auto rawReader = formatManager.createReaderFor(std::move(inputStream))) + { + juce::BigInteger midiNotes; + + for (auto note : midiNoteSet) + { + midiNotes.setBit(note); + } + + // Store the raw readers in our vector of smart audio readers so they outlast this function call + audioReaders.emplace_back(rawReader); + + return new juce::SamplerSound(name, *audioReaders.back(), midiNotes, originalMidiNote, 0.0, 0.1, 10.0); + } + + return nullptr; +} + +void SamplerAudioProcessor::updateDecay() +{ + auto decay = apvts.getRawParameterValue("decay")->load(); + + if (!juce::approximatelyEqual(decay, oldDecay)) + { + auto normalized = decay * 0.01f; + float skew = normalized * normalized; + float decayTime = 0.95f * skew + 0.05f; + + for (int i = 0; i < midiPlaybackEngine.getNumSounds(); ++i) + { + if (auto* sound = dynamic_cast(midiPlaybackEngine.getSound(i).get())) + { + sound->setEnvelopeParameters({ 0.0f, decayTime, 0.0f, 0.05f }); + } + } + + oldDecay = decay; + } +} + +void SamplerAudioProcessor::updateReverb() +{ + float reverbPercent = apvts.getRawParameterValue("reverb")->load(); + + if (!juce::approximatelyEqual(reverbPercent, oldReverb)) + { + juce::Reverb::Parameters reverbParams; + reverbParams.roomSize = reverbPercent * 0.01f; + reverbParams.damping = 0.5f; + reverbParams.wetLevel = 0.33f; + reverbParams.dryLevel = 0.4f; + reverb.setParameters(reverbParams); + + oldReverb = reverbPercent; + } +} + + SamplerAudioProcessor::SamplerAudioProcessor() : AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true)), apvts(*this, nullptr, "Parameters", createParameterLayout()) { + for (int i = 0; i < numVoices; ++i) + { + midiPlaybackEngine.addVoice(new juce::SamplerVoice); + } + + formatManager.registerBasicFormats(); + + midiPlaybackEngine.addSound(loadSound("C5", 72, { 24, 25, 36, 37, 48, 49, 60, 61, 72, 73 }, BinaryData::c5_wav, BinaryData::c5_wavSize)); + midiPlaybackEngine.addSound(loadSound("D5", 74, { 26, 27, 38, 39, 50, 51, 62, 63, 74, 75, 86, 87, 98, 99 }, BinaryData::d5_wav, BinaryData::d5_wavSize)); + midiPlaybackEngine.addSound(loadSound("E5", 76, { 28, 40, 52, 64, 76, 88, 100 }, BinaryData::e5_wav, BinaryData::e5_wavSize)); + midiPlaybackEngine.addSound(loadSound("F5", 77, { 29, 30, 41, 42, 53, 54, 65, 66, 77, 78, 89, 90, 101, 102 }, BinaryData::f5_wav, BinaryData::f5_wavSize)); + midiPlaybackEngine.addSound(loadSound("G5", 79, { 31, 32, 43, 44, 55, 56, 67, 68, 79, 80, 91, 92, 103, 104 }, BinaryData::g5_wav, BinaryData::g5_wavSize)); + midiPlaybackEngine.addSound(loadSound("A5", 81, { 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 94, 105, 106 }, BinaryData::a5_wav, BinaryData::a5_wavSize)); + midiPlaybackEngine.addSound(loadSound("B5", 83, { 23, 35, 47, 59, 71, 83, 95, 107 }, BinaryData::b5_wav, BinaryData::b5_wavSize)); + midiPlaybackEngine.addSound(loadSound("C6", 84, { 84, 85, 96, 97, 108 }, BinaryData::c6_wav, BinaryData::c6_wavSize)); } bool SamplerAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const @@ -14,14 +96,22 @@ bool SamplerAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) c void SamplerAudioProcessor::prepareToPlay(double newSampleRate, int maximumBlockSize) { - juce::ignoreUnused(newSampleRate, maximumBlockSize); + midiPlaybackEngine.setCurrentPlaybackSampleRate(newSampleRate); + reverb.setSampleRate(newSampleRate); + + oldDecay = -1.0f; + oldReverb = -1.0f; } void SamplerAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { - juce::ignoreUnused(midiMessages); juce::ScopedNoDenormals noDenormals; clearUnusedOutputChannels(buffer); + + updateDecay(); + updateReverb(); + midiPlaybackEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + reverb.processStereo(buffer.getWritePointer(0), buffer.getWritePointer(1), buffer.getNumSamples()); } void SamplerAudioProcessor::clearUnusedOutputChannels(juce::AudioBuffer& buffer) const @@ -53,6 +143,25 @@ juce::AudioProcessorEditor* SamplerAudioProcessor::createEditor() juce::AudioProcessorValueTreeState::ParameterLayout SamplerAudioProcessor::createParameterLayout() { juce::AudioProcessorValueTreeState::ParameterLayout layout; + + auto valueToPercentAsString = [](float value, int) { return juce::String(int(value)) + "%"; }; + + layout.add(std::make_unique( + juce::ParameterID { "decay", 1 }, + "Decay", + juce::NormalisableRange(0.0f, 100.0f), + 0.0f, + juce::AudioParameterFloatAttributes(). + withStringFromValueFunction(valueToPercentAsString))); + + layout.add(std::make_unique( + juce::ParameterID { "reverb", 1 }, + "Reverb", + juce::NormalisableRange(0.0f, 100.0f), + 50.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(valueToPercentAsString))); + return layout; } diff --git a/sources/PluginProcessor.h b/sources/PluginProcessor.h index 03f2a95..5ff363e 100644 --- a/sources/PluginProcessor.h +++ b/sources/PluginProcessor.h @@ -1,6 +1,8 @@ #pragma once #include "juce_audio_processors/juce_audio_processors.h" +#include "juce_audio_formats/juce_audio_formats.h" +#include class SamplerAudioProcessor final : public juce::AudioProcessor { @@ -20,7 +22,7 @@ class SamplerAudioProcessor final : public juce::AudioProcessor void changeProgramName(int, const juce::String&) override { } bool isBusesLayoutSupported(const BusesLayout& layouts) const override; - void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void prepareToPlay(double newSampleRate, int samplesPerBlock) override; void releaseResources() override { } void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; void getStateInformation(juce::MemoryBlock& destData) override; @@ -33,5 +35,27 @@ class SamplerAudioProcessor final : public juce::AudioProcessor static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); void clearUnusedOutputChannels(juce::AudioBuffer& buffer) const; + juce::SamplerSound* loadSound(const juce::String& name, + int originalMidiNote, + const std::vector& midiNoteSet, + const void* data, + size_t sizeInBytes); + + void updateDecay(); + void updateReverb(); + + juce::Synthesiser midiPlaybackEngine; + static constexpr auto numVoices = 8; + + juce::AudioFormatManager formatManager; + + // Fixes memory leak with reader in loadSound() + std::vector> audioReaders; + + juce::Reverb reverb; + + float oldDecay = 0.0f; + float oldReverb = 0.0f; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SamplerAudioProcessor) }; diff --git a/sources/SamplerLookAndFeel.cpp b/sources/SamplerLookAndFeel.cpp new file mode 100644 index 0000000..d60bd64 --- /dev/null +++ b/sources/SamplerLookAndFeel.cpp @@ -0,0 +1,30 @@ +// +// Created by Joshua Hodge on 6/22/26. +// + +#include "SamplerLookAndFeel.h" + +KnobLookAndFeel::KnobLookAndFeel(const juce::Image& image, int frames) +: filmstrip(image), numFrames(frames) +{ + +} + +void KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, + [[maybe_unused]] int x, + [[maybe_unused]] int y, + [[maybe_unused]] int width, + [[maybe_unused]] int height, + float sliderPosProportional, + [[maybe_unused]] float rotaryStartAngle, + [[maybe_unused]] float rotaryEndAngle, + [[maybe_unused]] juce::Slider& slider) +{ + const int imageWidth = filmstrip.getWidth(); + const int imageHeight = filmstrip.getHeight() / numFrames; + const int frameIndex = static_cast(sliderPosProportional * static_cast(numFrames - 1)); + + g.drawImage(filmstrip, 0, 0, imageWidth / 2, imageHeight / 2, + 0, frameIndex * imageHeight, imageWidth, imageHeight); +} + diff --git a/sources/SamplerLookAndFeel.h b/sources/SamplerLookAndFeel.h new file mode 100644 index 0000000..5694649 --- /dev/null +++ b/sources/SamplerLookAndFeel.h @@ -0,0 +1,71 @@ +// +// Created by Joshua Hodge on 5/5/26. +// + +#pragma once + +#include +#include +#include + +namespace Colors +{ + const juce::Colour textBright { 209, 209, 209 }; + const juce::Colour textMid { 178, 179, 178 }; + const juce::Colour textDim { 156, 157, 156 }; +} + +namespace Fonts +{ + static const juce::Typeface::Ptr typefaceRegular = juce::Typeface::createSystemTypefaceFor + (BinaryData::InterRegular_ttf, BinaryData::InterRegular_ttfSize); + static const juce::Typeface::Ptr typefaceMedium = juce::Typeface::createSystemTypefaceFor + (BinaryData::InterMedium_ttf, BinaryData::InterMedium_ttfSize); + static const juce::Typeface::Ptr typefaceBold = juce::Typeface::createSystemTypefaceFor + (BinaryData::InterSemiBold_ttf, BinaryData::InterSemiBold_ttfSize); + + inline juce::Font getRegularFont(const float height = 14.0f) + { + return { juce::FontOptions(typefaceRegular).withHeight(height) }; + } + + inline juce::Font getMediumFont(const float height = 14.0f) + { + return { juce::FontOptions(typefaceMedium).withHeight(height) }; + } + + inline juce::Font getBoldFont(const float height = 14.0f) + { + return { juce::FontOptions(typefaceBold).withHeight(height) }; + } +} // namespace Fonts + +namespace Images +{ + inline juce::Image getBackground() + { + return juce::ImageCache::getFromMemory(BinaryData::BG_1376x1032_1_png, BinaryData::BG_1376x1032_1_pngSize); + } + + inline juce::Image getKnob() + { + return juce::ImageCache::getFromMemory(BinaryData::Knob_152x132_129_png, BinaryData::Knob_152x132_129_pngSize); + } +} + +// 1. Now let's set up the drawing behavior for the knob png strip +class KnobLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + KnobLookAndFeel(const juce::Image& image, int numFrames); + + void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height, + float sliderPosProportional, float rotaryStartAngle, + float rotaryEndAngle, juce::Slider&) override; + +private: + juce::Image filmstrip; + int numFrames; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KnobLookAndFeel) +}; \ No newline at end of file