Merge pull request #253 from jpcima/scala

Tuning
This commit is contained in:
JP Cimalando 2020-06-10 23:42:44 +02:00 committed by GitHub
commit a369d61b78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1670 additions and 21 deletions

View file

@ -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)

1
dpf.mk
View file

@ -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

View file

@ -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,22 @@ restore(LV2_Handle instance,
}
}
value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags);
if (value)
{
if (sfizz_load_scala_file(self->synth, (const char *)value))
{
lv2_log_note(&self->logger,
"[sfizz] Restoring the scale %s\n", (const char *)value);
strcpy(self->scala_file_path, (const char *)value);
}
else
{
lv2_log_error(&self->logger,
"[sfizz] Error while restoring the scale %s\n", (const char *)value);
}
}
value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags);
if (value)
{
@ -894,6 +944,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,
@ -921,6 +979,19 @@ save(LV2_Handle instance,
return LV2_STATE_SUCCESS;
}
static void
sfizz_lv2_activate_file_checking(
sfizz_plugin_t *self,
LV2_Worker_Respond_Function respond,
LV2_Worker_Respond_Handle handle)
{
LV2_Atom check_modification_atom = {
.size = 0,
.type = self->sfizz_check_modification_uri
};
respond(handle, lv2_atom_total_size(&check_modification_atom), &check_modification_atom);
}
// This runs in a lower priority thread
static LV2_Worker_Status
work(LV2_Handle instance,
@ -942,15 +1013,26 @@ work(LV2_Handle instance,
if (sfizz_load_file(self->synth, sfz_file_path)) {
sfizz_lv2_update_file_info(self, sfz_file_path);
} else {
lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", sfz_file_path);
lv2_log_error(&self->logger,
"[sfizz] Error with %s; no file should be loaded\n", sfz_file_path);
}
// Reactivate checking for file changes
LV2_Atom check_modification_atom = {
.size = 0,
.type = self->sfizz_check_modification_uri
};
respond(handle, lv2_atom_total_size(&check_modification_atom), &check_modification_atom);
sfizz_lv2_activate_file_checking(self, respond, handle);
}
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)) {
strcpy(self->scala_file_path, 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 new scala file should be loaded\n", scala_file_path);
}
// Reactivate checking for file changes
sfizz_lv2_activate_file_checking(self, respond, handle);
}
else if (atom->type == self->sfizz_num_voices_uri)
{
@ -997,10 +1079,26 @@ work(LV2_Handle instance,
if (sfizz_load_file(self->synth, self->sfz_file_path)) {
sfizz_lv2_update_file_info(self, self->sfz_file_path);
} else {
lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", self->sfz_file_path);
lv2_log_error(&self->logger,
"[sfizz] Error with %s; no file should be loaded\n", self->sfz_file_path);
}
}
respond(handle, size, data); // reactivate file checking
if (sfizz_should_reload_scala(self->synth))
{
lv2_log_note(&self->logger,
"[sfizz] Scala file %s seems to have been updated, reloading\n",
self->scala_file_path);
if (sfizz_load_scala_file(self->synth, self->scala_file_path)) {
lv2_log_note(&self->logger, "[sfizz] Scala file loaded: %s\n", self->scala_file_path);
} else {
lv2_log_error(&self->logger,
"[sfizz] Error with %s; no new scala file should be loaded\n", self->scala_file_path);
}
}
// Reactivate checking for file changes
sfizz_lv2_activate_file_checking(self, respond, handle);
}
else
{

View file

@ -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
] .

View file

@ -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()

9
src/external/tunings/LICENSE.md vendored Normal file
View file

@ -0,0 +1,9 @@
Copyright 2019-2020, Paul Walker
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.

2
src/external/tunings/README.txt vendored Normal file
View file

@ -0,0 +1,2 @@
Based on the Surge tuning library, revision a5f2879, with small modifications
https://github.com/surge-synthesizer/tuning-library

245
src/external/tunings/include/Tunings.h vendored Normal file
View file

@ -0,0 +1,245 @@
// -*-c++-*-
/**
* Tunings.h
* Copyright Paul Walker, 2019-2020
* Released under the MIT License. See LICENSE.md
*
* Tunings.h contains the public API required to determine full keyboard frequency maps
* for a scala SCL and KBM file in standalone, tested, open licensed C++ header only library.
*
* An example of using the API is
*
* ```
* auto s = Tunings::readSCLFile( "./my-scale.scl" );
* auto k = Tunings::readKBMFile( "./my-mapping.kbm" );
*
* Tunings::Tuning t( s, k );
*
* std::cout << "The frequency of C4 and A4 are "
* << t.frequencyForMidiNote( 60 ) << " and "
* << t.frequencyForMidiNote( 69 ) << std::endl;
* ```
*
* The API provides several other points, such as access to the structure of the SCL and KBM,
* the ability to create several prototype SCL and KBM files wthout SCL or KBM content,
* a frequency measure which is normalized by the frequency of standard tuning midi note 0
* and the logarithmic frequency scale, with a doubling per frequency doubling.
*
* Documentation is in the class header below; tests are in `tests/all_tests.cpp` and
* a variety of command line tools accompany the header.
*/
#pragma once
#include <string>
#include <memory>
#include <array>
namespace Tunings
{
static constexpr double MIDI_0_FREQ = 8.17579891564371; // or 440.0 * pow( 2.0, - (69.0/12.0 ) )
static constexpr int MAX_CAPACITY = 64; // fixed capacity of note/key lists
/**
* A Tone is a single entry in an SCL file. It is expressed either in cents or in
* a ratio, as described in the SCL documentation.
*
* In most normal use, you will not use this class, and it will be internal to a Scale
*/
struct Tone
{
typedef enum Type
{
kToneCents, // An SCL representation like "133.0"
kToneRatio // An SCL representation like "3/7"
} Type;
Type type = kToneRatio;
double cents = 0;
int ratio_d = 1, ratio_n = 1;
double floatValue = 1.0; // cents / 1200 + 1.
};
/**
* Given an SCL string like "100.231" or "3/7" set up a Tone
*/
Tone toneFromString(const std::string &t, int lineno=-1);
/**
* The Scale is the representation of the SCL file. It contains several key
* features. Most importantly it has a count and a vector of Tones.
*
* In most normal use, you will simply pass around instances of this class
* to a Tunings::Tuning instance, but in some cases you may want to create
* or inspect this class yourself.
*/
struct Scale
{
std::string description; // The description in the SCL file. Informational only
int count = 0; // The number of tones.
Tone tones[MAX_CAPACITY]; // The tones
};
/**
* The KeyboardMapping class represents a KBM file. In most cases, the salient
* features are the tuningConstantNote and tuningFrequency, which allow you to
* pick a fixed note in the midi keyboard when retuning. The KBM file can also
* remap individual keys to individual points in a scale, which kere is done with the
* keys vector.
*/
struct KeyboardMapping
{
int count = 0;
int firstMidi = 0, lastMidi = 127;
int middleNote = 60;
int tuningConstantNote = 60;
double tuningFrequency = MIDI_0_FREQ * 32.0, tuningPitch = 32.0; // pitch = frequency / MIDI_0_FREQ
int octaveDegrees = 12;
int keys[MAX_CAPACITY]; // rather than an 'x' we use a '-1' for skipped keys
};
/**
* In some failure states, the tuning library will throw an exception of
* type TuningError with a descriptive message.
*/
class TuningError : public std::exception {
public:
explicit TuningError(std::string m) : whatv(std::move(m)) { }
virtual const char* what() const noexcept override { return whatv.c_str(); }
private:
std::string whatv;
};
/**
* readSCLStream returns a Scale from the SCL input stream
*/
Scale readSCLStream(std::istream &inf);
/**
* readSCLFile returns a Scale from the SCL File in fname
*/
Scale readSCLFile(std::string fname);
/**
* parseSCLData returns a scale from the SCL file contents in memory
*/
Scale parseSCLData(const std::string &sclContents);
/**
* evenTemperament12NoteScale provides a utility scale which is
* the "standard tuning" scale
*/
Scale evenTemperament12NoteScale();
#if 0 // Note(jpc): not rewritten RT-safe, not used, disabled
/**
* evenDivisionOfSpanByM provides a scale referd to as "ED2-17" or
* "ED3-24" by dividing the Span into M points. eventDivisionOfSpanByM(2,12)
* should be the evenTemperament12NoteScale
*/
Scale evenDivisionOfSpanByM( int Span, int M );
#endif
/**
* readKBMStream returns a KeyboardMapping from a KBM input stream
*/
KeyboardMapping readKBMStream(std::istream &inf);
/**
* readKBMFile returns a KeyboardMapping from a KBM file name
*/
KeyboardMapping readKBMFile(std::string fname);
/**
* parseKBMData returns a KeyboardMapping from a KBM data in memory
*/
KeyboardMapping parseKBMData(const std::string &kbmContents);
/**
* tuneA69To creates a KeyboardMapping which keeps the midi note 69 (A4) set
* to a constant frequency, given
*/
KeyboardMapping tuneA69To(double freq);
/**
* tuneNoteTo creates a KeyboardMapping which keeps the midi note given is set
* to a constant frequency, given
*/
KeyboardMapping tuneNoteTo(int midiNote, double freq);
/**
* startScaleOnAndTuneNoteTo generates a KBM where scaleStart is the note 0
* of the scale, where midiNote is the tuned note, and where feq is the frequency
*/
KeyboardMapping startScaleOnAndTuneNoteTo(int scaleStart, int midiNote, double freq);
/**
* The Tuning class is the primary place where you will interact with this library.
* It is constructed for a scale and mapping and then gives you the ability to
* determine frequencies across and beyond the midi keyboard. Since modulation
* can force key number well outside the [0,127] range in some of our synths we
* support a midi note range from -256 to + 256 spanning more than the entire frequency
* space reasonable.
*
* To use this class, you construct a fresh instance every time you want to use a
* different Scale and Keyboard. If you want to tune to a different scale or mapping,
* just construct a new instance.
*/
class Tuning {
public:
// The number of notes we pre-compute
constexpr static int N = 512;
// Construct a tuning with even temperament and standard mapping
Tuning();
/**
* Construct a tuning for a particular scale, mapping, or for both.
*/
explicit Tuning( const Scale &s );
explicit Tuning( const KeyboardMapping &k );
Tuning( const Scale &s, const KeyboardMapping &k );
/**
* These three related functions provide you the information you
* need to use this tuning.
*
* frequencyForMidiNote returns the Frequency in HZ for a given midi
* note. In standard tuning, FrequencyForMidiNote(69) will be 440
* and frequencyForMidiNote(60) will be 261.62 - the standard frequencies
* for A and middle C.
*
* frequencyForMidiNoteScaledByMidi0 returns the frequency but with the
* standard frequency of midi note 0 divided out. So in standard tuning
* frequencyForMidiNoteScaledByMidi0(0) = 1 and frequencyForMidiNoteScaledByMidi0(60) = 32
*
* Finally logScaledFrequencyForMidiNote returns the log base 2 of the scaled frequency.
* So logScaledFrequencyForMidiNote(0) = 0 and logScaledFrequencyForMidiNote(60) = 5.
*
* Both the frequency measures have the feature of doubling when frequency doubles
* (or when a standard octave is spanned), whereas the log one increase by 1 per frequency double.
*
* Depending on your internal pitch model, one of these three methods should allow you
* to calibrate your oscillators to the appropriate frequency based on the midi note
* at hand.
*
* The scalePositionForMidiNote returns the space in the logical scale. Note 0 is the root.
* It has a maxiumum value of count-1. Note that SCL files omit the root internally and so
* this logical scale position is off by 1 from the index in the tones array of the Scale data.
*/
double frequencyForMidiNote( int mn ) const;
double frequencyForMidiNoteScaledByMidi0( int mn ) const;
double logScaledFrequencyForMidiNote( int mn ) const;
int scalePositionForMidiNote( int mn ) const;
// For convenience, the scale and mapping used to construct this are kept as public copies
Scale scale;
KeyboardMapping keyboardMapping;
private:
std::array<double, N> ptable, lptable;
std::array<int, N> scalepositiontable;
};
} // namespace Tunings

522
src/external/tunings/src/Tunings.cpp vendored Normal file
View file

@ -0,0 +1,522 @@
// -*-c++-*-
/**
* TuningsImpl.h
* Copyright 2019-2020 Paul Walker
* Released under the MIT License. See LICENSE.md
*
* This contains the nasty nitty gritty implementation of the api in Tunings.h. You probably
* don't need to read it unless you have found and are fixing a bug, are curious, or want
* to add a feature to the API. For usages of this library, the documentation in Tunings.h and
* the usages in tests/all_tests.cpp should provide you more than enough guidance.
*/
#include "Tunings.h"
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <cctype>
#include <cmath>
namespace Tunings
{
static double locale_atof(const char* s)
{
double result = 0;
std::istringstream istr(s);
istr.imbue(std::locale("C"));
istr >> result;
return result;
}
Tone toneFromString(const std::string &line, int lineno)
{
Tone t;
if (line.find(".") != std::string::npos)
{
t.type = Tone::kToneCents;
t.cents = locale_atof(line.c_str());
}
else
{
t.type = Tone::kToneRatio;
auto slashPos = line.find("/");
if (slashPos == std::string::npos)
{
t.ratio_n = atoi(line.c_str());
t.ratio_d = 1;
}
else
{
t.ratio_n = atoi(line.substr(0, slashPos).c_str());
t.ratio_d = atoi(line.substr(slashPos + 1).c_str());
}
if( t.ratio_n == 0 || t.ratio_d == 0 )
{
std::string s = "Invalid Tone in SCL file.";
if( lineno >= 0 )
s += "Line " + std::to_string(lineno) + ".";
s += " Line is '" + line + "'.";
throw TuningError(s);
}
// 2^(cents/1200) = n/d
// cents = 1200 * log2(n/d)
t.cents = 1200 * log2(1.0 * t.ratio_n/t.ratio_d);
}
t.floatValue = t.cents / 1200.0 + 1.0;
return t;
}
Scale readSCLStream(std::istream &inf)
{
std::string line;
const int read_header = 0, read_count = 1, read_note = 2, trailing = 3;
int state = read_header;
int tone_index = 0;
Scale res;
int lineno = 0;
while (std::getline(inf, line))
{
lineno ++;
if (line.empty() || line[0] == '!')
{
continue;
}
switch (state)
{
case read_header:
res.description = line;
state = read_count;
break;
case read_count:
res.count = atoi(line.c_str());
if(res.count < 0 || res.count > MAX_CAPACITY)
{
throw TuningError( "Tone count invalid or too large in SCL file." );
}
state = ( res.count > 0 ) ? read_note : trailing;
break;
case read_note:
res.tones[tone_index++] = toneFromString(line, lineno);
if( tone_index == res.count )
state = trailing;
break;
}
}
if( ! ( state == read_note || state == trailing ) )
{
throw TuningError( "Incomplete SCL file. Found no notes section in the file" );
}
if( tone_index != res.count )
{
std::string s = "Read fewer notes than count in file. Count=" + std::to_string( res.count )
+ " notes array size=" + std::to_string( tone_index );
throw TuningError(s);
}
return res;
}
Scale readSCLFile(std::string fname)
{
std::ifstream inf;
inf.open(fname);
if (!inf.is_open())
{
std::string s = "Unable to open file '" + fname + "'";
throw TuningError(s);
}
auto res = readSCLStream(inf);
return res;
}
Scale parseSCLData(const std::string &d)
{
std::istringstream iss(d);
auto res = readSCLStream(iss);
return res;
}
Scale evenTemperament12NoteScale()
{
Scale res;
res.count = 12;
for (int i = 0; i < 12; ++i) {
Tone &t = res.tones[i];
t.type = Tone::kToneCents;
t.cents = 100 * (i + 1);
t.floatValue = t.cents / 1200.0 + 1.0;
}
return res;
}
#if 0
Scale evenDivisionOfSpanByM( int Span, int M )
{
if( Span <= 0 )
throw Tunings::TuningError( "Span should be a positive number. You entered " + std::to_string( Span ) );
if( M <= 0 )
throw Tunings::TuningError( "You must divide the period into at least one step. You entered " + std::to_string( M ) );
std::ostringstream oss;
oss.imbue( std::locale( "C" ) );
oss << "! Automatically generated ED" << Span << "-" << M << " scale\n";
oss << "Automatically generated ED" << Span << "-" << M << " scale\n";
oss << M << "\n";
oss << "!\n";
double topCents = 1200.0 * log2(1.0 * Span);
double dCents = topCents / M;
for( int i=1; i<M; ++i )
oss << std::fixed << dCents * i << "\n";
oss << Span << "/1\n";
return parseSCLData( oss.str() );
}
#endif
KeyboardMapping readKBMStream(std::istream &inf)
{
std::string line;
KeyboardMapping res;
enum parsePosition {
map_size = 0,
first_midi,
last_midi,
middle,
reference,
freq,
degree,
keys,
trailing
};
parsePosition state = map_size;
int key_index = 0;
int lineno = 0;
while (std::getline(inf, line))
{
lineno ++;
if (line.empty() || line[0] == '!')
{
continue;
}
if( line == "x" ) line = "-1";
else if( state != trailing )
{
const char* lc = line.c_str();
bool validLine = line.length() > 0;
char badChar = '\0';
while( validLine && *lc != '\0' )
{
if( ! ( *lc == ' ' || std::isdigit( *lc ) || *lc == '.' || *lc == (char)13 || *lc == '\n' ) )
{
validLine = false;
badChar = *lc;
}
lc ++;
}
if( ! validLine )
{
throw TuningError( "Invalid line " + std::to_string( lineno ) + ". line='" + line + "'. Bad char is '" +
badChar + "/" + std::to_string( (int)badChar ) + "'" );
}
}
int i = std::atoi(line.c_str());
double v = locale_atof(line.c_str());
switch (state)
{
case map_size:
res.count = i;
if(res.count < 0 || res.count > MAX_CAPACITY)
{
throw TuningError( "Key count invalid or too large in KBM file." );
}
break;
case first_midi:
res.firstMidi = i;
break;
case last_midi:
res.lastMidi = i;
break;
case middle:
res.middleNote = i;
break;
case reference:
res.tuningConstantNote = i;
break;
case freq:
res.tuningFrequency = v;
res.tuningPitch = res.tuningFrequency / 8.17579891564371;
break;
case degree:
res.octaveDegrees = i;
break;
case keys:
res.keys[key_index++] = i;
if( key_index == res.count ) state = trailing;
break;
case trailing:
break;
}
if( ! ( state == keys || state == trailing ) ) state = (parsePosition)(state + 1);
if( state == keys && res.count == 0 ) state = trailing;
}
if( ! ( state == keys || state == trailing ) )
{
throw TuningError( "Incomplete KBM file. Ubable to get to keys section of file" );
}
if( key_index != res.count )
{
throw TuningError( "Different number of keys than mapping file indicates. Count is "
+ std::to_string( res.count ) + " and we parsed " + std::to_string( key_index ) + " keys." );
}
return res;
}
KeyboardMapping readKBMFile(std::string fname)
{
std::ifstream inf;
inf.open(fname);
if (!inf.is_open())
{
std::string s = "Unable to open file '" + fname + "'";
throw TuningError(s);
}
auto res = readKBMStream(inf);
//res.name = std::move(fname);
return res;
}
KeyboardMapping parseKBMData(const std::string &d)
{
std::istringstream iss(d);
auto res = readKBMStream(iss);
//res.name = "Mapping from Patch";
return res;
}
Tuning::Tuning() : Tuning( evenTemperament12NoteScale(), KeyboardMapping() ) { }
Tuning::Tuning(const Scale &s ) : Tuning( s, KeyboardMapping() ) {}
Tuning::Tuning(const KeyboardMapping &k ) : Tuning( evenTemperament12NoteScale(), k ) {}
Tuning::Tuning(const Scale& s, const KeyboardMapping &k)
{
scale = s;
keyboardMapping = k;
if( s.count <= 0 )
throw TuningError( "Unable to tune to a scale with no notes. Your scale provided " + std::to_string( s.count ) + " notes." );
double pitches[N];
int posPitch0 = 256 + k.tuningConstantNote;
int posScale0 = 256 + k.middleNote;
double pitchMod = log2(k.tuningPitch) - 1;
int scalePositionOfTuningNote = k.tuningConstantNote - k.middleNote;
if( k.count > 0 )
scalePositionOfTuningNote = k.keys[scalePositionOfTuningNote];
double tuningCenterPitchOffset;
if( scalePositionOfTuningNote == 0 )
tuningCenterPitchOffset = 0;
else
{
double tshift = 0;
double dt = s.tones[s.count -1].floatValue - 1.0;
while( scalePositionOfTuningNote < 0 )
{
scalePositionOfTuningNote += s.count;
tshift += dt;
}
while( scalePositionOfTuningNote > s.count )
{
scalePositionOfTuningNote -= s.count;
tshift -= dt;
}
if( scalePositionOfTuningNote == 0 )
tuningCenterPitchOffset = -tshift;
else
tuningCenterPitchOffset = s.tones[scalePositionOfTuningNote-1].floatValue - 1.0 - tshift;
}
for (int i=0; i<N; ++i)
{
// TODO: ScaleCenter and PitchCenter are now two different notes.
int distanceFromPitch0 = i - posPitch0;
int distanceFromScale0 = i - posScale0;
if( distanceFromPitch0 == 0 )
{
pitches[i] = 1;
lptable[i] = pitches[i] + pitchMod;
ptable[i] = pow( 2.0, lptable[i] );
scalepositiontable[i] = scalePositionOfTuningNote % s.count;
#if DEBUG_SCALES
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " p=" << pitches[i]
<< " lp=" << lptable[i]
<< " tp=" << ptable[i]
<< " fr=" << ptable[i] * 8.175798915
<< std::endl;
#endif
}
else
{
/*
We used to have this which assumed 1-12
Now we have our note number, our distance from the
center note, and the key remapping
int rounds = (distanceFromScale0-1) / s.count;
int thisRound = (distanceFromScale0-1) % s.count;
*/
int rounds;
int thisRound;
int disable = false;
if( ( k.count == 0 ) )
{
rounds = (distanceFromScale0-1) / s.count;
thisRound = (distanceFromScale0-1) % s.count;
}
else
{
/*
** Now we have this situation. We are at note i so we
** are m away from the center note which is distanceFromScale0
**
** If we mod that by the mapping size we know which note we are on
*/
int mappingKey = distanceFromScale0 % k.count;
if( mappingKey < 0 )
mappingKey += k.count;
int cm = k.keys[mappingKey];
int push = 0;
if( cm < 0 )
{
disable = true;
}
else
{
push = mappingKey - cm;
}
rounds = (distanceFromScale0 - push - 1) / s.count;
thisRound = (distanceFromScale0 - push - 1) % s.count;
#ifdef DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "MAPPING n=" << i - 256 << " pushes ds0=" << distanceFromScale0 << " cmc=" << k.count << " tr=" << thisRound << " r=" << rounds << " mk=" << mappingKey << " cm=" << cm << " push=" << push << " dis=" << disable << " mk-p-1=" << mappingKey - push - 1 << std::endl;
#endif
}
if( thisRound < 0 )
{
thisRound += s.count;
rounds -= 1;
}
if( disable )
{
pitches[i] = 0;
scalepositiontable[i] = -1;
}
else
{
pitches[i] = s.tones[thisRound].floatValue + rounds * (s.tones[s.count - 1].floatValue - 1.0) - tuningCenterPitchOffset;
scalepositiontable[i] = ( thisRound + 1 ) % s.count;
}
lptable[i] = pitches[i] + pitchMod;
ptable[i] = pow( 2.0, pitches[i] + pitchMod );
#if DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " ds0=" << distanceFromScale0
<< " dp0=" << distanceFromPitch0
<< " r=" << rounds << " t=" << thisRound
<< " p=" << pitches[i]
<< " t=" << s.tones[thisRound].floatValue << " " << s.tones[thisRound ].cents
<< " dis=" << disable
<< " tp=" << ptable[i]
<< " fr=" << ptable[i] * 8.175798915
<< " tcpo=" << tuningCenterPitchOffset
//<< " l2p=" << log2(otp)
//<< " l2p-p=" << log2(otp) - pitches[i] - rounds - 3
<< std::endl;
#endif
}
}
}
double Tuning::frequencyForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return ptable[ mni ] * MIDI_0_FREQ;
}
double Tuning::frequencyForMidiNoteScaledByMidi0( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return ptable[ mni ];
}
double Tuning::logScaledFrequencyForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return lptable[ mni ];
}
int Tuning::scalePositionForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return scalepositiontable[ mni ];
}
KeyboardMapping tuneA69To(double freq)
{
return tuneNoteTo( 69, freq );
}
KeyboardMapping tuneNoteTo( int midiNote, double freq )
{
return startScaleOnAndTuneNoteTo( 60, midiNote, freq );
}
KeyboardMapping startScaleOnAndTuneNoteTo( int scaleStart, int midiNote, double freq )
{
KeyboardMapping res;
res.count = 0;
res.firstMidi = 0;
res.lastMidi = 127;
res.middleNote = scaleStart;
res.tuningConstantNote = midiNote;
res.tuningFrequency = freq;
res.tuningPitch = freq / MIDI_0_FREQ;
res.octaveDegrees = 0;
return res;
}
}

View file

@ -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.
*
@ -372,6 +424,16 @@ SFIZZ_EXPORTED_API char* sfizz_get_unknown_opcodes(sfizz_synth_t* synth);
*/
SFIZZ_EXPORTED_API bool sfizz_should_reload_file(sfizz_synth_t* synth);
/**
* @brief Check if the scala file should be reloaded.
* Depending on the platform this can create file descriptors.
*
* @param synth The synth.
*
* @return @true if the scala file has been modified since loading.
*/
SFIZZ_EXPORTED_API bool sfizz_should_reload_scala(sfizz_synth_t* synth);
/**
* @brief Enable logging of timings to sidecar CSV files. This can produce
* many outputs so use with caution.

View file

@ -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.
*/
@ -304,6 +350,16 @@ public:
*/
bool shouldReloadFile();
/**
* @brief Check if the tuning (scala) file should be reloaded.
*
* Depending on the platform this can create file descriptors.
*
* @return true if a scala file has been loaded and has changed
* @return false
*/
bool shouldReloadScala();
/**
* @brief Enable logging of timings to sidecar CSV files. This can produce
* many outputs so use with caution.

View file

@ -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<int> 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<int>(velocity) * pitchVeltrack; // track velocity

View file

@ -142,7 +142,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.

View file

@ -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)
{

View file

@ -486,6 +486,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>& voice) {
@ -1162,6 +1192,11 @@ bool sfz::Synth::shouldReloadFile()
return (checkModificationTime() > modificationTime);
}
bool sfz::Synth::shouldReloadScala()
{
return resources.tuning.shouldReloadScala();
}
void sfz::Synth::enableLogging(absl::string_view prefix) noexcept
{
resources.logger.enableLogging(prefix);

View file

@ -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
*
@ -383,6 +423,17 @@ public:
* @return false
*/
bool shouldReloadFile();
/**
* @brief Check if the tuning (scala) file should be reloaded.
*
* Depending on the platform this can create file descriptors.
*
* @return true if a scala file has been loaded and has changed
* @return false
*/
bool shouldReloadScala();
/**
* @brief Enable logging of timings to sidecar CSV files. This can produce
* many outputs so use with caution.

242
src/sfizz/Tuning.cpp Normal file
View file

@ -0,0 +1,242 @@
// 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 "absl/types/optional.h"
#include "Tunings.h" // Surge tuning library
#include <fstream>
#include <sstream>
#include <array>
#include <cmath>
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, absl::optional<fs::path> sourceFile = {});
bool shouldReloadScala();
void updateRootKey(int rootKey);
void updateTuningFrequency(float tuningFrequency);
void reset();
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)
};
absl::optional<fs::path> scalaFile_;
fs::file_time_type modificationTime_ {};
static constexpr int numKeys = Tunings::Tuning::N;
static constexpr int keyOffset = 256; // Surge tuning has key range ±256
std::array<float, numKeys> keysFractional12TET_;
};
void Tuning::Impl::reset()
{
rootKey_ = defaultRootKey;
tuningFrequency_ = defaultTuningFrequency;
tuning_ = Tunings::Tuning(
Tunings::evenTemperament12NoteScale(),
mappingFromParameters(defaultRootKey, defaultTuningFrequency)
);
scalaFile_.reset();
modificationTime_ = fs::file_time_type::min();
updateKeysFractional12TET();
}
float Tuning::Impl::getKeyFractional12TET(int midiKey) const
{
return keysFractional12TET_[std::max(0, std::min(numKeys - 1, midiKey + keyOffset))];
}
void Tuning::Impl::updateScale(const Tunings::Scale& scale, absl::optional<fs::path> sourceFile)
{
tuning_ = Tunings::Tuning(scale, tuning_.keyboardMapping);
updateKeysFractional12TET();
scalaFile_ = sourceFile;
if (sourceFile) {
std::error_code ec;
modificationTime_ = fs::last_write_time(*sourceFile, ec);
}
}
bool Tuning::Impl::shouldReloadScala()
{
DBG("Should reload scala called");
if (!scalaFile_)
return false;
std::error_code ec;
const auto newTime = fs::last_write_time(*scalaFile_, ec);
if (newTime > modificationTime_) {
DBG("File changed!");
modificationTime_ = newTime;
return true;
}
return false;
}
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 - keyOffset);
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)
{
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, path);
return true;
}
bool Tuning::loadScalaString(const std::string& text)
{
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);
}
bool Tuning::shouldReloadScala()
{
return impl_->shouldReloadScala();
}
} // namespace sfz

74
src/sfizz/Tuning.h Normal file
View file

@ -0,0 +1,74 @@
// 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 <memory>
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);
/**
* @brief Check whether the underlying scala file has changed.
*
*/
bool shouldReloadScala();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace sfz

View file

@ -81,7 +81,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
}
speedRatio = static_cast<float>(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<int>(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);
}

View file

@ -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();
@ -191,6 +221,11 @@ bool sfz::Sfizz::shouldReloadFile()
return synth->shouldReloadFile();
}
bool sfz::Sfizz::shouldReloadScala()
{
return synth->shouldReloadScala();
}
void sfz::Sfizz::enableLogging() noexcept
{
synth->enableLogging();

View file

@ -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<sfz::Synth*>(synth);
return self->loadScalaFile(path);
}
bool sfizz_load_scala_string(sfizz_synth_t* synth, const char* text)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
return self->loadScalaString(text);
}
void sfizz_set_scala_root_key(sfizz_synth_t* synth, int root_key)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
self->setScalaRootKey(root_key);
}
int sfizz_get_scala_root_key(sfizz_synth_t* synth)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
return self->getScalaRootKey();
}
void sfizz_set_tuning_frequency(sfizz_synth_t* synth, float frequency)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
self->setTuningFrequency(frequency);
}
float sfizz_get_tuning_frequency(sfizz_synth_t* synth)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
return self->getTuningFrequency();
}
void sfizz_free(sfizz_synth_t* synth)
{
delete reinterpret_cast<sfz::Synth*>(synth);
@ -239,6 +275,12 @@ bool sfizz_should_reload_file(sfizz_synth_t* synth)
return self->shouldReloadFile();
}
bool sfizz_should_reload_scala(sfizz_synth_t* synth)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);
return self->shouldReloadScala();
}
void sfizz_enable_logging(sfizz_synth_t* synth)
{
auto self = reinterpret_cast<sfz::Synth*>(synth);

View file

@ -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)

106
tests/Tuning.cpp Normal file
View file

@ -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 <string>
#include <cstdio>
#include <cstring>
#include <cmath>
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<std::string>())
("f,frequency", "Tuning frequency", cxxopts::value<float>()->default_value("440.0"))
("r,root-key", "Root key", cxxopts::value<std::string>()->default_value("C4"));
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<std::string>())) {
fprintf(stderr, "Could not load the scale file.\n");
return 1;
}
}
absl::optional<uint8_t> noteNumber = sfz::readNoteValue(
result["root-key"].as<std::string>());
if (!noteNumber) {
fprintf(stderr, "The root key is not a valid note name.\n");
return 1;
}
tuning.setScalaRootKey(*noteNumber);
tuning.setTuningFrequency(result["frequency"].as<float>());
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;
}

17
tests/TuningT.cpp Normal file
View file

@ -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)));
}