12 constexpr char kScalaFileExtension[] =
".scl";
13 constexpr char kKeyboardMapExtension[] =
".kbm";
14 constexpr char kTunFileExtension[] =
".tun";
15 constexpr int kDefaultMidiReference = 60;
16 constexpr char kScalaKbmComment =
'!';
17 constexpr char kTunComment =
';';
20 enum ScalaReadingState {
29 kStartMidiMapPosition,
31 kMidiMapMiddlePosition,
32 kReferenceNotePosition,
33 kReferenceFrequencyPosition,
38 enum TunReadingState {
44 String extractFirstToken(
const String& source) {
46 tokens.addTokens(source,
false);
51 float readCentsToTranspose(
const String& cents) {
56 float readRatioToTranspose(
const String& ratio) {
58 tokens.addTokens(ratio,
"/",
"");
59 float value = tokens[0].getIntValue();
61 if (tokens.size() == 2)
62 value /= tokens[1].getIntValue();
67 String readTunSection(
const String& line) {
68 return line.substring(1, line.length() - 1).toLowerCase();
71 bool isBaseFrequencyAssignment(
const String& line) {
72 return line.upToFirstOccurrenceOf(
"=",
false,
true).toLowerCase().trim() ==
"basefreq";
75 int getNoteAssignmentIndex(
const String& line) {
76 String variable = line.upToFirstOccurrenceOf(
"=",
false,
true);
78 tokens.addTokens(variable,
false);
79 if (tokens.size() <= 1 || tokens[0].toLowerCase() !=
"note")
81 int index = tokens[1].getIntValue();
87 float getAssignmentValue(
const String& line) {
88 String value = line.fromLastOccurrenceOf(
"=",
false,
true).trim();
89 return value.getFloatValue();
94 return String(
"*") + kScalaFileExtension + String(
";") +
95 String(
"*") + kKeyboardMapExtension + String(
";") +
96 String(
"*") + kTunFileExtension;
103 constexpr int kNotesInScale = 7;
104 constexpr int kOctaveStart = -1;
105 constexpr int kScale[kNotesInScale] = { -3, -1, 0, 2, 4, 5, 7 };
107 String text = note_text.toLowerCase().removeCharacters(
" ");
108 if (note_text.length() < 2)
111 char note_in_scale = text[0] -
'a';
112 if (note_in_scale < 0 || note_in_scale >= kNotesInScale)
115 int offset = kScale[note_in_scale];
116 text = text.substring(1);
117 if (text[0] ==
'#') {
118 text = text.substring(1);
121 else if (text[0] ==
'b') {
122 text = text.substring(1);
126 if (text.length() == 0)
129 bool negative =
false;
130 if (text[0] ==
'-') {
131 text = text.substring(1);
133 if (text.length() == 0)
136 int octave = text[0] -
'0';
139 octave = octave - kOctaveStart;
149 String extension = file.getFileExtension().toLowerCase();
150 if (extension == String(kScalaFileExtension))
152 else if (extension == String(kTunFileExtension))
154 else if (extension == String(kKeyboardMapExtension))
155 loadKeyboardMapFile(file);
162 ScalaReadingState state = kDescription;
164 int scale_length = 1;
165 std::vector<float> scale;
166 scale.push_back(0.0f);
168 for (
const String& line : scala_lines) {
169 String trimmed_line = line.trim();
171 if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
174 if (scale.size() >= scale_length + 1)
179 state = kScaleLength;
182 scale_length = extractFirstToken(trimmed_line).getIntValue();
183 state = kScaleRatios;
186 String tuning = extractFirstToken(trimmed_line);
187 if (tuning.contains(
"."))
188 scale.push_back(readCentsToTranspose(tuning));
190 scale.push_back(readRatioToTranspose(tuning));
196 keyboard_mapping_.clear();
197 for (
int i = 0; i < scale.size() - 1; ++i)
198 keyboard_mapping_.push_back(i);
199 scale_start_midi_note_ = kDefaultMidiReference;
200 reference_midi_note_ = 0;
208 scala_file.readLines(lines);
210 tuning_name_ = scala_file.getFileNameWithoutExtension().toStdString();
213void Tuning::loadKeyboardMapFile(File kbm_file) {
215 static constexpr int kHeaderSize = 7;
218 kbm_file.readLines(lines);
220 float header_data[kHeaderSize];
221 memset(header_data, 0, kHeaderSize *
sizeof(
float));
222 int header_position = 0;
224 int last_scale_value = 0;
225 keyboard_mapping_.clear();
227 for (
const String& line : lines) {
228 String trimmed_line = line.trim();
229 if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
232 if (header_position >= kHeaderSize) {
233 String token = extractFirstToken(trimmed_line);
234 if (token.toLowerCase()[0] !=
'x')
235 last_scale_value = token.getIntValue();
237 keyboard_mapping_.push_back(last_scale_value);
239 if (keyboard_mapping_.size() >= map_size)
243 header_data[header_position] = extractFirstToken(trimmed_line).getFloatValue();
244 if (header_position == kMapSizePosition)
245 map_size = header_data[header_position];
254 mapping_name_ = kbm_file.getFileNameWithoutExtension().toStdString();
257void Tuning::loadTunFile(File tun_file) {
259 keyboard_mapping_.clear();
261 TunReadingState state = kScanningForSection;
263 tun_file.readLines(lines);
265 int last_read_note = 0;
267 std::vector<float> scale;
271 for (
const String& line : lines) {
272 String trimmed_line = line.trim();
273 if (trimmed_line.length() == 0 || trimmed_line[0] == kTunComment)
276 if (trimmed_line[0] ==
'[') {
277 String section = readTunSection(trimmed_line);
278 if (section ==
"tuning")
280 else if (section ==
"exact tuning")
281 state = kExactTuning;
283 state = kScanningForSection;
285 else if (state == kTuning || state == kExactTuning) {
286 if (isBaseFrequencyAssignment(trimmed_line))
287 base_frequency = getAssignmentValue(trimmed_line);
289 int index = getNoteAssignmentIndex(trimmed_line);
290 last_read_note = std::max(last_read_note, index);
297 scale.resize(last_read_note + 1);
302 tuning_name_ = tun_file.getFileNameWithoutExtension().toStdString();
306 scale_start_midi_note_ = kDefaultMidiReference;
307 reference_midi_note_ = 0;
318 if (scale.size() <= 1) {
324 int scale_size =
static_cast<int>(scale.size() - 1);
325 int mapping_size = scale_size;
326 if (keyboard_mapping_.size())
327 mapping_size =
static_cast<int>(keyboard_mapping_.size());
329 float octave_offset = scale[scale_size];
331 int mapping_position = -
kTuningCenter - start_octave * mapping_size;
333 float current_offset = start_octave * octave_offset;
335 if (mapping_position >= mapping_size) {
336 current_offset += octave_offset;
337 mapping_position = 0;
340 int note_in_scale = mapping_position;
341 if (keyboard_mapping_.size())
342 note_in_scale = keyboard_mapping_[mapping_position];
344 tuning_[i] = current_offset + scale[note_in_scale];
363 keyboard_mapping_.clear();
371 int scale_offset = note - scale_start_midi_note_;
372 return tuning_[
kTuningCenter + scale_offset] + scale_start_midi_note_ + reference_midi_note_;
389 data[
"scale_start_midi_note"] = scale_start_midi_note_;
390 data[
"reference_midi_note"] = reference_midi_note_;
391 data[
"tuning_name"] = tuning_name_;
392 data[
"mapping_name"] = mapping_name_;
393 data[
"default"] = default_;
396 for (
float scale_value : scale_)
397 scale_data.push_back(scale_value);
398 data[
"scale"] = scale_data;
400 if (keyboard_mapping_.size()) {
402 for (
int mapping_value : keyboard_mapping_)
403 mapping_data.push_back(mapping_value);
404 data[
"mapping"] = mapping_data;
411 scale_start_midi_note_ = data[
"scale_start_midi_note"];
412 reference_midi_note_ = data[
"reference_midi_note"];
413 std::string tuning_name = data[
"tuning_name"];
414 tuning_name_ = tuning_name;
415 std::string mapping_name = data[
"mapping_name"];
416 mapping_name_ = mapping_name;
418 if (data.count(
"default"))
419 default_ = data[
"default"];
421 json scale_data = data[
"scale"];
423 for (
json& value : scale_data) {
424 float scale_value = value;
425 scale_.push_back(scale_value);
428 keyboard_mapping_.clear();
429 if (data.count(
"mapping")) {
430 json mapping_data = data[
"mapping"];
431 for (
json& value : mapping_data) {
432 int keyboard_value = value;
433 keyboard_mapping_.push_back(keyboard_value);
A class for managing microtonal tunings and custom pitch mappings in Vital.
Definition tuning.h:23
Tuning()
Constructs a Tuning object representing default (12-tone equal temperament) tuning.
Definition tuning.cpp:305
void setReferenceNoteFrequency(int midi_note, float frequency)
Sets the reference note and frequency pair.
Definition tuning.cpp:379
static constexpr int kTuningCenter
The center index of the tuning table.
Definition tuning.h:38
void setReferenceRatio(float ratio)
Sets the reference ratio, defining a pitch offset in ratio form.
Definition tuning.cpp:383
static String allFileExtensions()
Returns a string listing all supported tuning file extensions.
Definition tuning.cpp:93
void setStartMidiNote(int start_midi_note)
Sets the starting MIDI note for the scale mapping.
Definition tuning.h:121
static constexpr int kTuningSize
The total size of the internal tuning table.
Definition tuning.h:31
void setConstantTuning(float note)
Sets a constant tuning, making all notes map to the same pitch offset.
Definition tuning.cpp:349
json stateToJson() const
Saves the current tuning state into a JSON object.
Definition tuning.cpp:387
void loadScalaFile(const StringArray &scala_lines)
Loads a Scala (.scl) file from a set of lines.
Definition tuning.cpp:160
static int noteToMidiKey(const String ¬e)
Converts a note name (e.g. "A4") to a MIDI key number.
Definition tuning.cpp:99
void loadFile(File file)
Loads a tuning from a given file, automatically detecting its format (.scl, .kbm, ....
Definition tuning.cpp:147
static Tuning getTuningForFile(File file)
Creates a Tuning object from a given file.
Definition tuning.cpp:143
void loadScale(std::vector< float > scale)
Loads a custom scale from a vector of offsets, specified in semitones or transposition units.
Definition tuning.cpp:316
void setDefaultTuning()
Resets the tuning to the default 12-tone equal temperament with a standard reference pitch.
Definition tuning.cpp:354
vital::mono_float convertMidiNote(int note) const
Converts a MIDI note number to a pitch offset based on the current tuning.
Definition tuning.cpp:370
void setReferenceFrequency(float frequency)
Sets the reference frequency used for calculating pitches.
Definition tuning.cpp:375
void jsonToState(const json &data)
Restores the tuning state from a JSON object.
Definition tuning.cpp:410
nlohmann::json json
Definition line_generator.h:7
force_inline poly_float frequencyToMidiNote(poly_float value)
Converts a frequency to a MIDI note (vectorized).
Definition poly_utils.h:130
force_inline poly_float ratioToMidiTranspose(poly_float value)
Converts a ratio to MIDI transpose (vectorized).
Definition poly_utils.h:109
constexpr mono_float kMidi0Frequency
Frequency of MIDI note 0 (C-1).
Definition common.h:47
constexpr int kNotesPerOctave
Number of semitones per octave.
Definition common.h:51
constexpr int kMidiSize
MIDI note count (0-127).
Definition common.h:44
constexpr int kCentsPerNote
Number of cents per semitone.
Definition common.h:52
float mono_float
Definition common.h:33
Provides various utility functions, classes, and constants for audio, math, and general-purpose opera...