Merge pull request #267 from jpcima/quality-api
Add the sample quality API
This commit is contained in:
commit
9e764b132a
14 changed files with 249 additions and 9 deletions
34
src/sfizz.h
34
src/sfizz.h
|
|
@ -40,6 +40,13 @@ typedef enum {
|
||||||
SFIZZ_OVERSAMPLING_X4 = 4,
|
SFIZZ_OVERSAMPLING_X4 = 4,
|
||||||
SFIZZ_OVERSAMPLING_X8 = 8
|
SFIZZ_OVERSAMPLING_X8 = 8
|
||||||
} sfizz_oversampling_factor_t;
|
} sfizz_oversampling_factor_t;
|
||||||
|
/**
|
||||||
|
* @brief Processing mode
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
SFIZZ_PROCESS_LIVE,
|
||||||
|
SFIZZ_PROCESS_FREEWHEELING,
|
||||||
|
} sfizz_process_mode_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates a sfizz synth. This object has to be freed by the caller
|
* @brief Creates a sfizz synth. This object has to be freed by the caller
|
||||||
|
|
@ -366,6 +373,33 @@ SFIZZ_EXPORTED_API sfizz_oversampling_factor_t sfizz_get_oversampling_factor(sfi
|
||||||
*/
|
*/
|
||||||
SFIZZ_EXPORTED_API bool sfizz_set_oversampling_factor(sfizz_synth_t* synth, sfizz_oversampling_factor_t oversampling);
|
SFIZZ_EXPORTED_API bool sfizz_set_oversampling_factor(sfizz_synth_t* synth, sfizz_oversampling_factor_t oversampling);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the default resampling quality. This is the quality setting
|
||||||
|
* which the engine uses when the instrument does not use the
|
||||||
|
* opcode `sample_quality`. The engine uses distinct default quality
|
||||||
|
* settings for live mode and freewheeling mode, which both can be
|
||||||
|
* accessed by the means of this function.
|
||||||
|
*
|
||||||
|
* @param synth The synth.
|
||||||
|
* @param[in] mode The processing mode.
|
||||||
|
*
|
||||||
|
* @return The sample quality for the given mode, in the range 1 to 10.
|
||||||
|
*/
|
||||||
|
SFIZZ_EXPORTED_API int sfizz_get_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the default resampling quality. This is the quality setting
|
||||||
|
* which the engine uses when the instrument does not use the
|
||||||
|
* opcode `sample_quality`. The engine uses distinct default quality
|
||||||
|
* settings for live mode and freewheeling mode, which both can be
|
||||||
|
* accessed by the means of this function.
|
||||||
|
*
|
||||||
|
* @param synth The synth.
|
||||||
|
* @param[in] mode The processing mode.
|
||||||
|
* @param[in] quality The desired sample quality, in the range 1 to 10.
|
||||||
|
*/
|
||||||
|
SFIZZ_EXPORTED_API void sfizz_set_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode, int quality);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set the global instrument volume.
|
* @brief Set the global instrument volume.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@ public:
|
||||||
Sfizz();
|
Sfizz();
|
||||||
~Sfizz();
|
~Sfizz();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Processing mode
|
||||||
|
*/
|
||||||
|
enum ProcessMode {
|
||||||
|
ProcessLive,
|
||||||
|
ProcessFreewheeling,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Empties the current regions and load a new SFZ file into the synth.
|
* @brief Empties the current regions and load a new SFZ file into the synth.
|
||||||
*
|
*
|
||||||
|
|
@ -174,6 +182,31 @@ public:
|
||||||
*/
|
*/
|
||||||
void setSampleRate(float sampleRate) noexcept;
|
void setSampleRate(float sampleRate) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the default resampling quality. This is the quality setting
|
||||||
|
* which the engine uses when the instrument does not use the
|
||||||
|
* opcode `sample_quality`. The engine uses distinct default quality
|
||||||
|
* settings for live mode and freewheeling mode, which both can be
|
||||||
|
* accessed by the means of this function.
|
||||||
|
*
|
||||||
|
* @param[in] mode The processing mode.
|
||||||
|
*
|
||||||
|
* @return The sample quality for the given mode, in the range 1 to 10.
|
||||||
|
*/
|
||||||
|
int getSampleQuality(ProcessMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the default resampling quality. This is the quality setting
|
||||||
|
* which the engine uses when the instrument does not use the
|
||||||
|
* opcode `sample_quality`. The engine uses distinct default quality
|
||||||
|
* settings for live mode and freewheeling mode, which both can be
|
||||||
|
* accessed by the means of this function.
|
||||||
|
*
|
||||||
|
* @param[in] mode The processing mode.
|
||||||
|
* @param[in] quality The desired sample quality, in the range 1 to 10.
|
||||||
|
*/
|
||||||
|
void setSampleQuality(ProcessMode mode, int quality);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return the current value for the volume, in dB.
|
* @brief Return the current value for the volume, in dB.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ namespace Default
|
||||||
|
|
||||||
// ***** SFZ v2 ********
|
// ***** SFZ v2 ********
|
||||||
constexpr int sampleQuality { 2 };
|
constexpr int sampleQuality { 2 };
|
||||||
|
constexpr int sampleQualityInFreewheelingMode { 10 }; // for future use, possibly excessive
|
||||||
constexpr Range<int> sampleQualityRange { 1, 10 }; // sample_quality
|
constexpr Range<int> sampleQualityRange { 1, 10 }; // sample_quality
|
||||||
|
|
||||||
constexpr bool checkSustain { true }; // sustain_sw
|
constexpr bool checkSustain { true }; // sustain_sw
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
break;
|
break;
|
||||||
case hash("sample_quality"):
|
case hash("sample_quality"):
|
||||||
{
|
{
|
||||||
setValueFromOpcode(opcode, sampleQuality, Default::sampleQualityRange);
|
if (opcode.value == "-1")
|
||||||
|
sampleQuality.reset();
|
||||||
|
else
|
||||||
|
setValueFromOpcode(opcode, sampleQuality, Default::sampleQualityRange);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ struct Region {
|
||||||
|
|
||||||
// Sound source: sample playback
|
// Sound source: sample playback
|
||||||
FileId sampleId {}; // Sample
|
FileId sampleId {}; // Sample
|
||||||
int sampleQuality { Default::sampleQuality };
|
absl::optional<int> sampleQuality {};
|
||||||
float delay { Default::delay }; // delay
|
float delay { Default::delay }; // delay
|
||||||
float delayRandom { Default::delayRandom }; // delay_random
|
float delayRandom { Default::delayRandom }; // delay_random
|
||||||
int64_t offset { Default::offset }; // offset
|
int64_t offset { Default::offset }; // offset
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "SynthConfig.h"
|
||||||
#include "FilePool.h"
|
#include "FilePool.h"
|
||||||
#include "BufferPool.h"
|
#include "BufferPool.h"
|
||||||
#include "FilterPool.h"
|
#include "FilterPool.h"
|
||||||
|
|
@ -21,6 +22,7 @@ class WavetableMulti;
|
||||||
|
|
||||||
struct Resources
|
struct Resources
|
||||||
{
|
{
|
||||||
|
SynthConfig synthConfig;
|
||||||
BufferPool bufferPool;
|
BufferPool bufferPool;
|
||||||
MidiState midiState;
|
MidiState midiState;
|
||||||
Logger logger;
|
Logger logger;
|
||||||
|
|
|
||||||
|
|
@ -667,7 +667,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
buffer.fill(0.0f);
|
buffer.fill(0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (freeWheeling)
|
if (resources.synthConfig.freeWheeling)
|
||||||
resources.filePool.waitForBackgroundLoading();
|
resources.filePool.waitForBackgroundLoading();
|
||||||
|
|
||||||
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
const std::unique_lock<std::mutex> lock { callbackGuard, std::try_to_lock };
|
||||||
|
|
@ -1112,6 +1112,37 @@ size_t sfz::Synth::getNumPreloadedSamples() const noexcept
|
||||||
return resources.filePool.getNumPreloadedSamples();
|
return resources.filePool.getNumPreloadedSamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sfz::Synth::getSampleQuality(ProcessMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case ProcessLive:
|
||||||
|
return resources.synthConfig.liveSampleQuality;
|
||||||
|
case ProcessFreewheeling:
|
||||||
|
return resources.synthConfig.freeWheelingSampleQuality;
|
||||||
|
default:
|
||||||
|
CHECK(false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::Synth::setSampleQuality(ProcessMode mode, int quality)
|
||||||
|
{
|
||||||
|
CHECK(quality >= 1 && quality <= 10);
|
||||||
|
quality = clamp(quality, 1, 10);
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case ProcessLive:
|
||||||
|
resources.synthConfig.liveSampleQuality = quality;
|
||||||
|
break;
|
||||||
|
case ProcessFreewheeling:
|
||||||
|
resources.synthConfig.freeWheelingSampleQuality = quality;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
CHECK(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float sfz::Synth::getVolume() const noexcept
|
float sfz::Synth::getVolume() const noexcept
|
||||||
{
|
{
|
||||||
return volume;
|
return volume;
|
||||||
|
|
@ -1200,15 +1231,15 @@ uint32_t sfz::Synth::getPreloadSize() const noexcept
|
||||||
|
|
||||||
void sfz::Synth::enableFreeWheeling() noexcept
|
void sfz::Synth::enableFreeWheeling() noexcept
|
||||||
{
|
{
|
||||||
if (!freeWheeling) {
|
if (!resources.synthConfig.freeWheeling) {
|
||||||
freeWheeling = true;
|
resources.synthConfig.freeWheeling = true;
|
||||||
DBG("Enabling freewheeling");
|
DBG("Enabling freewheeling");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void sfz::Synth::disableFreeWheeling() noexcept
|
void sfz::Synth::disableFreeWheeling() noexcept
|
||||||
{
|
{
|
||||||
if (freeWheeling) {
|
if (resources.synthConfig.freeWheeling) {
|
||||||
freeWheeling = false;
|
resources.synthConfig.freeWheeling = false;
|
||||||
DBG("Disabling freewheeling");
|
DBG("Disabling freewheeling");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,15 @@ public:
|
||||||
* @param numVoices
|
* @param numVoices
|
||||||
*/
|
*/
|
||||||
Synth(int numVoices);
|
Synth(int numVoices);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Processing mode
|
||||||
|
*/
|
||||||
|
enum ProcessMode {
|
||||||
|
ProcessLive,
|
||||||
|
ProcessFreewheeling,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Empties the current regions and load a new SFZ file into the synth.
|
* @brief Empties the current regions and load a new SFZ file into the synth.
|
||||||
*
|
*
|
||||||
|
|
@ -255,6 +264,21 @@ public:
|
||||||
* @param sampleRate
|
* @param sampleRate
|
||||||
*/
|
*/
|
||||||
void setSampleRate(float sampleRate) noexcept;
|
void setSampleRate(float sampleRate) noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get the default resampling quality for the given mode.
|
||||||
|
*
|
||||||
|
* @param mode the processing mode
|
||||||
|
*
|
||||||
|
* @return the quality setting
|
||||||
|
*/
|
||||||
|
int getSampleQuality(ProcessMode mode);
|
||||||
|
/**
|
||||||
|
* @brief Set the default resampling quality for the given mode.
|
||||||
|
*
|
||||||
|
* @param mode the processing mode
|
||||||
|
* @param quality the quality setting
|
||||||
|
*/
|
||||||
|
void setSampleQuality(ProcessMode mode, int quality);
|
||||||
/**
|
/**
|
||||||
* @brief Get the current value for the volume, in dB.
|
* @brief Get the current value for the volume, in dB.
|
||||||
*
|
*
|
||||||
|
|
@ -669,7 +693,6 @@ private:
|
||||||
std::uniform_real_distribution<float> randNoteDistribution { 0, 1 };
|
std::uniform_real_distribution<float> randNoteDistribution { 0, 1 };
|
||||||
|
|
||||||
std::mutex callbackGuard;
|
std::mutex callbackGuard;
|
||||||
bool freeWheeling { false };
|
|
||||||
|
|
||||||
// Singletons passed as references to the voices
|
// Singletons passed as references to the voices
|
||||||
Resources resources;
|
Resources resources;
|
||||||
|
|
|
||||||
24
src/sfizz/SynthConfig.h
Normal file
24
src/sfizz/SynthConfig.h
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||||
|
// license. You should have receive a LICENSE.md file along with the code.
|
||||||
|
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "Defaults.h"
|
||||||
|
|
||||||
|
namespace sfz
|
||||||
|
{
|
||||||
|
struct SynthConfig
|
||||||
|
{
|
||||||
|
bool freeWheeling { false };
|
||||||
|
|
||||||
|
int liveSampleQuality { sfz::Default::sampleQuality };
|
||||||
|
int freeWheelingSampleQuality { sfz::Default::sampleQualityInFreewheelingMode };
|
||||||
|
|
||||||
|
int currentSampleQuality() const noexcept
|
||||||
|
{
|
||||||
|
return freeWheeling ? freeWheelingSampleQuality : liveSampleQuality;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -122,6 +122,12 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
||||||
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate);
|
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sfz::Voice::getCurrentSampleQuality() const noexcept
|
||||||
|
{
|
||||||
|
return (region && region->sampleQuality) ?
|
||||||
|
*region->sampleQuality : resources.synthConfig.currentSampleQuality();
|
||||||
|
}
|
||||||
|
|
||||||
bool sfz::Voice::isFree() const noexcept
|
bool sfz::Voice::isFree() const noexcept
|
||||||
{
|
{
|
||||||
return (state == State::idle);
|
return (state == State::idle);
|
||||||
|
|
@ -511,7 +517,7 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int quality = region->sampleQuality;
|
const int quality = getCurrentSampleQuality();
|
||||||
|
|
||||||
switch (quality) {
|
switch (quality) {
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,13 @@ public:
|
||||||
*/
|
*/
|
||||||
void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept;
|
void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the sample quality determined by the active region.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
int getCurrentSampleQuality() const noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Register a note-off event; this may trigger a release.
|
* @brief Register a note-off event; this may trigger a release.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,16 @@ void sfz::Sfizz::setSampleRate(float sampleRate) noexcept
|
||||||
synth->setSampleRate(sampleRate);
|
synth->setSampleRate(sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sfz::Sfizz::getSampleQuality(ProcessMode mode)
|
||||||
|
{
|
||||||
|
return synth->getSampleQuality(static_cast<sfz::Synth::ProcessMode>(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfz::Sfizz::setSampleQuality(ProcessMode mode, int quality)
|
||||||
|
{
|
||||||
|
synth->setSampleQuality(static_cast<sfz::Synth::ProcessMode>(mode), quality);
|
||||||
|
}
|
||||||
|
|
||||||
float sfz::Sfizz::getVolume() const noexcept
|
float sfz::Sfizz::getVolume() const noexcept
|
||||||
{
|
{
|
||||||
return synth->getVolume();
|
return synth->getVolume();
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,18 @@ bool sfizz_set_oversampling_factor(sfizz_synth_t* synth, sfizz_oversampling_fact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sfizz_get_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode)
|
||||||
|
{
|
||||||
|
auto self = reinterpret_cast<sfz::Synth*>(synth);
|
||||||
|
return self->getSampleQuality(static_cast<sfz::Synth::ProcessMode>(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sfizz_set_sample_quality(sfizz_synth_t* synth, sfizz_process_mode_t mode, int quality)
|
||||||
|
{
|
||||||
|
auto self = reinterpret_cast<sfz::Synth*>(synth);
|
||||||
|
return self->setSampleQuality(static_cast<sfz::Synth::ProcessMode>(mode), quality);
|
||||||
|
}
|
||||||
|
|
||||||
void sfizz_set_volume(sfizz_synth_t* synth, float volume)
|
void sfizz_set_volume(sfizz_synth_t* synth, float volume)
|
||||||
{
|
{
|
||||||
auto self = reinterpret_cast<sfz::Synth*>(synth);
|
auto self = reinterpret_cast<sfz::Synth*>(synth);
|
||||||
|
|
|
||||||
|
|
@ -486,3 +486,57 @@ TEST_CASE("[Synth] Region by identifier")
|
||||||
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{6}));
|
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{6}));
|
||||||
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{}));
|
REQUIRE(nullptr == synth.getRegionById(NumericId<sfz::Region>{}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] sample quality")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
|
||||||
|
synth.loadSfzString("tests/TestFiles/sampleQuality.sfz", R"(
|
||||||
|
<region> sample=kick.wav key=60
|
||||||
|
<region> sample=kick.wav key=61 sample_quality=5
|
||||||
|
)");
|
||||||
|
|
||||||
|
// default sample quality
|
||||||
|
synth.noteOn(0, 60, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQuality);
|
||||||
|
synth.allSoundOff();
|
||||||
|
|
||||||
|
// default sample quality, freewheeling
|
||||||
|
synth.enableFreeWheeling();
|
||||||
|
synth.noteOn(0, 60, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == sfz::Default::sampleQualityInFreewheelingMode);
|
||||||
|
synth.allSoundOff();
|
||||||
|
synth.disableFreeWheeling();
|
||||||
|
|
||||||
|
// user-defined sample quality
|
||||||
|
synth.setSampleQuality(sfz::Synth::ProcessLive, 3);
|
||||||
|
synth.noteOn(0, 60, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 3);
|
||||||
|
synth.allSoundOff();
|
||||||
|
|
||||||
|
// user-defined sample quality, freewheeling
|
||||||
|
synth.enableFreeWheeling();
|
||||||
|
synth.setSampleQuality(sfz::Synth::ProcessFreewheeling, 8);
|
||||||
|
synth.noteOn(0, 60, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 8);
|
||||||
|
synth.allSoundOff();
|
||||||
|
synth.disableFreeWheeling();
|
||||||
|
|
||||||
|
// region sample quality
|
||||||
|
synth.noteOn(0, 61, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5);
|
||||||
|
synth.allSoundOff();
|
||||||
|
|
||||||
|
// region sample quality, freewheeling
|
||||||
|
synth.enableFreeWheeling();
|
||||||
|
synth.noteOn(0, 61, 100);
|
||||||
|
REQUIRE(synth.getNumActiveVoices() == 1);
|
||||||
|
REQUIRE(synth.getVoiceView(0)->getCurrentSampleQuality() == 5);
|
||||||
|
synth.allSoundOff();
|
||||||
|
synth.disableFreeWheeling();
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue