- Tutorial
- Source code
- License
- Contact
Freesound Simple Sampler – JUCE Plugin Tutorial
This tutorial requires some basic knowledge of JUCE and C++.
1. Overview
In this short tutorial, we’ll explore the implementation of the Freesound Simple Sampler developed by António Ramires.
The goal is to learn how to:
- Use the Freesound API inside a JUCE plugin.
- Search, download, and play sounds directly in a plugin environment.
- Manage asynchronous downloads
- Playback via auditioning or MIDI input
You can use the installers available here to try out the plugin before diving into the code.
2. CMake Setup
You can use the provided CMakeLists.txt
file to set up the project. All dependecies are managed via CMake, hence no need to manually install JUCE.
Plugin Attributes
To specify the name of your plugin, as well as the formats you want to build, modify the following section in ./Plugins/FreesoundSimpleSampler/CMakeLists.txt
:
juce_add_plugin("${BaseTargetName}"
COMPANY_NAME "MusicTechnologyGroup"
IS_SYNTH FALSE
NEEDS_MIDI_INPUT TRUE
NEEDS_MIDI_OUTPUT FALSE
IS_MIDI_EFFECT FALSE
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
COPY_PLUGIN_AFTER_BUILD TRUE`
PLUGIN_MANUFACTURER_CODE MTG1
PLUGIN_CODE FrSU
FORMATS AU VST3 Standalone
PRODUCT_NAME "Freesound Simple Sampler")
The resulting plugin will be named Freesound Simple Sampler and will be built in the following formats: AudioUnit (AU), VST3, and Standalone Application. In your host (in my case, Ableton Live), it will appear under the company name MusicTechnologyGroup.
Customizing JUCE Version
If you need to use a specific version of JUCE, modify the following section in ./CMake/Findjuce.cmake
.
if (MSVC)
CPMAddPackage("gh:juce-framework/JUCE#69795dc") # JUCE#69795dc refers to JUCE commit 69795dc on GitHub
elseif (APPLE)
CPMAddPackage("gh:juce-framework/JUCE#develop")
elseif (UNIX)
CPMAddPackage("gh:juce-framework/JUCE#69795dc")
endif ()
The cmake setup is based on Eyal Amir’s Template Available Here repository. I highly recommend checking it out for more details on setting up JUCE projects with CMake. Also, a video tutorial by Eyal is available here.
3. Querying the Freesound API
For this part, refer to ./Plugins/FreesoundSimpleSampler/Source/FreesoundSearchComponent.h
In the plugin, we have a top panel with a TextEditor for entering search queries, a button for initiating the search, and a custom component (ResultsTableComponent
) for displaying the search results.
The ButtonListener callback for the search button is implemented as follows:
void buttonClicked (Button* button) override
{
if (button == &searchButton)
{
Array<FSSound> sounds = searchSounds();
if (sounds.size() == 0) {
AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "No results",
"No sounds found for the query: " + searchInput.getText(true) + ". Possibly no network connection.");
return;
}
...
As you can see, when the button is clicked, we call the searchSounds()
function to perform the search. If no sounds are found, an alert window is displayed to inform the user about the lack of results, possibly due to no network connection.
The searchSounds()
method is the first place where we interact with the Freesound API.
Array<FSSound> searchSounds ()
{
// Makes a query to Freesound to retrieve short sounds using the query text from searchInput label
// Sorts the results randomly and chooses the first 16 to be automatically assinged to the pads
String query = searchInput.getText(true);
FreesoundClient client(FREESOUND_API_KEY);
SoundList list = client.textSearch(query, "duration:[0 TO 0.5]", "score", 1, -1, 150, "id,name,username,license,previews");
Array<FSSound> sounds = list.toArrayOfSounds();
auto num_sounds = sounds.size();
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(sounds.begin(), sounds.end(), g);
// minimum of 16 sounds, or the number of sounds available
sounds.resize(std::min(num_sounds, 16));
// Update results table
searchResults.clearItems();
for (int i=0; i<sounds.size(); i++){
FSSound sound = sounds[i];
StringArray soundData;
soundData.add(sound.name);
soundData.add(sound.user);
soundData.add(sound.license);
searchResults.addRowData(soundData);
}
searchResults.updateContent();
return sounds;
}
FreesoundClient
To search for sounds, we first create an instance of the FreesoundClient
class, passing our API key as a parameter. This class is responsible for handling all interactions with the Freesound API.
FreesoundClient client(FREESOUND_API_KEY);
Do not hardcode your API key in production code. After completing the CMake setup, there will be a header file generated in ./Plugins/FreesoundSimpleSampler/Source/FreesoundKeys.h
.
Make sure to add this file to your .gitignore
to avoid exposing your API key in public repositories.
Performing the Search
Next, we call the textSearch()
method of the FreesoundClient
instance to perform the search. This method takes several parameters:
SoundList list = client.textSearch(query, "duration:[0 TO 0.5]", "score", 1, -1, 150, "id,name,username,license,previews");
The parameters passed to textSearch()
are as follows:
query
: The search query entered by the user.filter
: A filter to limit the search results. In this case, we are filtering for sounds with a duration between 0 and 0.5 seconds.sort
: The sorting method for the results. Here, we are sorting by score.page
: The page number of the results to retrieve. We are retrieving the first page.page_size
: The number of results per page. We are retrieving up to 150 results.fields
: The fields to include in the response. We are requesting theid
,name
,username
,license
, andpreviews
fields.
Handling the Results
The textSearch()
method returns a SoundList
object containing the search results. We then convert this list to an array of FSSound
objects using the toArrayOfSounds()
method.
This will allow us to easily check the number of results and access individual sound properties.
Array<FSSound> sounds = list.toArrayOfSounds();
auto num_sounds = sounds.size();
Randomizing and Populating the Results Table
To add some variability to the results, we shuffle the array of sounds randomly and resize it to a maximum of 16 sounds (or fewer if there are not enough results).
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(sounds.begin(), sounds.end(), g);
// minimum of 16 sounds, or the number of sounds available
sounds.resize(std::min(num_sounds, 16));
Finally, we update the ResultsTableComponent
with the names, usernames, and licenses of the retrieved sounds.
// Update results table
searchResults.clearItems();
for (int i=0; i<sounds.size(); i++){
FSSound sound = sounds[i];
StringArray soundData;
soundData.add(sound.name);
soundData.add(sound.user);
soundData.add(sound.license);
searchResults.addRowData(soundData);
}
searchResults.updateContent();
We are not downloading any sounds here. We have so far only selected the sounds we want to download and play later. The method returns the resulting sounds and we notify the processor to download them asynchronously and prepare the playback samplers.
4. Asynchronous Downloading of Selected Sounds from Freesound
After the execution of the searchSounds()
method, the selected sounds are passed to the audio processor via the newSoundsReady()
method.
void buttonClicked (Button* button) override
{
if (button == &searchButton)
{
Array<FSSound> sounds = searchSounds();
// ...
processor->newSoundsReady(sounds, searchInput.getText(true), searchResults.getData()); # <------ Here
}
}
Let’s take a look at the newSoundsReady()
method in the processor. The newSoundsReady()
initiates a chain of callbacks that eventually leads to a background thread downloading the selected sounds (thread implementation in Plugins/FreesoundSimpleSampler/Source/AudioDownloadManager.h
).
void FreesoundSimpleSamplerAudioProcessor::newSoundsReady (Array<FSSound> sounds, String textQuery, std::vector<juce::StringArray> soundInfo)
{
// ...
// Start downloads using the new download manager
startDownloads(sounds);
}
void FreesoundSimpleSamplerAudioProcessor::startDownloads(const Array<FSSound>& sounds)
{
// ...
downloadManager.startDownloads(sounds, tmpDownloadLocation);
}
void AudioDownloadManager::startDownloads(const juce::Array<FSSound>& sounds, const juce::File& downloadDirectory)
{
// ...
startThread();
// ...
}
The threading in JUCE is such that when you call startThread()
, the run()
method of the thread class is executed in a separate thread. The actual downloading of sounds happens in the run()
method of the AudioDownloadManager
class.
void AudioDownloadManager::run()
{
for (int i = 0; i < soundsToDownload.size() && !threadShouldExit(); ++i) // iterate over sounds to download
{
// Create web input stream
juce::URL downloadUrl = url;
currentStream = std::make_unique<juce::WebInputStream>(downloadUrl, false);
if (currentStream->connect(nullptr))
{
// Get file size if available
int64 totalSize = currentStream->getTotalLength();
// ...
// Create output stream
std::unique_ptr<juce::FileOutputStream> output = currentOutputFile.createOutputStream();
if (output != nullptr)
{
const int bufferSize = 8192;
juce::HeapBlock<char> buffer(bufferSize);
// ...
while (!currentStream->isExhausted() && !threadShouldExit()) // keep downloading and write to the dedicated temporary file
{
int bytesRead = currentStream->read(buffer, bufferSize);
if (bytesRead > 0)
{
output->write(buffer, bytesRead);
// ...
}
else if (bytesRead == 0)
{
break; // End of stream
}
else
{
allSuccessful = false;
break; // Error
}
}
output->flush();
}
listeners.call([allSuccessful](Listener& l) { l.downloadCompleted(allSuccessful); }); <--- notify listener (processor) that download is complete
As mentioned above, we can only download previews without user authentication. For this reason, we are using the getOGGPreviewURL()
method of the FSSound
class to get the URL of the OGG preview of the sound.
In the implementation, we also handle progress updates and error handling, but for brevity, I have omitted those parts here. You can refer to the full implementation in AudioDownloadManager.h
and AudioDownloadManager.cpp
.
5. Playback of Downloaded Sounds
The sounds are downloaded in a temporary directory, and once the download is complete, the processor will load the sounds into dedicated samplers for playback.
void FreesoundSimpleSamplerAudioProcessor::downloadCompleted(bool success)
{
if (success)
{
// Set up the sampler with the downloaded files
setSources();
}
// Forward to editor listeners (so that sounds can be auditioned by clicking on the pads)
downloadListeners.call([success](DownloadListener& l) {
l.downloadCompleted(success);
});
}
The setSources()
method is responsible for loading the downloaded sounds into the samplers.
void FreesoundSimpleSamplerAudioProcessor::setSources()
{
// Clear existing sounds and voices before adding new ones
sampler.clearSounds();
sampler.clearVoices();
int poliphony = 16;
int maxLength = 10;
// Add voices
for (int i = 0; i < poliphony; i++) {
sampler.addVoice(new SamplerVoice());
}
if(audioFormatManager.getNumKnownFormats() == 0){
audioFormatManager.registerBasicFormats();
}
Array<File> files = tmpDownloadLocation.findChildFiles(2, false);
for (int i = 0; i < files.size(); i++) {
std::unique_ptr<AudioFormatReader> reader(audioFormatManager.createReaderFor(files[i]));
if (reader != nullptr) // Add null check for safety
{
BigInteger notes;
notes.setRange(i * 8, i * 8 + 7, true);
sampler.addSound(new SamplerSound(String(i), *reader, notes, i*8, 0, maxLength, maxLength));
}
}
}
6. Auditioning and MIDI Playback
The plugin allows users to audition sounds by clicking on the pads in the GUI or by playing MIDI notes.
void FreesoundSimpleSamplerAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
midiMessages.addEvents(midiFromEditor, 0, INT_MAX, 0); <--- Add MIDI events from editor (for auditioning by clicking on pads)
midiFromEditor.clear();
sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); <--- Render audio from sampler based on MIDI events
midiMessages.clear();
}
What’s Next?
In the next tutorial, we will learn how to allow users to login to their Freesound accounts from within the plugin, and how to access their accounts.
The source code for this tutorial is available at https://github.com/behzadhaki/Freesound-Juce-API
Specifically, you can find the code in the Plugins/FreesoundSimpleSampler/Source
directory.
Freesound API License
MIT License
Copyright (c) 2019 Music Technology Group - Universitat Pompeu Fabra
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
JUCE License
Refer to the JUCE license terms at https://juce.com/get-juce/ for details.
Keep in mind that you should ensure to comply with the licenses of JUCE or any other third-party libraries you may use in your project.
Support, Bug Reports, and Feature Requests
If you have issues compiling the source code used in this tutorial, please contact us through https://github.com/behzadhaki/Freesound-Juce-API/issues.
If you prefer to contact us directly, use the contact information below.
Questions, Suggestions, or Any Other Inquiries
If you prefer to contact us directly, please contact Behzad Haki.