diff --git a/cmake/SfizzConfig.cmake b/cmake/SfizzConfig.cmake index e651bc18..16c805bd 100644 --- a/cmake/SfizzConfig.cmake +++ b/cmake/SfizzConfig.cmake @@ -78,6 +78,9 @@ target_include_directories(sfizz-pugixml PUBLIC "src/external/pugixml/src") add_library(sfizz-spline STATIC "src/external/spline/spline/spline.cpp") target_include_directories(sfizz-spline PUBLIC "src/external/spline") +add_library(sfizz-tunings STATIC "src/external/tunings/src/Tunings.cpp") +target_include_directories(sfizz-tunings PUBLIC "src/external/tunings/include") + include (CheckLibraryExists) add_library (sfizz-atomic INTERFACE) if (UNIX AND NOT APPLE) diff --git a/dpf.mk b/dpf.mk index 58e5721d..c02c2f0e 100644 --- a/dpf.mk +++ b/dpf.mk @@ -99,6 +99,7 @@ SFIZZ_SOURCES = \ src/sfizz/SIMDNEON.cpp \ src/sfizz/SIMDSSE.cpp \ src/sfizz/Synth.cpp \ + src/sfizz/Tuning.cpp \ src/sfizz/Voice.cpp \ src/sfizz/Wavetables.cpp diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 89529d09..6322240b 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -61,6 +61,7 @@ #define SFIZZ_URI "http://sfztools.github.io/sfizz" #define SFIZZ_PREFIX SFIZZ_URI "#" #define SFIZZ__sfzFile "http://sfztools.github.io/sfizz:sfzfile" +#define SFIZZ__scalaFile "http://sfztools.github.io/sfizz:scalafile" #define SFIZZ__numVoices "http://sfztools.github.io/sfizz:numvoices" #define SFIZZ__preloadSize "http://sfztools.github.io/sfizz:preload_size" #define SFIZZ__oversampling "http://sfztools.github.io/sfizz:oversampling" @@ -105,6 +106,8 @@ typedef struct const float *oversampling_port; const float *preload_port; const float *freewheel_port; + const float *scala_root_key_port; + const float *tuning_frequency_port; // Atom forge LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread @@ -132,6 +135,7 @@ typedef struct LV2_URID patch_body_uri; LV2_URID state_changed_uri; LV2_URID sfizz_sfz_file_uri; + LV2_URID sfizz_scala_file_uri; LV2_URID sfizz_num_voices_uri; LV2_URID sfizz_preload_size_uri; LV2_URID sfizz_oversampling_uri; @@ -142,6 +146,7 @@ typedef struct sfizz_synth_t *synth; bool expect_nominal_block_length; char sfz_file_path[MAX_PATH_SIZE]; + char scala_file_path[MAX_PATH_SIZE]; int num_voices; unsigned int preload_size; sfizz_oversampling_factor_t oversampling; @@ -162,7 +167,9 @@ enum SFIZZ_POLYPHONY = 5, SFIZZ_OVERSAMPLING = 6, SFIZZ_PRELOAD = 7, - SFIZZ_FREEWHEELING = 8 + SFIZZ_FREEWHEELING = 8, + SFIZZ_SCALA_ROOT_KEY = 9, + SFIZZ_TUNING_FREQUENCY = 10, }; static void @@ -186,6 +193,7 @@ sfizz_lv2_map_required_uris(sfizz_plugin_t *self) self->patch_value_uri = map->map(map->handle, LV2_PATCH__value); self->state_changed_uri = map->map(map->handle, LV2_STATE__StateChanged); self->sfizz_sfz_file_uri = map->map(map->handle, SFIZZ__sfzFile); + self->sfizz_scala_file_uri = map->map(map->handle, SFIZZ__scalaFile); self->sfizz_num_voices_uri = map->map(map->handle, SFIZZ__numVoices); self->sfizz_preload_size_uri = map->map(map->handle, SFIZZ__preloadSize); self->sfizz_oversampling_uri = map->map(map->handle, SFIZZ__oversampling); @@ -228,6 +236,12 @@ connect_port(LV2_Handle instance, case SFIZZ_FREEWHEELING: self->freewheel_port = (const float *)data; break; + case SFIZZ_SCALA_ROOT_KEY: + self->scala_root_key_port = (const float *)data; + break; + case SFIZZ_TUNING_FREQUENCY: + self->tuning_frequency_port = (const float *)data; + break; default: break; } @@ -284,6 +298,7 @@ instantiate(const LV2_Descriptor *descriptor, self->sample_rate = (float)rate; self->expect_nominal_block_length = false; self->sfz_file_path[0] = '\0'; + self->scala_file_path[0] = '\0'; self->num_voices = DEFAULT_VOICES; self->oversampling = DEFAULT_OVERSAMPLING; self->preload_size = DEFAULT_PRELOAD; @@ -420,15 +435,15 @@ deactivate(LV2_Handle instance) } static void -sfizz_lv2_send_file_path(sfizz_plugin_t *self) +sfizz_lv2_send_file_path(sfizz_plugin_t *self, LV2_URID urid, const char *path) { LV2_Atom_Forge_Frame frame; lv2_atom_forge_frame_time(&self->forge, 0); lv2_atom_forge_object(&self->forge, &frame, 0, self->patch_set_uri); lv2_atom_forge_key(&self->forge, self->patch_property_uri); - lv2_atom_forge_urid(&self->forge, self->sfizz_sfz_file_uri); + lv2_atom_forge_urid(&self->forge, urid); lv2_atom_forge_key(&self->forge, self->patch_value_uri); - lv2_atom_forge_path(&self->forge, self->sfz_file_path, (uint32_t)strlen(self->sfz_file_path)); + lv2_atom_forge_path(&self->forge, path, (uint32_t)strlen(path)); lv2_atom_forge_pop(&self->forge, &frame); } @@ -477,6 +492,18 @@ sfizz_lv2_handle_atom_object(sfizz_plugin_t *self, const LV2_Atom_Object *obj) self->worker->schedule_work(self->worker->handle, null_terminated_atom_size, sfz_file_path); self->check_modification = false; } + else if (key == self->sfizz_scala_file_uri) + { + const uint32_t original_atom_size = lv2_atom_total_size((const LV2_Atom *)atom); + const uint32_t null_terminated_atom_size = original_atom_size + 1; + char atom_buffer[MAX_PATH_SIZE]; + memcpy(&atom_buffer, atom, original_atom_size); + atom_buffer[original_atom_size] = 0; // Null terminate the string for safety + LV2_Atom *scala_file_path = (LV2_Atom *)&atom_buffer; + scala_file_path->type = self->sfizz_scala_file_uri; + self->worker->schedule_work(self->worker->handle, null_terminated_atom_size, scala_file_path); + self->check_modification = false; + } else { lv2_log_warning(&self->logger, "[sfizz] Unknown or unsupported object\n"); @@ -657,11 +684,16 @@ run(LV2_Handle instance, uint32_t sample_count) lv2_atom_object_get(obj, self->patch_property_uri, &property, 0); if (!property) // Send the full state { - sfizz_lv2_send_file_path(self); + sfizz_lv2_send_file_path(self, self->sfizz_sfz_file_uri, self->sfz_file_path); + sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); } else if (property->body == self->sfizz_sfz_file_uri) { - sfizz_lv2_send_file_path(self); + sfizz_lv2_send_file_path(self, self->sfizz_sfz_file_uri, self->sfz_file_path); + } + else if (property->body == self->sfizz_scala_file_uri) + { + sfizz_lv2_send_file_path(self, self->sfizz_scala_file_uri, self->scala_file_path); } } else @@ -685,6 +717,8 @@ run(LV2_Handle instance, uint32_t sample_count) // Check and update parameters if needed sfizz_lv2_check_freewheeling(self); sfizz_set_volume(self->synth, *(self->volume_port)); + sfizz_set_scala_root_key(self->synth, *(self->scala_root_key_port)); + sfizz_set_tuning_frequency(self->synth, *(self->tuning_frequency_port)); sfizz_lv2_check_preload_size(self); sfizz_lv2_check_oversampling(self); sfizz_lv2_check_num_voices(self); @@ -838,6 +872,13 @@ restore(LV2_Handle instance, } } + value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags); + if (value) + { + lv2_log_note(&self->logger, "[sfizz] Restoring the scale %s\n", (const char *)value); + sfizz_load_scala_file(self->synth, (const char *)value); + } + value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags); if (value) { @@ -894,6 +935,14 @@ save(LV2_Handle instance, self->atom_path_uri, LV2_STATE_IS_POD); + // Save the scala file path + store(handle, + self->sfizz_scala_file_uri, + self->scala_file_path, + strlen(self->scala_file_path) + 1, + self->atom_path_uri, + LV2_STATE_IS_POD); + // Save the number of voices store(handle, self->sfizz_num_voices_uri, @@ -952,6 +1001,15 @@ work(LV2_Handle instance, }; respond(handle, lv2_atom_total_size(&check_modification_atom), &check_modification_atom); } + else if (atom->type == self->sfizz_scala_file_uri) + { + const char *scala_file_path = LV2_ATOM_BODY_CONST(atom); + if (sfizz_load_scala_file(self->synth, scala_file_path)) { + lv2_log_note(&self->logger, "[sfizz] Scala file loaded: %s\n", scala_file_path); + } else { + lv2_log_error(&self->logger, "[sfizz] Error with %s; no scala file should be loaded\n", scala_file_path); + } + } else if (atom->type == self->sfizz_num_voices_uri) { const int num_voices = *(const int *)LV2_ATOM_BODY_CONST(atom); diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 3b07c0cf..4e52bcfe 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -27,6 +27,12 @@ midnam:update a lv2:Feature . "Configuration"@fr , "Impostazioni"@it . +<@LV2PLUGIN_URI@#tuning> + a pg:Group ; + lv2:symbol "tuning" ; + lv2:name "Tuning", + "Accordage"@fr . + <@LV2PLUGIN_URI@:sfzfile> a lv2:Parameter ; rdfs:label "SFZ file", @@ -34,6 +40,14 @@ midnam:update a lv2:Feature . "File SFZ"@it ; rdfs:range atom:Path . +<@LV2PLUGIN_URI@:scalafile> + a lv2:Parameter ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + rdfs:label "Scala file", + "Fichier Scala"@fr , + "File Scala"@it ; + rdfs:range atom:Path . + <@LV2PLUGIN_URI@> a doap:Project, lv2:Plugin, lv2:InstrumentPlugin ; @@ -62,6 +76,7 @@ midnam:update a lv2:Feature . opts:supportedOption bufsize:maxBlockLength, bufsize:nominalBlockLength ; patch:writable <@LV2PLUGIN_URI@:sfzfile> ; + patch:writable <@LV2PLUGIN_URI@:scalafile> ; lv2:port [ a lv2:InputPort, atom:AtomPort ; @@ -242,4 +257,27 @@ midnam:update a lv2:Feature . lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 1 ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 9 ; + lv2:symbol "scala_root_key" ; + lv2:name "Scala root key", + "Tonalité de base Scala"@fr ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:portProperty lv2:integer ; + lv2:default 60 ; + lv2:minimum 0 ; + lv2:maximum 127 ; + units:unit units:midiNote + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 10 ; + lv2:symbol "tuning_frequency" ; + lv2:name "Tuning frequency", + "Fréquence d'accordage"@fr ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:default 440.0 ; + lv2:minimum 300.0 ; + lv2:maximum 500.0 ; + units:unit units:hz ] . diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e4ddcb56..0307417f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ set (SFIZZ_SOURCES sfizz/SfzFilter.cpp sfizz/Curve.cpp sfizz/Wavetables.cpp + sfizz/Tuning.cpp sfizz/RTSemaphore.cpp sfizz/Effects.cpp sfizz/effects/Nothing.cpp @@ -58,7 +59,7 @@ target_sources(sfizz_static PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp sfi target_include_directories (sfizz_static PUBLIC .) target_include_directories (sfizz_static PUBLIC external) target_link_libraries (sfizz_static PUBLIC absl::strings absl::span) -target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-kissfft sfizz-cpuid sfizz-atomic) +target_link_libraries (sfizz_static PRIVATE sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-atomic) set_target_properties (sfizz_static PROPERTIES OUTPUT_NAME sfizz PUBLIC_HEADER "sfizz.h;sfizz.hpp") if (WIN32) target_compile_definitions (sfizz_static PRIVATE _USE_MATH_DEFINES) @@ -95,7 +96,7 @@ if (SFIZZ_SHARED) target_sources(sfizz_shared PRIVATE ${SFIZZ_SOURCES} sfizz/sfizz_wrapper.cpp sfizz/sfizz.cpp) target_include_directories (sfizz_shared PRIVATE .) target_include_directories (sfizz_shared PRIVATE external) - target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-kissfft sfizz-cpuid sfizz-atomic) + target_link_libraries (sfizz_shared PRIVATE absl::strings absl::span sfizz_parser absl::flat_hash_map Threads::Threads sfizz-sndfile sfizz-pugixml sfizz-spline sfizz-tunings sfizz-kissfft sfizz-cpuid sfizz-atomic) if (WIN32) target_compile_definitions (sfizz_shared PRIVATE _USE_MATH_DEFINES) endif() diff --git a/src/sfizz.h b/src/sfizz.h index 13432f5d..35ab02ba 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -85,6 +85,58 @@ SFIZZ_EXPORTED_API bool sfizz_load_file(sfizz_synth_t* synth, const char* path); */ SFIZZ_EXPORTED_API bool sfizz_load_string(sfizz_synth_t* synth, const char* path, const char* text); +/** + * @brief Sets the tuning from a Scala file loaded from the file system. + * + * @param synth The sfizz synth. + * @param path The path to the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ +SFIZZ_EXPORTED_API bool sfizz_load_scala_file(sfizz_synth_t* synth, const char* path); + +/** + * @brief Sets the tuning from a Scala file loaded from memory. + * + * @param synth The sfizz synth. + * @param text The contents of the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ +SFIZZ_EXPORTED_API bool sfizz_load_scala_string(sfizz_synth_t* synth, const char* text); + +/** + * @brief Sets the scala root key. + * + * @param synth The sfizz synth. + * @param root_key The MIDI number of the Scala root key (default 60 for C4). + */ +SFIZZ_EXPORTED_API void sfizz_set_scala_root_key(sfizz_synth_t* synth, int root_key); + +/** + * @brief Gets the scala root key. + * + * @param synth The sfizz synth. + * @return The MIDI number of the Scala root key (default 60 for C4). + */ +SFIZZ_EXPORTED_API int sfizz_get_scala_root_key(sfizz_synth_t* synth); + +/** + * @brief Sets the reference tuning frequency. + * + * @param synth The sfizz synth. + * @param frequency The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ +SFIZZ_EXPORTED_API void sfizz_set_tuning_frequency(sfizz_synth_t* synth, float frequency); + +/** + * @brief Gets the reference tuning frequency. + * + * @param synth The sfizz synth. + * @return The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ +SFIZZ_EXPORTED_API float sfizz_get_tuning_frequency(sfizz_synth_t* synth); + /** * @brief Return the number of regions in the currently loaded SFZ file. * diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 63bd8173..c64b748d 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -73,6 +73,52 @@ public: */ bool loadSfzString(const std::string& path, const std::string& text); + /** + * @brief Sets the tuning from a Scala file loaded from the file system. + * + * @param path The path to the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ + bool loadScalaFile(const std::string& path); + + /** + * @brief Sets the tuning from a Scala file loaded from memory. + * + * @param text The contents of the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ + bool loadScalaString(const std::string& text); + + /** + * @brief Sets the scala root key. + * + * @param rootKey The MIDI number of the Scala root key (default 60 for C4). + */ + void setScalaRootKey(int rootKey); + + /** + * @brief Gets the scala root key. + * + * @return The MIDI number of the Scala root key (default 60 for C4). + */ + int getScalaRootKey() const; + + /** + * @brief Sets the reference tuning frequency. + * + * @param frequency The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ + void setTuningFrequency(float frequency); + + /** + * @brief Gets the reference tuning frequency. + * + * @return The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ + float getTuningFrequency() const; + /** * @brief Return the current number of regions loaded. */ diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index afd438bd..1a285c01 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1012,12 +1012,12 @@ void sfz::Region::registerTempo(float secondsPerQuarter) noexcept bpmSwitched = false; } -float sfz::Region::getBasePitchVariation(int noteNumber, float velocity) const noexcept +float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); std::uniform_int_distribution pitchDistribution { -pitchRandom, pitchRandom }; - auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center + auto pitchVariationInCents = pitchKeytrack * (noteNumber - pitchKeycenter); // note difference with pitch center pitchVariationInCents += tune; // sample tuning pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose pitchVariationInCents += static_cast(velocity) * pitchVeltrack; // track velocity diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index b532f712..7634c7f3 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -141,7 +141,7 @@ struct Region { * @param velocity * @return float */ - float getBasePitchVariation(int noteNumber, float velocity) const noexcept; + float getBasePitchVariation(float noteNumber, float velocity) const noexcept; /** * @brief Get the note-related gain of the region depending on which note has been * pressed and at which velocity. diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 17f924a9..c998eb3c 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -12,6 +12,7 @@ #include "Logger.h" #include "Wavetables.h" #include "Curve.h" +#include "Tuning.h" namespace sfz { @@ -27,6 +28,7 @@ struct Resources FilterPool filterPool { midiState }; EQPool eqPool { midiState }; WavetablePool wavePool; + Tuning tuning; void setSampleRate(float samplerate) { diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c980e5a0..b6d9a566 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -504,6 +504,36 @@ void sfz::Synth::finalizeSfzLoad() } } +bool sfz::Synth::loadScalaFile(const fs::path& path) +{ + return resources.tuning.loadScalaFile(path); +} + +bool sfz::Synth::loadScalaString(const std::string& text) +{ + return resources.tuning.loadScalaString(text); +} + +void sfz::Synth::setScalaRootKey(int rootKey) +{ + resources.tuning.setScalaRootKey(rootKey); +} + +int sfz::Synth::getScalaRootKey() const +{ + return resources.tuning.getScalaRootKey(); +} + +void sfz::Synth::setTuningFrequency(float frequency) +{ + resources.tuning.setTuningFrequency(frequency); +} + +float sfz::Synth::getTuningFrequency() const +{ + return resources.tuning.getTuningFrequency(); +} + sfz::Voice* sfz::Synth::findFreeVoice() noexcept { auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 5cbeb455..83f49e63 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -110,6 +110,46 @@ public: * parsing step. */ void finalizeSfzLoad(); + /** + * @brief Sets the tuning from a Scala file loaded from the file system. + * + * @param path The path to the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ + bool loadScalaFile(const fs::path& path); + /** + * @brief Sets the tuning from a Scala file loaded from memory. + * + * @param text The contents of the file in Scala format. + * @return @true when tuning scale loaded OK, + * @false if some error occurred. + */ + bool loadScalaString(const std::string& text); + /** + * @brief Sets the scala root key. + * + * @param rootKey The MIDI number of the Scala root key (default 60 for C4). + */ + void setScalaRootKey(int rootKey); + /** + * @brief Gets the scala root key. + * + * @return The MIDI number of the Scala root key (default 60 for C4). + */ + int getScalaRootKey() const; + /** + * @brief Sets the reference tuning frequency. + * + * @param frequency The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ + void setTuningFrequency(float frequency); + /** + * @brief Gets the reference tuning frequency. + * + * @return The frequency which indicates where standard tuning A4 is (default 440 Hz). + */ + float getTuningFrequency() const; /** * @brief Get the current number of regions loaded * diff --git a/src/sfizz/Tuning.cpp b/src/sfizz/Tuning.cpp new file mode 100644 index 00000000..cf6fdf67 --- /dev/null +++ b/src/sfizz/Tuning.cpp @@ -0,0 +1,202 @@ +// 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 + +#include "Tuning.h" +#include "Debug.h" +#include "Tunings.h" // Surge tuning library +#include +#include +#include +#include + +namespace sfz { + +struct Tuning::Impl { +public: + Impl() { updateKeysFractional12TET(); } + + const Tunings::Tuning& tuning() const { return tuning_; } + + float getKeyFractional12TET(int midiKey) const; + + int rootKey() const { return rootKey_; } + float tuningFrequency() const { return tuningFrequency_; } + + void updateScale(const Tunings::Scale& scale); + void updateRootKey(int rootKey); + void updateTuningFrequency(float tuningFrequency); + +private: + void updateKeysFractional12TET(); + static Tunings::KeyboardMapping mappingFromParameters(int rootKey, float tuningFrequency); + +private: + static constexpr int defaultRootKey = 60; + static constexpr float defaultTuningFrequency = 440.0; + int rootKey_ = defaultRootKey; + float tuningFrequency_ = defaultTuningFrequency; + + Tunings::Tuning tuning_ { + Tunings::evenTemperament12NoteScale(), + mappingFromParameters(defaultRootKey, defaultTuningFrequency) + }; + + static constexpr int numKeys = Tunings::Tuning::N; + std::array keysFractional12TET_; +}; + +/// +constexpr int Tuning::Impl::defaultRootKey; +constexpr float Tuning::Impl::defaultTuningFrequency; +constexpr int Tuning::Impl::numKeys; + +float Tuning::Impl::getKeyFractional12TET(int midiKey) const +{ + return keysFractional12TET_[std::max(0, std::min(numKeys - 1, midiKey))]; +} + +void Tuning::Impl::updateScale(const Tunings::Scale& scale) +{ + tuning_ = Tunings::Tuning(scale, tuning_.keyboardMapping); + updateKeysFractional12TET(); +} + +void Tuning::Impl::updateRootKey(int rootKey) +{ + ASSERT(rootKey >= 0); + rootKey = std::max(0, rootKey); + + if (rootKey_ == rootKey) + return; + + tuning_ = Tunings::Tuning(tuning_.scale, mappingFromParameters(rootKey, tuningFrequency_)); + rootKey_ = rootKey; + updateKeysFractional12TET(); +} + +void Tuning::Impl::updateTuningFrequency(float tuningFrequency) +{ + ASSERT(tuningFrequency >= 0); + tuningFrequency = std::max(0.0f, tuningFrequency); + + if (tuningFrequency_ == tuningFrequency) + return; + + tuning_ = Tunings::Tuning(tuning_.scale, mappingFromParameters(rootKey_, tuningFrequency)); + tuningFrequency_ = tuningFrequency; + updateKeysFractional12TET(); +} + +void Tuning::Impl::updateKeysFractional12TET() +{ + // mapping of MIDI key to equal temperament key + for (int key = 0; key < numKeys; ++key) { + double freq = tuning_.frequencyForMidiNote(key); + keysFractional12TET_[key] = 12.0 * std::log2(freq / 440.0) + 69.0; + } +} + +Tunings::KeyboardMapping Tuning::Impl::mappingFromParameters(int rootKey, float tuningFrequency) +{ +#if 1 + // root note is the start of octave. like Scala +#else + // root note is the start of next octave. like Sforzando + rootKey = std::max(0, rootKey - 12); +#endif + // fixed frequency of the root note + const double rootFrequency = tuningFrequency * std::exp2((rootKey - 69) / 12.0); + return Tunings::tuneNoteTo(rootKey, rootFrequency); +} + +/// +Tuning::Tuning() + : impl_(new Impl) +{ +} + +Tuning::~Tuning() +{ +} + +bool Tuning::loadScalaFile(const fs::path& path) +{ + const Tunings::KeyboardMapping kbm = impl_->tuning().keyboardMapping; + + fs::ifstream stream(path); + if (stream.bad()) { + DBG("Cannot open scale file: " << path); + return false; + } + + Tunings::Scale scl; + try { + scl = Tunings::readSCLStream(stream); + } + catch (Tunings::TuningError& error) { + DBG("Tuning: " << error.what()); + return false; + } + + impl_->updateScale(scl); + return true; +} + +bool Tuning::loadScalaString(const std::string& text) +{ + const Tunings::KeyboardMapping kbm = impl_->tuning().keyboardMapping; + + std::istringstream stream(text); + + Tunings::Scale scl; + try { + scl = Tunings::readSCLStream(stream); + } + catch (Tunings::TuningError& error) { + DBG("Tuning: " << error.what()); + return false; + } + + impl_->updateScale(scl); + return true; +} + +void Tuning::setScalaRootKey(int rootKey) +{ + impl_->updateRootKey(rootKey); +} + +int Tuning::getScalaRootKey() const +{ + return impl_->rootKey(); +} + +void Tuning::setTuningFrequency(float frequency) +{ + impl_->updateTuningFrequency(frequency); +} + +float Tuning::getTuningFrequency() const +{ + return impl_->tuningFrequency(); +} + +void Tuning::loadEqualTemperamentScale() +{ + impl_->updateScale(Tunings::evenTemperament12NoteScale()); +} + +float Tuning::getFrequencyOfKey(int midiKey) const +{ + return impl_->tuning().frequencyForMidiNote(midiKey); +} + +float Tuning::getKeyFractional12TET(int midiKey) +{ + return impl_->getKeyFractional12TET(midiKey); +} + +} // namespace sfz diff --git a/src/sfizz/Tuning.h b/src/sfizz/Tuning.h new file mode 100644 index 00000000..9e794ad9 --- /dev/null +++ b/src/sfizz/Tuning.h @@ -0,0 +1,68 @@ +// 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 "ghc/fs_std.hpp" +#include + +namespace sfz { + +class Tuning { +public: + Tuning(); + ~Tuning(); + + /** + * @brief Load a scale from a file in the Scala format. + */ + bool loadScalaFile(const fs::path& path); + + /** + * @brief Load a scale from memory in the Scala format. + */ + bool loadScalaString(const std::string& text); + + /** + * @brief Set the root key. + */ + void setScalaRootKey(int rootKey); + + /** + * @brief Get the root key. + */ + int getScalaRootKey() const; + + /** + * @brief Set the tuning frequency. + */ + void setTuningFrequency(float frequency); + + /** + * @brief Get the tuning frequency. + */ + float getTuningFrequency() const; + + /** + * @brief Load the equal-temperament scale. + */ + void loadEqualTemperamentScale(); + + /** + * @brief Get the MIDI key frequency under the present tuning. + */ + float getFrequencyOfKey(int midiKey) const; + + /** + * @brief Get the fractional MIDI key reconverted into equal temperament scale. + */ + float getKeyFractional12TET(int midiKey); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace sfz diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index edb00a07..5c64fb86 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -81,7 +81,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, } speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); } - pitchRatio = region->getBasePitchVariation(number, value); + + // do Scala retuning and reconvert the frequency into a 12TET key number + const float numberRetuned = resources.tuning.getKeyFractional12TET(number); + + pitchRatio = region->getBasePitchVariation(numberRetuned, value); baseVolumedB = region->getBaseVolumedB(number); baseGain = region->getBaseGain(); if (triggerType != TriggerType::CC) @@ -107,7 +111,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sourcePosition = region->getOffset(); triggerDelay = delay; initialDelay = delay + static_cast(region->getDelay() * sampleRate); - baseFrequency = midiNoteFrequency(number); + baseFrequency = resources.tuning.getFrequencyOfKey(number); bendStepFactor = centsFactor(region->bendStep); egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); } diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 6020dd11..2b33edd7 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -28,6 +28,36 @@ bool sfz::Sfizz::loadSfzString(const std::string& path, const std::string& text) return synth->loadSfzString(path, text); } +bool sfz::Sfizz::loadScalaFile(const std::string& path) +{ + return synth->loadScalaFile(path); +} + +bool sfz::Sfizz::loadScalaString(const std::string& text) +{ + return synth->loadScalaString(text); +} + +void sfz::Sfizz::setScalaRootKey(int rootKey) +{ + return synth->setScalaRootKey(rootKey); +} + +int sfz::Sfizz::getScalaRootKey() const +{ + return synth->getScalaRootKey(); +} + +void sfz::Sfizz::setTuningFrequency(float frequency) +{ + return synth->setTuningFrequency(frequency); +} + +float sfz::Sfizz::getTuningFrequency() const +{ + return synth->getTuningFrequency(); +} + int sfz::Sfizz::getNumRegions() const noexcept { return synth->getNumRegions(); diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 28eb3fdd..eb06640b 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -31,6 +31,42 @@ bool sfizz_load_string(sfizz_synth_t* synth, const char* path, const char* text) return self->loadSfzString(path, text); } +bool sfizz_load_scala_file(sfizz_synth_t* synth, const char* path) +{ + auto self = reinterpret_cast(synth); + return self->loadScalaFile(path); +} + +bool sfizz_load_scala_string(sfizz_synth_t* synth, const char* text) +{ + auto self = reinterpret_cast(synth); + return self->loadScalaString(text); +} + +void sfizz_set_scala_root_key(sfizz_synth_t* synth, int root_key) +{ + auto self = reinterpret_cast(synth); + self->setScalaRootKey(root_key); +} + +int sfizz_get_scala_root_key(sfizz_synth_t* synth) +{ + auto self = reinterpret_cast(synth); + return self->getScalaRootKey(); +} + +void sfizz_set_tuning_frequency(sfizz_synth_t* synth, float frequency) +{ + auto self = reinterpret_cast(synth); + self->setTuningFrequency(frequency); +} + +float sfizz_get_tuning_frequency(sfizz_synth_t* synth) +{ + auto self = reinterpret_cast(synth); + return self->getTuningFrequency(); +} + void sfizz_free(sfizz_synth_t* synth) { delete reinterpret_cast(synth); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e0d54bc4..c871114c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,7 @@ set(SFIZZ_TEST_SOURCES FloatHelpersT.cpp WavetablesT.cpp SemaphoreT.cpp + TuningT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) @@ -79,4 +80,7 @@ target_link_libraries(sfizz_plot_wavetables PRIVATE sfizz::sfizz) add_executable(sfizz_file_instrument FileInstrument.cpp) target_link_libraries(sfizz_file_instrument PRIVATE sfizz::sfizz) +add_executable(sfizz_tuning Tuning.cpp) +target_link_libraries(sfizz_tuning PRIVATE sfizz::sfizz) + file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests) diff --git a/tests/Tuning.cpp b/tests/Tuning.cpp new file mode 100644 index 00000000..ee65f190 --- /dev/null +++ b/tests/Tuning.cpp @@ -0,0 +1,106 @@ +// 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 + +#include "sfizz/Tuning.h" +#include "sfizz/SfzHelpers.h" +#include "cxxopts.hpp" +#include +#include +#include +#include + +static const char *octNoteNames[12] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", +}; + +static std::string noteName(int key) +{ + int octNum; + int octNoteNum; + if (key >= 0) { + octNum = key / 12 - 1; + octNoteNum = key % 12; + } + else { + octNum = -2 - (key + 1) / -12; + octNoteNum = (key % 12 + 12) % 12; + } + return std::string(octNoteNames[octNoteNum]) + std::to_string(octNum); +} + +int main(int argc, char* argv[]) +{ + cxxopts::Options options(argv[0], " - command line options"); + + options.add_options() + ("h,help", "Print help") + ("s,scale", "Path of scala tuning file", cxxopts::value()) + ("f,frequency", "Tuning frequency", cxxopts::value()->default_value("440.0")) + ("r,root-key", "Root key", cxxopts::value()->default_value("C5")); + + auto result = options.parse(argc, argv); + + if (result.count("help")) { + std::cout << options.help({""}) << std::endl; + return 0; + } + + /// + sfz::Tuning tuning; + + if (result.count("scale")) { + if (!tuning.loadScalaFile(result["scale"].as())) { + fprintf(stderr, "Could not load the scale file.\n"); + return 1; + } + } + + absl::optional noteNumber = sfz::readNoteValue( + result["root-key"].as()); + if (!noteNumber) { + fprintf(stderr, "The root key is not a valid note name.\n"); + return 1; + } + + tuning.setScalaRootKey(*noteNumber); + tuning.setTuningFrequency(result["frequency"].as()); + + const int numRows = 3; + const int numCols = 4; + + for (int row = 0; row < numRows; ++row) { + for (int i = 0; i < 73; ++i) + putchar('-'); + putchar('\n'); + for (int nthKey = 0; nthKey < 12; ++nthKey) { + for (int col = 0; col < numCols; ++col) { + const int key = nthKey + (col + row * numCols) * 12; + + std::string label; + label.push_back('|'); + label.append(noteName(key)); + while (label.size() < 5) + label.push_back(' '); + label.push_back('|'); + + if (col > 0) { + for (int i = 0; i < 1; ++i) + putchar(' '); + } + + printf("%s %10.4f", label.c_str(), tuning.getFrequencyOfKey(key)); + } + putchar(' '); + putchar('|'); + putchar('\n'); + } + } + for (int i = 0; i < 73; ++i) + putchar('-'); + putchar('\n'); + + return 0; +} diff --git a/tests/TuningT.cpp b/tests/TuningT.cpp new file mode 100644 index 00000000..76c94d67 --- /dev/null +++ b/tests/TuningT.cpp @@ -0,0 +1,17 @@ +// 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 + +#include "sfizz/Tuning.h" +#include "sfizz/MathHelpers.h" +#include "catch2/catch.hpp" + +TEST_CASE("[Tuning] Default tuning") +{ + sfz::Tuning defaultTuning; + + for (int key = 0; key < 128; ++key) + REQUIRE(defaultTuning.getFrequencyOfKey(key) == Approx(midiNoteFrequency(key))); +}