Just Intonation  Version 1.3.1 (19)
Explore key-independent dynamically adapting tuning in just intonation
tuneralgorithm.cpp
Go to the documentation of this file.
1 /*****************************************************************************
2  * Copyright 2016-2017 Karolin Stange, Christoph Wick, and Haye Hinrichsen
3  *
4  * This file is part of JustIntonation.
5  *
6  * JustIntonation is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * JustIntonation is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with JustIntonation. If not, see http://www.gnu.org/licenses/.
18  *****************************************************************************/
19 
20 //=============================================================================
21 // Main Tuning Algorithm
22 //=============================================================================
23 
24 #include "tuneralgorithm.h"
25 #include "tuner.h"
26 #include "tunerdebug.h"
27 
28 #include <QGenericMatrix>
29 
30 //-----------------------------------------------------------------------------
31 // Constructor
32 //-----------------------------------------------------------------------------
36 
38  : mSelectedVariant(128,128) // selected JI-variant of in interval
39  , mProgression(0) // pitch progression variable
40 {
41  // Reset just intonation interval sizes
42  mSelectedVariant.setZero();
43 }
44 
45 
46 //-----------------------------------------------------------------------------
47 // Tune in a statically defined unequal temperament
48 //-----------------------------------------------------------------------------
69 
71  const QVector<double> pitches,
72  const int referenceKey,
73  const int wolfsIntervalShift)
74 {
75  for (KeyData &key : keys) if (key.intensity > 0)
76  {
77  int distance = (key.key + 240 - referenceKey + wolfsIntervalShift)%12;
78  int A4dist = (69 + 240 - referenceKey + wolfsIntervalShift)%12;
79  key.pitch = pitches[distance]-pitches[A4dist];
80  }
81  mProgression = 0;
82 }
83 
84 
85 //-----------------------------------------------------------------------------
86 // Main Tuning Algorithm: Tune dynamically
87 //-----------------------------------------------------------------------------
108 
110  const QVector<double> intervals,
111  const QVector<double> weights,
112  bool optimizedJI)
113 {
114  using namespace Eigen;
115 
116  // optimizedJI=false; // turn off optimazation by force
117 
118  // First we set up maps <intensity,keynumber> of the
119  // playing keys (the ones which are audible) and the memorized
120  // key (the playing ones AND those that were audible short time ago).
121 
122  QMultiMap<double,int> audibleKeys, memorizedKeys;
123  for (auto &key : keyDataVector)
124  {
125  if (key.intensity>0) audibleKeys.insert (-key.intensity, key.key);
126  if (key.memory>0) memorizedKeys.insert(-key.memory, key.key);
127  }
128 
129  // The entries in the maps are automatically sorted by intensity.
130  // In order to limit the complexity of the tuning process, we
131  // take at most 8 keys and copy them to the vectors
132  // playingKeys and memorizedKeys:
133 
134  const int Pmax=8, Nmax=16;
135  QVector<int> keys = audibleKeys.values().mid(0,Pmax).toVector();
136  const int P = keys.size();
137  keys += memorizedKeys.values().mid(0,Nmax-P).toVector();
138  const int N = keys.size();
139 
140  if (P==0) return 0;
141 
142  // Copy pitch, define the significance as sum of the actual volume
143  // and the level of memorization. Find out whether we have newly
144  // pressed keys.
145  VectorXd pitch(N), significance(N);
146  bool newKeys = false;
147  for (int i=0; i<N; ++i)
148  {
149  KeyData& key = keyDataVector[keys[i]];
150  pitch(i) = key.pitch;
151  significance(i) = key.intensity + key.memory;
152  if (key.newlyPressed) newKeys = true;
153  }
154 
155  // Determine the properties of the intervals
156  MatrixXi variants(N,N), semitones(N,N), direction(N,N);
157  MatrixXd weight(N,N); weight.setZero();
158  for (int i=0; i<N; ++i) for (int j=0; j<N; ++j) if (i<P or j<P)
159  {
160  semitones(i,j) = abs(keys[j] - keys[i]);
161  direction(i,j) = (keys[j] >= keys[i] ? 1:-1);
162  variants(i,j) = cJustIntonation[semitones(i,j)%12].size();
163  if (i!=j)
164  {
165  weight(i,j) = weights[semitones(i,j)%12] *
166  std::pow(octaveWeightFactor, (semitones(i,j)-1)/12) *
167  sqrt(significance(i) * significance(j));
168  if (i>P or j>P) if (keys[i]!=keys[j]) weight(i,j) *= memoryWeight;
169  }
170  }
171 
172  // Construct the matrix A and its inverse AI (independent of optimization)
173  VectorXd diagonal = VectorXd(weight.array().rowwise().sum());
174  MatrixXd A = -weight.block(0,0,P,P) + MatrixXd::Identity(P,P)*epsilon
175  + MatrixXd(diagonal.head(P).asDiagonal());
176  MatrixXd AI = A.inverse();
177 
178  // Determine the interval sizes (optimized or without optimization)
179  MatrixXd interval(N,N); interval.setZero();
180 
181  if (optimizedJI and newKeys)
182  {
183  // ####################################################################
184  // Determine interval sizes for optimized JI according to the hardcoded
185  // in cJustIntonation in the header file, iterating over all possible
186  // combinations and searching for the lowest degree of tempering.
187 
188  MatrixXi m(N,N); m.setZero();
189  for (int i=0; i<N; i++) for (int j=0; j<N; j++) if (i<P or j<P)
190  {
191  if (keyDataVector[keys[i]].newlyPressed or
192  keyDataVector[keys[j]].newlyPressed) m(j,i)=m(i,j)=0;
193  else m(i,j) = mSelectedVariant(keys[i],keys[j]);
194  }
195  bool searching = true; // Flag for searching
196  MatrixXi mmin = m; // Optimal indices
197  double Pmin = 1e100; // Min value of dissipated power
198  QTimer timer; timer.start(20); // Timeout 20ms
199 
200  while (searching and timer.remainingTime()>0)
201  {
202  MatrixXd optimalInterval(N,N); optimalInterval.setZero();
203  for (int i=0; i<N; ++i) for (int j=0; j<N; ++j) if (i<P or j<P)
204  {
205  optimalInterval(i,j) = direction(i,j) *
206  cJustIntonation[semitones(i,j)%12][m(i,j)];
207  if (j>=P) optimalInterval(i,j) -= pitch(j);
208  if (i>=P) optimalInterval(i,j) += pitch(i);
209  }
210  VectorXd B = (optimalInterval * weight).diagonal().head(P)
211  - epsilon*pitch.head(P);
212  VectorXd V = -AI * B;
213  double C = (optimalInterval.array() * optimalInterval.array()
214  * weight.array()).sum() / 4;
215  double P = V.dot(A*V)/2 + B.dot(V) + C;
216  if (P<Pmin-1E-7) { Pmin=P; mmin=m; }
217 
218  // interate over all combinations:
219  searching = false;
220  for (int i=0; i<N; ++i) for (int j=0; j<N; ++j) if (i<P or j<P) if (i<j)
221  if (keyDataVector[keys[i]].newlyPressed
222  or keyDataVector[keys[j]].newlyPressed)
223  if (variants(i,j)>1 and not searching)
224  {
225  m(j,i) = m(i,j) = (m(i,j)+1) % variants(i,j);
226  if (m(i,j)>0) searching = true;
227  }
228  }
229 
230  // After finding optimum (or timeout) copy the optimal result
231  for (int i=0; i<N; i++) for (int j=0; j<N; j++) if (i<P or j<P)
232  {
233  mSelectedVariant(keys[i],keys[j]) = mmin(i,j);
234  interval(i,j) = direction(i,j) *
235  cJustIntonation[semitones(i,j)%12][mmin(i,j)];
236  }
237  // ####################################################################
238  }
239 
240  else // if not optimized
241  {
242  // ####################################################################
243  // Standard procedure: Non-optimized tuning according to a given table
244  // of interval sizes passed in the second parameter named 'intervals'
245  for (int i=0; i<N; ++i) for (int j=0; j<N; ++j) if (i<P or j<P)
246  interval(i,j) = direction(i,j) * intervals[semitones(i,j)%12];
247  }
248 
249  // Incorporate the -lambda terms on the right hand side (see paper)
250  for (int i=0; i<P; ++i) for (int j=P; j<N; ++j)
251  {
252  interval(i,j) -= pitch(j);
253  interval(j,i) += pitch(j);
254  }
255 
256  // Tune the keys microtonally:
257  VectorXd B = (interval * weight).diagonal().head(P) - epsilon*pitch.head(P);
258  VectorXd U = - AI * B;
259  for (int i=0; i<P; i++) keyDataVector[keys[i]].pitch = U(i);
260 
261  // Cancel newly-pressed-key flags
262  for (int i=0; i<P; ++i) keyDataVector[keys[i]].newlyPressed = false;
263 
264  // Compute potential as a measure of how much the chord is tempered
265  double C = (interval.array() * interval.array() * weight.array()).sum() / 4;
266  return C - B.dot(AI*B)/2;
267 }
const double octaveWeightFactor
void tuneStatically(KeyDataVector &keys, const QVector< double > pitches, const int referenceKey, const int wolfsIntervalShift)
Tune statically in a given unequal temperament (UT)
QVector< KeyData > KeyDataVector
Data of all keys of the keyboard.
Definition: tunerkeydata.h:60
double mProgression
Actual pitch progression.
const double epsilon
Structure holding the tuner&#39;s data of a single key.
Definition: tunerkeydata.h:32
bool newlyPressed
Flag indicating a newly pressed key.
Definition: tunerkeydata.h:36
TunerAlgorithm()
Constructor of the TunerAlgorithm.
double memory
Psychoacoustic memory M(t)
Definition: tunerkeydata.h:39
const double memoryWeight
const QVector< QVector< double > > cJustIntonation
major seventh 15/8
Eigen::MatrixXi mSelectedVariant
Previously used pitch variant.
double intensity
Intensity (volume) I(t)
Definition: tunerkeydata.h:38
double pitch
Actual pitch.
Definition: tunerkeydata.h:43
double tuneDynamically(KeyDataVector &keyData, const QVector< double > intervals, const QVector< double > weights, bool optimizedJI)
Main Tuninig Procedure: Tune dynamically.