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