1 // padthv1_tuning.cpp 2 // 3 /**************************************************************************** 4 Copyright (C) 2012-2021, rncbc aka Rui Nuno Capela. All rights reserved. 5 6 This program is free software; you can redistribute it and/or 7 modify it under the terms of the GNU General Public License 8 as published by the Free Software Foundation; either version 2 9 of the License, or (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public License along 17 with this program; if not, write to the Free Software Foundation, Inc., 18 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 20 *****************************************************************************/ 21 22 //------------------------------------------------------------------------- 23 // TuningMap 24 // 25 // -- borrowed, stirred and refactored from amsynth -- 26 // https://github.com/amsynth/amsynth 27 // Copyright (C) 2001-2012 Nick Dowell 28 // 29 30 /* 31 * A TuningMap consists of two parts. 32 * 33 * The "key map" maps from MIDI note numbers to logical note numbers 34 * for the scale. This is often the identity mapping, but if your 35 * scale has, for example, 11 notes in it, you'll want to skip one 36 * per octave so the scale lines up with the pattern of keys on a 37 * standard keyboard. 38 * 39 * The "scale" maps from those logical note numbers to actual pitches. 40 * In terms of member variables, "scale" and "scaleDesc" belong to the 41 * scale, and everything else belongs to the mapping. 42 * 43 * For more information, refer to http://www.huygens-fokker.org/scala/ 44 */ 45 46 #include "padthv1_tuning.h" 47 48 #include <QTextStream> 49 #include <QFile> 50 51 #include <cmath> 52 53 54 // Default ctor. 55 padthv1_tuning::padthv1_tuning ( float refPitch, int refNote ) 56 { 57 reset(refPitch, refNote); 58 } 59 60 61 // Default is 12-tone equal temperament, wstern standard mapping. 62 void padthv1_tuning::reset ( float refPitch, int refNote ) 63 { 64 m_refPitch = refPitch; 65 m_refNote = refNote; 66 m_zeroNote = 0; 67 68 m_scale.clear(); 69 70 for (int i = 0; i < 12; ++i) 71 m_scale.push_back(::powf(2.0f, (i + 1) / 12.0f)); 72 73 m_mapRepeatInc = 1; 74 75 m_mapping.clear(); 76 m_mapping.push_back(0); 77 78 updateBasePitch(); 79 } 80 81 82 void padthv1_tuning::updateBasePitch (void) 83 { 84 // Clever, huh? 85 m_basePitch = 1.0f; 86 m_basePitch = m_refPitch / noteToPitch(m_refNote); 87 } 88 89 90 // Load custom Scala key-map file (.kbm) 91 bool padthv1_tuning::loadKeyMapFile ( const QString& keyMapFile ) 92 { 93 QFile file(keyMapFile); 94 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) 95 return false; 96 97 QTextStream fs(&file); 98 int mapSize = -1; 99 int firstNote = -1; 100 int lastNote = -1; 101 int zeroNote = -1; 102 int refNote = -1; 103 float refPitch = 0.0f; 104 int mapRepeatInc = -1; 105 QVector<int> mapping; 106 107 while (!fs.atEnd()) { 108 const QString& line 109 = fs.readLine().simplified(); 110 // Skip all-whitespace lines... 111 if (line.isEmpty()) 112 continue; 113 // Skip comment lines... 114 if (line.at(0) == '!') 115 continue; 116 bool ok = false; 117 const QString& val 118 = line.section(' ', 0, 0); 119 // An active range should be defined on this line... 120 if (line.at(0) == '<') { 121 // No overlap is checked for; 122 // it wouldn't hurt anything if ranges overlapped. 123 const int min = line.section(' ', 1, 1).toInt(&ok); 124 if (!ok || min < 0) 125 return false; 126 ok = false; 127 const int max = line.section(' ', 2, 2).toInt(&ok); 128 if (!ok || max < min || max > 127) 129 return false; 130 } 131 else 132 if (mapSize < 0) { 133 mapSize = val.toInt(&ok); 134 if (!ok || mapSize < 0) 135 return false; 136 } 137 else 138 if (firstNote < 0) { 139 firstNote = val.toInt(&ok); 140 if (!ok || firstNote < 0 || firstNote > 127) 141 return false; 142 } 143 else 144 if (lastNote < 0) { 145 lastNote = val.toInt(&ok); 146 if (!ok || lastNote < 0 || lastNote > 127) 147 return false; 148 } 149 else 150 if (zeroNote < 0) { 151 zeroNote = val.toInt(&ok); 152 if (!ok || zeroNote < 0 || zeroNote > 127) 153 return false; 154 } 155 else 156 if (refNote < 0) { 157 refNote = val.toInt(&ok); 158 if (!ok || refNote < 0 || refNote > 127) 159 return false; 160 } 161 else 162 if (refPitch <= 0.0f) { 163 refPitch = val.toFloat(&ok); 164 if (!ok || refPitch < 0.001f) 165 return false; 166 } 167 else 168 if (mapRepeatInc < 0) { 169 mapRepeatInc = val.toInt(&ok); 170 if (!ok || mapRepeatInc < 0) 171 return false; 172 } 173 else 174 if (line.at(0).toLower() == 'x') { 175 mapping.push_back(-1); // unmapped key 176 } 177 else { 178 const int mapEntry = val.toInt(&ok); 179 if (!ok || mapEntry < 0) 180 return false; 181 mapping.push_back(mapEntry); 182 } 183 } 184 185 // Didn't get far enough? 186 if (mapRepeatInc < 0) 187 return false; 188 189 // Special case for "automatic" linear mapping 190 if (mapSize == 0) { 191 if (!mapping.empty()) 192 return false; 193 m_keyMapFile = keyMapFile; 194 m_zeroNote = zeroNote; 195 m_refNote = refNote; 196 m_refPitch = refPitch; 197 m_mapRepeatInc = 1; 198 m_mapping.clear(); 199 m_mapping.push_back(0); 200 updateBasePitch(); 201 return true; 202 } 203 204 // Some of the kbm files included with Scala have 205 // extra x's at the end for no good reason 206 //if (mapping.size() > mapSize) 207 // return false; 208 209 mapping.resize(mapSize); 210 211 // Check to make sure reference pitch is actually mapped 212 int refIndex = (refNote - zeroNote) % mapSize; 213 if (refIndex < 0) 214 refIndex += mapSize; 215 if (mapping.at(refIndex) < 0) 216 return false; 217 218 m_keyMapFile = keyMapFile; 219 m_zeroNote = zeroNote; 220 m_refNote = refNote; 221 m_refPitch = refPitch; 222 223 if (mapRepeatInc == 0) 224 m_mapRepeatInc = mapSize; 225 else 226 m_mapRepeatInc = mapRepeatInc; 227 228 m_mapping = mapping; 229 230 updateBasePitch(); 231 return true; 232 } 233 234 235 // Load custom Scala scale file (.scl) 236 bool padthv1_tuning::loadScaleFile ( const QString& scaleFile ) 237 { 238 QFile file(scaleFile); 239 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) 240 return false; 241 242 QTextStream fs(&file); 243 QString scaleDesc; 244 int scaleSize = -1; 245 QVector<float> scale; 246 247 while (!fs.atEnd()) { 248 const QString& line 249 = fs.readLine().simplified(); 250 // Skip all-whitespace lines after description... 251 if (line.isEmpty() && !scaleDesc.isEmpty()) 252 continue; 253 // Skip comment lines 254 if (line.at(0) == '!') 255 continue; 256 if (scaleDesc.isEmpty()) 257 scaleDesc = line; 258 else 259 if (scaleSize < 0) { 260 bool ok = false; 261 scaleSize = line.section(' ', 0, 0).toInt(&ok); 262 if (!ok || scaleSize < 0) 263 return false; 264 } 265 else scale.push_back(parseScaleLine(line)); 266 } 267 268 if (scaleDesc.isEmpty() || scale.size() != scaleSize) 269 return false; 270 271 m_scaleFile = scaleFile; 272 m_scaleDesc = scaleDesc; 273 274 m_scale = scale; 275 276 updateBasePitch(); 277 return true; 278 } 279 280 281 // Convert a single line of a Scala scale file to a frequency relative to 1/1. 282 float padthv1_tuning::parseScaleLine ( const QString& line ) const 283 { 284 bool ok = false; 285 286 if (line.contains('.')) { 287 // Treat as cents... 288 const float cents = line.section(' ', 0, 0).toFloat(&ok); 289 if (!ok || cents < 0.001f) 290 return 0.0f; 291 else 292 return ::powf(2.0f, cents / 1200.0f); 293 } else { 294 // Treat as ratio... 295 const long n = line.section('/', 0, 0).toLong(&ok); 296 if (!ok || n < 0) 297 return 0.0f; 298 ok = false; 299 const long d = line.section('/', 1, 1).toLong(&ok); 300 if (!ok || d < 0) 301 return 0.0f; 302 else 303 return float(n) / float(d); 304 } 305 } 306 307 308 // The main pitch/frequency (Hz) getter. 309 float padthv1_tuning::noteToPitch ( int note ) const 310 { 311 if (note < 0 || note > 127 || m_mapping.empty()) 312 return 0.0f; 313 314 const int mapSize = m_mapping.size(); 315 316 int nRepeats = (note - m_zeroNote) / mapSize; 317 int mapIndex = (note - m_zeroNote) % mapSize; 318 319 if (mapIndex < 0) { 320 mapIndex += mapSize; 321 --nRepeats; 322 } 323 324 if (m_mapping.at(mapIndex) < 0) 325 return 0.0f; // unmapped note 326 327 const int scaleDegree = nRepeats * m_mapRepeatInc + m_mapping.at(mapIndex); 328 const int scaleSize = m_scale.size(); 329 330 int nOctaves = scaleDegree / scaleSize; 331 int scaleIndex = scaleDegree % scaleSize; 332 333 if (scaleIndex < 0) { 334 scaleIndex += scaleSize; 335 --nOctaves; 336 } 337 338 const float pitch 339 = m_basePitch * ::powf(m_scale.at(scaleSize - 1), nOctaves); 340 if (scaleIndex > 0) 341 return pitch * m_scale.at(scaleIndex - 1); 342 else 343 return pitch; 344 } 345 346 347 // end of padthv1_tuning.cpp 348