You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
12KB

  1. /* Copyright 2013-2019 Matt Tytel
  2. *
  3. * vital is free software: you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation, either version 3 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * vital is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License
  14. * along with vital. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #include "tuning.h"
  17. #include "utils.h"
  18. namespace {
  19. constexpr char kScalaFileExtension[] = ".scl";
  20. constexpr char kKeyboardMapExtension[] = ".kbm";
  21. constexpr char kTunFileExtension[] = ".tun";
  22. constexpr int kDefaultMidiReference = 60;
  23. constexpr char kScalaKbmComment = '!';
  24. constexpr char kTunComment = ';';
  25. enum ScalaReadingState {
  26. kDescription,
  27. kScaleLength,
  28. kScaleRatios
  29. };
  30. enum KbmPositions {
  31. kMapSizePosition,
  32. kStartMidiMapPosition,
  33. kEndMidiMapPosition,
  34. kMidiMapMiddlePosition,
  35. kReferenceNotePosition,
  36. kReferenceFrequencyPosition,
  37. kScaleDegreePosition,
  38. };
  39. enum TunReadingState {
  40. kScanningForSection,
  41. kTuning,
  42. kExactTuning
  43. };
  44. String extractFirstToken(const String& source) {
  45. StringArray tokens;
  46. tokens.addTokens(source, false);
  47. return tokens[0];
  48. }
  49. float readCentsToTranspose(const String& cents) {
  50. return cents.getFloatValue() / vital::kCentsPerNote;
  51. }
  52. float readRatioToTranspose(const String& ratio) {
  53. StringArray tokens;
  54. tokens.addTokens(ratio, "/", "");
  55. float value = tokens[0].getIntValue();
  56. if (tokens.size() == 2)
  57. value /= tokens[1].getIntValue();
  58. return vital::utils::ratioToMidiTranspose(value);
  59. }
  60. String readTunSection(const String& line) {
  61. return line.substring(1, line.length() - 1).toLowerCase();
  62. }
  63. bool isBaseFrequencyAssignment(const String& line) {
  64. return line.upToFirstOccurrenceOf("=", false, true).toLowerCase().trim() == "basefreq";
  65. }
  66. int getNoteAssignmentIndex(const String& line) {
  67. String variable = line.upToFirstOccurrenceOf("=", false, true);
  68. StringArray tokens;
  69. tokens.addTokens(variable, false);
  70. if (tokens.size() <= 1 || tokens[0].toLowerCase() != "note")
  71. return -1;
  72. int index = tokens[1].getIntValue();
  73. if (index < 0 || index >= vital::kMidiSize)
  74. return -1;
  75. return index;
  76. }
  77. float getAssignmentValue(const String& line) {
  78. String value = line.fromLastOccurrenceOf("=", false, true).trim();
  79. return value.getFloatValue();
  80. }
  81. }
  82. String Tuning::allFileExtensions() {
  83. return String("*") + kScalaFileExtension + String(";") +
  84. String("*") + kKeyboardMapExtension + String(";") +
  85. String("*") + kTunFileExtension;
  86. }
  87. int Tuning::noteToMidiKey(const String& note_text) {
  88. constexpr int kNotesInScale = 7;
  89. constexpr int kOctaveStart = -1;
  90. constexpr int kScale[kNotesInScale] = { -3, -1, 0, 2, 4, 5, 7 };
  91. String text = note_text.toLowerCase().removeCharacters(" ");
  92. if (note_text.length() < 2)
  93. return -1;
  94. char note_in_scale = text[0] - 'a';
  95. if (note_in_scale < 0 || note_in_scale >= kNotesInScale)
  96. return -1;
  97. int offset = kScale[note_in_scale];
  98. text = text.substring(1);
  99. if (text[0] == '#') {
  100. text = text.substring(1);
  101. offset++;
  102. }
  103. else if (text[0] == 'b') {
  104. text = text.substring(1);
  105. offset--;
  106. }
  107. if (text.length() == 0)
  108. return -1;
  109. bool negative = false;
  110. if (text[0] == '-') {
  111. text = text.substring(1);
  112. negative = true;
  113. if (text.length() == 0)
  114. return -1;
  115. }
  116. int octave = text[0] - '0';
  117. if (negative)
  118. octave = -octave;
  119. octave = octave - kOctaveStart;
  120. return vital::kNotesPerOctave * octave + offset;
  121. }
  122. Tuning Tuning::getTuningForFile(File file) {
  123. return Tuning(file);
  124. }
  125. void Tuning::loadFile(File file) {
  126. String extension = file.getFileExtension().toLowerCase();
  127. if (extension == String(kScalaFileExtension))
  128. loadScalaFile(file);
  129. else if (extension == String(kTunFileExtension))
  130. loadTunFile(file);
  131. else if (extension == String(kKeyboardMapExtension))
  132. loadKeyboardMapFile(file);
  133. default_ = false;
  134. }
  135. void Tuning::loadScalaFile(const StringArray& scala_lines) {
  136. ScalaReadingState state = kDescription;
  137. int scale_length = 1;
  138. std::vector<float> scale;
  139. scale.push_back(0.0f);
  140. for (const String& line : scala_lines) {
  141. String trimmed_line = line.trim();
  142. if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
  143. continue;
  144. if (scale.size() >= scale_length + 1)
  145. break;
  146. switch (state) {
  147. case kDescription:
  148. state = kScaleLength;
  149. break;
  150. case kScaleLength:
  151. scale_length = extractFirstToken(trimmed_line).getIntValue();
  152. state = kScaleRatios;
  153. break;
  154. case kScaleRatios: {
  155. String tuning = extractFirstToken(trimmed_line);
  156. if (tuning.contains("."))
  157. scale.push_back(readCentsToTranspose(tuning));
  158. else
  159. scale.push_back(readRatioToTranspose(tuning));
  160. break;
  161. }
  162. }
  163. }
  164. keyboard_mapping_.clear();
  165. for (int i = 0; i < scale.size() - 1; ++i)
  166. keyboard_mapping_.push_back(i);
  167. scale_start_midi_note_ = kDefaultMidiReference;
  168. reference_midi_note_ = 0;
  169. loadScale(scale);
  170. default_ = false;
  171. }
  172. void Tuning::loadScalaFile(File scala_file) {
  173. StringArray lines;
  174. scala_file.readLines(lines);
  175. loadScalaFile(lines);
  176. tuning_name_ = scala_file.getFileNameWithoutExtension().toStdString();
  177. }
  178. void Tuning::loadKeyboardMapFile(File kbm_file) {
  179. static constexpr int kHeaderSize = 7;
  180. StringArray lines;
  181. kbm_file.readLines(lines);
  182. float header_data[kHeaderSize];
  183. memset(header_data, 0, kHeaderSize * sizeof(float));
  184. int header_position = 0;
  185. int map_size = 0;
  186. int last_scale_value = 0;
  187. keyboard_mapping_.clear();
  188. for (const String& line : lines) {
  189. String trimmed_line = line.trim();
  190. if (trimmed_line.length() > 0 && trimmed_line[0] == kScalaKbmComment)
  191. continue;
  192. if (header_position >= kHeaderSize) {
  193. String token = extractFirstToken(trimmed_line);
  194. if (token.toLowerCase()[0] != 'x')
  195. last_scale_value = token.getIntValue();
  196. keyboard_mapping_.push_back(last_scale_value);
  197. if (keyboard_mapping_.size() >= map_size)
  198. break;
  199. }
  200. else {
  201. header_data[header_position] = extractFirstToken(trimmed_line).getFloatValue();
  202. if (header_position == kMapSizePosition)
  203. map_size = header_data[header_position];
  204. header_position++;
  205. }
  206. }
  207. setStartMidiNote(header_data[kMidiMapMiddlePosition]);
  208. setReferenceNoteFrequency(header_data[kReferenceNotePosition], header_data[kReferenceFrequencyPosition]);
  209. loadScale(scale_);
  210. mapping_name_ = kbm_file.getFileNameWithoutExtension().toStdString();
  211. }
  212. void Tuning::loadTunFile(File tun_file) {
  213. keyboard_mapping_.clear();
  214. TunReadingState state = kScanningForSection;
  215. StringArray lines;
  216. tun_file.readLines(lines);
  217. int last_read_note = 0;
  218. float base_frequency = vital::kMidi0Frequency;
  219. std::vector<float> scale;
  220. for (int i = 0; i < vital::kMidiSize; ++i)
  221. scale.push_back(i);
  222. for (const String& line : lines) {
  223. String trimmed_line = line.trim();
  224. if (trimmed_line.length() == 0 || trimmed_line[0] == kTunComment)
  225. continue;
  226. if (trimmed_line[0] == '[') {
  227. String section = readTunSection(trimmed_line);
  228. if (section == "tuning")
  229. state = kTuning;
  230. else if (section == "exact tuning")
  231. state = kExactTuning;
  232. else
  233. state = kScanningForSection;
  234. }
  235. else if (state == kTuning || state == kExactTuning) {
  236. if (isBaseFrequencyAssignment(trimmed_line))
  237. base_frequency = getAssignmentValue(trimmed_line);
  238. else {
  239. int index = getNoteAssignmentIndex(trimmed_line);
  240. last_read_note = std::max(last_read_note, index);
  241. if (index >= 0)
  242. scale[index] = getAssignmentValue(trimmed_line) / vital::kCentsPerNote;
  243. }
  244. }
  245. }
  246. scale.resize(last_read_note + 1);
  247. loadScale(scale);
  248. setStartMidiNote(0);
  249. setReferenceFrequency(base_frequency);
  250. tuning_name_ = tun_file.getFileNameWithoutExtension().toStdString();
  251. }
  252. Tuning::Tuning() : default_(true) {
  253. scale_start_midi_note_ = kDefaultMidiReference;
  254. reference_midi_note_ = 0;
  255. setDefaultTuning();
  256. }
  257. Tuning::Tuning(File file) : Tuning() {
  258. loadFile(file);
  259. }
  260. void Tuning::loadScale(std::vector<float> scale) {
  261. scale_ = scale;
  262. if (scale.size() <= 1) {
  263. setConstantTuning(kDefaultMidiReference);
  264. return;
  265. }
  266. int scale_size = static_cast<int>(scale.size() - 1);
  267. int mapping_size = scale_size;
  268. if (keyboard_mapping_.size())
  269. mapping_size = static_cast<int>(keyboard_mapping_.size());
  270. float octave_offset = scale[scale_size];
  271. int start_octave = -kTuningCenter / mapping_size - 1;
  272. int mapping_position = -kTuningCenter - start_octave * mapping_size;
  273. float current_offset = start_octave * octave_offset;
  274. for (int i = 0; i < kTuningSize; ++i) {
  275. if (mapping_position >= mapping_size) {
  276. current_offset += octave_offset;
  277. mapping_position = 0;
  278. }
  279. int note_in_scale = mapping_position;
  280. if (keyboard_mapping_.size())
  281. note_in_scale = keyboard_mapping_[mapping_position];
  282. tuning_[i] = current_offset + scale[note_in_scale];
  283. mapping_position++;
  284. }
  285. }
  286. void Tuning::setConstantTuning(float note) {
  287. for (int i = 0; i < kTuningSize; ++i)
  288. tuning_[i] = note;
  289. }
  290. void Tuning::setDefaultTuning() {
  291. for (int i = 0; i < kTuningSize; ++i)
  292. tuning_[i] = i - kTuningCenter;
  293. scale_.clear();
  294. for (int i = 0; i <= vital::kNotesPerOctave; ++i)
  295. scale_.push_back(i);
  296. keyboard_mapping_.clear();
  297. default_ = true;
  298. tuning_name_ = "";
  299. mapping_name_ = "";
  300. }
  301. vital::mono_float Tuning::convertMidiNote(int note) const {
  302. int scale_offset = note - scale_start_midi_note_;
  303. return tuning_[kTuningCenter + scale_offset] + scale_start_midi_note_ + reference_midi_note_;
  304. }
  305. void Tuning::setReferenceFrequency(float frequency) {
  306. setReferenceNoteFrequency(0, frequency);
  307. }
  308. void Tuning::setReferenceNoteFrequency(int midi_note, float frequency) {
  309. reference_midi_note_ = vital::utils::frequencyToMidiNote(frequency) - midi_note;
  310. }
  311. void Tuning::setReferenceRatio(float ratio) {
  312. reference_midi_note_ = vital::utils::ratioToMidiTranspose(ratio);
  313. }
  314. json Tuning::stateToJson() const {
  315. json data;
  316. data["scale_start_midi_note"] = scale_start_midi_note_;
  317. data["reference_midi_note"] = reference_midi_note_;
  318. data["tuning_name"] = tuning_name_;
  319. data["mapping_name"] = mapping_name_;
  320. data["default"] = default_;
  321. json scale_data;
  322. for (float scale_value : scale_)
  323. scale_data.push_back(scale_value);
  324. data["scale"] = scale_data;
  325. if (keyboard_mapping_.size()) {
  326. json mapping_data;
  327. for (int mapping_value : keyboard_mapping_)
  328. mapping_data.push_back(mapping_value);
  329. data["mapping"] = mapping_data;
  330. }
  331. return data;
  332. }
  333. void Tuning::jsonToState(const json& data) {
  334. scale_start_midi_note_ = data["scale_start_midi_note"];
  335. reference_midi_note_ = data["reference_midi_note"];
  336. std::string tuning_name = data["tuning_name"];
  337. tuning_name_ = tuning_name;
  338. std::string mapping_name = data["mapping_name"];
  339. mapping_name_ = mapping_name;
  340. if (data.count("default"))
  341. default_ = data["default"];
  342. json scale_data = data["scale"];
  343. scale_.clear();
  344. for (json& value : scale_data) {
  345. float scale_value = value;
  346. scale_.push_back(scale_value);
  347. }
  348. keyboard_mapping_.clear();
  349. if (data.count("mapping")) {
  350. json mapping_data = data["mapping"];
  351. for (json& value : mapping_data) {
  352. int keyboard_value = value;
  353. keyboard_mapping_.push_back(keyboard_value);
  354. }
  355. }
  356. loadScale(scale_);
  357. }