Vital
Loading...
Searching...
No Matches
tuning.cpp
Go to the documentation of this file.
1/*
2Summary:
3The Tuning class provides a flexible microtonal tuning system for Vital. It can load multiple file formats, define custom scales, and set reference notes and frequencies. The code handles reading and interpreting different tuning standards (Scala, Tun), as well as mapping notes via keyboard mapping files. This allows Vital users to easily explore alternate tunings beyond standard equal temperament.
4 */
5
6#include "tuning.h"
7
8#include "utils.h"
9
10namespace {
11 // File extensions supported
12 constexpr char kScalaFileExtension[] = ".scl";
13 constexpr char kKeyboardMapExtension[] = ".kbm";
14 constexpr char kTunFileExtension[] = ".tun";
15 constexpr int kDefaultMidiReference = 60; // Common reference is middle C (MIDI note 60)
16 constexpr char kScalaKbmComment = '!';
17 constexpr char kTunComment = ';';
18
19 // Parsing states for Scala files
20 enum ScalaReadingState {
21 kDescription,
22 kScaleLength,
23 kScaleRatios
24 };
25
26 // Positions in KBM (keyboard map) header
27 enum KbmPositions {
28 kMapSizePosition,
29 kStartMidiMapPosition,
30 kEndMidiMapPosition,
31 kMidiMapMiddlePosition,
32 kReferenceNotePosition,
33 kReferenceFrequencyPosition,
34 kScaleDegreePosition,
35 };
36
37 // Parsing states for .tun files
38 enum TunReadingState {
39 kScanningForSection,
40 kTuning,
41 kExactTuning
42 };
43
44 String extractFirstToken(const String& source) {
45 StringArray tokens;
46 tokens.addTokens(source, false);
47 return tokens[0];
48 }
49
50 // Converts cents to semitones
51 float readCentsToTranspose(const String& cents) {
52 return cents.getFloatValue() / vital::kCentsPerNote;
53 }
54
55 // Converts a ratio like "3/2" or "2" to a MIDI transpose value
56 float readRatioToTranspose(const String& ratio) {
57 StringArray tokens;
58 tokens.addTokens(ratio, "/", "");
59 float value = tokens[0].getIntValue();
60
61 if (tokens.size() == 2)
62 value /= tokens[1].getIntValue();
63
65 }
66
67 String readTunSection(const String& line) {
68 return line.substring(1, line.length() - 1).toLowerCase();
69 }
70
71 bool isBaseFrequencyAssignment(const String& line) {
72 return line.upToFirstOccurrenceOf("=", false, true).toLowerCase().trim() == "basefreq";
73 }
74
75 int getNoteAssignmentIndex(const String& line) {
76 String variable = line.upToFirstOccurrenceOf("=", false, true);
77 StringArray tokens;
78 tokens.addTokens(variable, false);
79 if (tokens.size() <= 1 || tokens[0].toLowerCase() != "note")
80 return -1;
81 int index = tokens[1].getIntValue();
82 if (index < 0 || index >= vital::kMidiSize)
83 return -1;
84 return index;
85 }
86
87 float getAssignmentValue(const String& line) {
88 String value = line.fromLastOccurrenceOf("=", false, true).trim();
89 return value.getFloatValue();
90 }
91}
92
94 return String("*") + kScalaFileExtension + String(";") +
95 String("*") + kKeyboardMapExtension + String(";") +
96 String("*") + kTunFileExtension;
97}
98
99int Tuning::noteToMidiKey(const String& note_text) {
100 // Converts textual note names to MIDI keys (e.g., "A4" to 69)
101 // Implementation is a heuristic that attempts to parse note name and octave.
102 // Returns -1 on failure.
103 constexpr int kNotesInScale = 7;
104 constexpr int kOctaveStart = -1;
105 constexpr int kScale[kNotesInScale] = { -3, -1, 0, 2, 4, 5, 7 };
106
107 String text = note_text.toLowerCase().removeCharacters(" ");
108 if (note_text.length() < 2)
109 return -1;
110
111 char note_in_scale = text[0] - 'a';
112 if (note_in_scale < 0 || note_in_scale >= kNotesInScale)
113 return -1;
114
115 int offset = kScale[note_in_scale];
116 text = text.substring(1);
117 if (text[0] == '#') {
118 text = text.substring(1);
119 offset++;
120 }
121 else if (text[0] == 'b') {
122 text = text.substring(1);
123 offset--;
124 }
125
126 if (text.length() == 0)
127 return -1;
128
129 bool negative = false;
130 if (text[0] == '-') {
131 text = text.substring(1);
132 negative = true;
133 if (text.length() == 0)
134 return -1;
135 }
136 int octave = text[0] - '0';
137 if (negative)
138 octave = -octave;
139 octave = octave - kOctaveStart;
140 return vital::kNotesPerOctave * octave + offset;
141}
142
144 return Tuning(file);
145}
146
147void Tuning::loadFile(File file) {
148 // Detect file extension and load accordingly
149 String extension = file.getFileExtension().toLowerCase();
150 if (extension == String(kScalaFileExtension))
151 loadScalaFile(file);
152 else if (extension == String(kTunFileExtension))
153 loadTunFile(file);
154 else if (extension == String(kKeyboardMapExtension))
155 loadKeyboardMapFile(file);
156
157 default_ = false;
158}
159
160void Tuning::loadScalaFile(const StringArray& scala_lines) {
161 // Parse a Scala file from in-memory lines.
162 ScalaReadingState state = kDescription;
163
164 int scale_length = 1;
165 std::vector<float> scale;
166 scale.push_back(0.0f); // Root note is always 0.0f
167
168 for (const String& line : scala_lines) {
169 String trimmed_line = line.trim();
170 // Skip comments
171 if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
172 continue;
173
174 if (scale.size() >= scale_length + 1)
175 break;
176
177 switch (state) {
178 case kDescription:
179 state = kScaleLength;
180 break;
181 case kScaleLength:
182 scale_length = extractFirstToken(trimmed_line).getIntValue();
183 state = kScaleRatios;
184 break;
185 case kScaleRatios: {
186 String tuning = extractFirstToken(trimmed_line);
187 if (tuning.contains("."))
188 scale.push_back(readCentsToTranspose(tuning));
189 else
190 scale.push_back(readRatioToTranspose(tuning));
191 break;
192 }
193 }
194 }
195
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;
201
202 loadScale(scale);
203 default_ = false;
204}
205
206void Tuning::loadScalaFile(File scala_file) {
207 StringArray lines;
208 scala_file.readLines(lines);
209 loadScalaFile(lines);
210 tuning_name_ = scala_file.getFileNameWithoutExtension().toStdString();
211}
212
213void Tuning::loadKeyboardMapFile(File kbm_file) {
214 // Loads a keyboard mapping (.kbm) file that remaps scale degrees to MIDI keys.
215 static constexpr int kHeaderSize = 7;
216
217 StringArray lines;
218 kbm_file.readLines(lines);
219
220 float header_data[kHeaderSize];
221 memset(header_data, 0, kHeaderSize * sizeof(float));
222 int header_position = 0;
223 int map_size = 0;
224 int last_scale_value = 0;
225 keyboard_mapping_.clear();
226
227 for (const String& line : lines) {
228 String trimmed_line = line.trim();
229 if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
230 continue;
231
232 if (header_position >= kHeaderSize) {
233 String token = extractFirstToken(trimmed_line);
234 if (token.toLowerCase()[0] != 'x')
235 last_scale_value = token.getIntValue();
236
237 keyboard_mapping_.push_back(last_scale_value);
238
239 if (keyboard_mapping_.size() >= map_size)
240 break;
241 }
242 else {
243 header_data[header_position] = extractFirstToken(trimmed_line).getFloatValue();
244 if (header_position == kMapSizePosition)
245 map_size = header_data[header_position];
246 header_position++;
247 }
248 }
249
250 setStartMidiNote(header_data[kMidiMapMiddlePosition]);
251 setReferenceNoteFrequency(header_data[kReferenceNotePosition], header_data[kReferenceFrequencyPosition]);
252 loadScale(scale_);
253
254 mapping_name_ = kbm_file.getFileNameWithoutExtension().toStdString();
255}
256
257void Tuning::loadTunFile(File tun_file) {
258 // Loads a .tun file for tunings, a different format than Scala and KBM.
259 keyboard_mapping_.clear();
260
261 TunReadingState state = kScanningForSection;
262 StringArray lines;
263 tun_file.readLines(lines);
264
265 int last_read_note = 0;
266 float base_frequency = vital::kMidi0Frequency;
267 std::vector<float> scale;
268 for (int i = 0; i < vital::kMidiSize; ++i)
269 scale.push_back(i); // start linear
270
271 for (const String& line : lines) {
272 String trimmed_line = line.trim();
273 if (trimmed_line.length() == 0 || trimmed_line[0] == kTunComment)
274 continue;
275
276 if (trimmed_line[0] == '[') {
277 String section = readTunSection(trimmed_line);
278 if (section == "tuning")
279 state = kTuning;
280 else if (section == "exact tuning")
281 state = kExactTuning;
282 else
283 state = kScanningForSection;
284 }
285 else if (state == kTuning || state == kExactTuning) {
286 if (isBaseFrequencyAssignment(trimmed_line))
287 base_frequency = getAssignmentValue(trimmed_line);
288 else {
289 int index = getNoteAssignmentIndex(trimmed_line);
290 last_read_note = std::max(last_read_note, index);
291 if (index >= 0)
292 scale[index] = getAssignmentValue(trimmed_line) / vital::kCentsPerNote;
293 }
294 }
295 }
296
297 scale.resize(last_read_note + 1);
298
299 loadScale(scale);
301 setReferenceFrequency(base_frequency);
302 tuning_name_ = tun_file.getFileNameWithoutExtension().toStdString();
303}
304
305Tuning::Tuning() : default_(true) {
306 scale_start_midi_note_ = kDefaultMidiReference;
307 reference_midi_note_ = 0;
308
310}
311
312Tuning::Tuning(File file) : Tuning() {
313 loadFile(file);
314}
315
316void Tuning::loadScale(std::vector<float> scale) {
317 scale_ = scale;
318 if (scale.size() <= 1) {
319 // If no meaningful scale loaded, set to constant
320 setConstantTuning(kDefaultMidiReference);
321 return;
322 }
323
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());
328
329 float octave_offset = scale[scale_size];
330 int start_octave = -kTuningCenter / mapping_size - 1;
331 int mapping_position = -kTuningCenter - start_octave * mapping_size;
332
333 float current_offset = start_octave * octave_offset;
334 for (int i = 0; i < kTuningSize; ++i) {
335 if (mapping_position >= mapping_size) {
336 current_offset += octave_offset;
337 mapping_position = 0;
338 }
339
340 int note_in_scale = mapping_position;
341 if (keyboard_mapping_.size())
342 note_in_scale = keyboard_mapping_[mapping_position];
343
344 tuning_[i] = current_offset + scale[note_in_scale];
345 mapping_position++;
346 }
347}
348
350 for (int i = 0; i < kTuningSize; ++i)
351 tuning_[i] = note;
352}
353
355 // Default: 12-TET standard tuning
356 for (int i = 0; i < kTuningSize; ++i)
357 tuning_[i] = i - kTuningCenter;
358
359 scale_.clear();
360 for (int i = 0; i <= vital::kNotesPerOctave; ++i)
361 scale_.push_back(i);
362
363 keyboard_mapping_.clear();
364
365 default_ = true;
366 tuning_name_ = "";
367 mapping_name_ = "";
368}
369
371 int scale_offset = note - scale_start_midi_note_;
372 return tuning_[kTuningCenter + scale_offset] + scale_start_midi_note_ + reference_midi_note_;
373}
374
375void Tuning::setReferenceFrequency(float frequency) {
376 setReferenceNoteFrequency(0, frequency);
377}
378
379void Tuning::setReferenceNoteFrequency(int midi_note, float frequency) {
380 reference_midi_note_ = vital::utils::frequencyToMidiNote(frequency) - midi_note;
381}
382
383void Tuning::setReferenceRatio(float ratio) {
384 reference_midi_note_ = vital::utils::ratioToMidiTranspose(ratio);
385}
386
388 json data;
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_;
394
395 json scale_data;
396 for (float scale_value : scale_)
397 scale_data.push_back(scale_value);
398 data["scale"] = scale_data;
399
400 if (keyboard_mapping_.size()) {
401 json mapping_data;
402 for (int mapping_value : keyboard_mapping_)
403 mapping_data.push_back(mapping_value);
404 data["mapping"] = mapping_data;
405 }
406
407 return data;
408}
409
410void Tuning::jsonToState(const json& 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;
417
418 if (data.count("default"))
419 default_ = data["default"];
420
421 json scale_data = data["scale"];
422 scale_.clear();
423 for (json& value : scale_data) {
424 float scale_value = value;
425 scale_.push_back(scale_value);
426 }
427
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);
434 }
435 }
436
437 loadScale(scale_);
438}
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 &note)
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...