comparison src/BayesDrumTracker.cpp @ 0:0f9165f96bdb

started drum tracker project svn
author Andrew N Robertson <andrew.robertson@eecs.qmul.ac.uk>
date Tue, 30 Aug 2011 20:16:35 +0100
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:0f9165f96bdb
1 /*
2 * BayesDrumTracker.cpp
3 * bayesianTempoInitialiser5
4 *
5 * Created by Andrew on 14/07/2011.
6 * Copyright 2011 QMUL. All rights reserved.
7 *
8 */
9
10 #include "BayesDrumTracker.h"
11 #define OUTPORT 12346
12 #define HOST "localhost"
13
14
15 //beatCorrection process indicates how the phase is changing from max
16
17
18 BayesDrumTracker::BayesDrumTracker(){
19
20 initialiseTracker();
21 sender.setup( HOST, OUTPORT );
22 }
23
24
25 BayesDrumTracker::~BayesDrumTracker(){}
26
27 void BayesDrumTracker::initialiseTracker(){
28
29 beatDistribution.initialiseArray();
30 tempoDistribution.initialiseArray();
31 beatTimes.lastBeatTime = 0;
32 correctionFactor = 0.5;
33
34 tempoDistribution.likelihoodStdDev = ARRAY_SIZE / 32;
35 // tempoDistribution.likelihoodNoise = 0.96;
36 tempoDistribution.likelihoodNoise = 0.7;
37 tempoDistribution.setGaussianPrior(ARRAY_SIZE/2, ARRAY_SIZE/1);//wide
38
39 beatDistribution.likelihoodStdDev = ARRAY_SIZE / 32;
40 beatDistribution.likelihoodNoise = 0.56;
41 beatDistribution.setGaussianPrior(ARRAY_SIZE/2, ARRAY_SIZE/1);
42
43
44 tempoMinimum = 180;
45 tempoMaximum = 400;
46 posteriorMaximum = 0.1;
47
48 adaptiveStandardDeviationMode = false;
49 setDistributionOnStartTempo = true;
50
51 setBeatToNowTime = ofGetElapsedTimeMillis();
52 recentClickTime = ofGetElapsedTimeMillis();
53
54 resetParameters();
55 //check what we can delete above SINCE RESET CALLED
56
57 }
58
59
60 void BayesDrumTracker::resetParameters(){
61
62 beatTimes.startIndex = 0;
63 beatTimes.lastBeatTime = 0;
64 maxPhase = 0;
65 posteriorMaximum = 0.1;
66
67 accompanimentStarted = false;
68
69 tempoDistribution.likelihoodNoise = 0.8;
70 tempoDistribution.setGaussianPrior(ARRAY_SIZE/2, ARRAY_SIZE/2);//wide
71
72 beatDistribution.initialiseArray();
73 tempoDistribution.initialiseArray();
74
75 tempoDistribution.calculateStandardDeviation();
76 beatDistribution.calculateStandardDeviation();
77
78 tempoStdDev = tempoDistribution.standardDeviation;
79
80 beatTimes.resetBeatTimeArray();
81
82 }
83
84
85
86 void BayesDrumTracker::decayDistributions(){
87
88 if (accompanimentStarted){
89 tempoDistribution.decayPosteriorWithGaussianNoise ();
90 beatDistribution.decayPosteriorWithGaussianNoise();
91 }
92 else{
93 if (tempoStdDev < 0.8 && beatDistribution.standardDeviation < 5)
94 accompanimentStarted = true;
95
96 }
97 }
98
99
100 void BayesDrumTracker::setBeatDistribution(int beatPosition){
101 switch (beatPosition){
102 //early sixteenth is that the beat is a sixteenth earlier
103 case 0:
104 case 1:
105 case 11:
106 //i.e. these zones are interpreted as "on the beat"
107 beatDistribution.eighthNoteProportion = 0;
108 beatDistribution.earlySixteenthNoteProportion = 0;
109 beatDistribution.lateSixteenthNoteProportion = 0;
110 break;
111 //10 and 2 were here
112
113 case 2:
114 beatDistribution.eighthNoteProportion = 0;
115 beatDistribution.earlySixteenthNoteProportion = 0.25;//was 0.3 in Bayesian8
116 //i.e. a 25% chance it is early sixteenth - 75% that the beat actually lies here
117 beatDistribution.lateSixteenthNoteProportion = 0;
118 break;
119
120 case 3:
121 beatDistribution.eighthNoteProportion = 0;
122 beatDistribution.earlySixteenthNoteProportion = 0.3;//was 0.4 in Bayesian8 //half chance it is early
123 beatDistribution.lateSixteenthNoteProportion = 0;
124 break;
125
126 case 5:
127 case 6:
128 case 7:
129 beatDistribution.eighthNoteProportion = 0.3;//i.e. nearly half a chance we are on the 8th note
130 beatDistribution.earlySixteenthNoteProportion = 0;
131 beatDistribution.lateSixteenthNoteProportion = 0;
132 break;
133
134 case 4:
135 beatDistribution.eighthNoteProportion = 0;
136 beatDistribution.earlySixteenthNoteProportion = 0.25;//was 0.3 in Bayesian8
137 beatDistribution.lateSixteenthNoteProportion = 0.05;//was 0.2 in Bayesian8
138 //chsanged to 0.2 and 0.1 then back
139 break;
140
141 case 8:
142 beatDistribution.eighthNoteProportion = 0;
143 beatDistribution.earlySixteenthNoteProportion = 0.05;//was 0.2 in Bayesian8
144 beatDistribution.lateSixteenthNoteProportion = 0.25;//was 0.3 in Bayesian8
145 break;
146
147 case 9:
148 beatDistribution.eighthNoteProportion = 0;
149 beatDistribution.earlySixteenthNoteProportion = 0;
150 beatDistribution.lateSixteenthNoteProportion = 0.35;//was 0.4 in Bayesian8
151 break;
152
153 case 10:
154 beatDistribution.eighthNoteProportion = 0;
155 beatDistribution.earlySixteenthNoteProportion = 0;
156 beatDistribution.lateSixteenthNoteProportion = 0.25;//was 0.2 in Bayesian8
157 break;
158
159 }
160
161 }
162
163 void BayesDrumTracker::newKickError(const float& error, const double& cpuEventTime, const string& onsetTypeString){
164
165 onsetType = onsetTypeString;
166 cpuBeatTime = cpuEventTime;
167 kickError = error;
168
169 //printf("beat errror %f time %f\n", kickError, cpuBeatTime);
170
171 while (kickError > 0.5){
172 kickError -= 1;
173 }
174
175 if (paused != true){
176 updateTempoProcess(cpuBeatTime, onsetType);
177 //this also cross updates the distributions
178 beatTimes.beatMapTimeDifferences[beatTimes.beatSegment] = kickError*beatTimes.tatum;
179 }//end if paused
180
181
182
183 if (onsetType == "kick"){
184 if (accompanimentStarted)
185 beatDistribution.likelihoodNoise = 0.5;
186 else
187 beatDistribution.likelihoodNoise = 0.5;
188 // printf("kick %f ", cpuBeatTime);
189 }
190 else{
191 //snare
192 if (accompanimentStarted)
193 beatDistribution.likelihoodNoise = 0.7;
194 else
195 beatDistribution.likelihoodNoise = 0.85;
196 // printf("snare %f ", cpuBeatTime);
197 }
198
199
200 setBeatDistribution(beatTimes.beatSegment%12);
201
202 if (kickError <= 0.5 && kickError >= -0.5)
203 {
204 float beatStandardDeviation;
205 if (adaptiveStandardDeviationMode)
206 beatStandardDeviation = min((double)beatDistribution.likelihoodStdDev, beatDistribution.standardDeviation);
207 else
208 beatStandardDeviation = beatDistribution.likelihoodStdDev;
209
210
211 beatDistribution.resetPrior();
212 beatDistribution.setGaussianLikelihoodForBeats((ARRAY_SIZE/2)+(kickError*ARRAY_SIZE), beatStandardDeviation);
213 beatDistribution.calculatePosterior();
214 beatDistribution.renormalisePosterior();
215
216 sendMaxPhase();
217
218 beatDistribution.calculateStandardDeviation();
219
220 }//end if error < 0.5
221
222
223
224 if (beatTimes.beatSegment % 12 == 6){
225 kickString = "Kick ";
226 kickString += ofToString(kickError);
227 kickString += " ERROR ";
228 kickString += ofToString(kickError, 2);
229 kickString += " at time diff ";
230 kickString += ofToString(cpuBeatTime - beatTimes.lastClickTime, 2);
231 kickString += " index ";
232 kickString += ofToString(beatTimes.lastClickIndex, 2);
233 kickString += " TYPE ";
234 kickString += ofToString(beatTimes.beatSegment%12);
235 kickString += " Time diff ";
236 kickString += ofToString(beatTimes.timeDifference, 2);
237 }
238
239
240 }
241
242
243
244 void BayesDrumTracker::startTatum(const float& startTatum){
245 beatTimes.tatum = startTatum;
246
247 if (setDistributionOnStartTempo){
248 beatDistribution.setGaussianPosterior(ARRAY_SIZE/2, 8);
249 tempoDistribution.setGaussianPosterior(ARRAY_SIZE/2, 12);
250 float tmpIndex;
251 tmpIndex = ( (beatTimes.tatum - ((tempoMinimum+tempoMaximum)/2) ) * ARRAY_SIZE)/(tempoMaximum - tempoMinimum);
252 tempoDistribution.translateDistribution(tmpIndex);
253
254 sendMaxTempo();
255 }
256 }
257
258
259 void BayesDrumTracker::setUniformTempo(){
260 for (int i = 0;i < ARRAY_SIZE;i++)
261 tempoDistribution.posterior[i] = (float)1/ARRAY_SIZE;
262 }
263
264
265 void BayesDrumTracker::setUniformPhase(){
266 for (int i = 0;i < ARRAY_SIZE;i++)
267 beatDistribution.posterior[i] = (float)1/ARRAY_SIZE;
268 }
269
270 void BayesDrumTracker::setBeatNow(const double& beatTime){
271 for (int i = 0;i < ARRAY_SIZE;i++)
272 beatDistribution.prior[i] = (float)1/ARRAY_SIZE;
273
274 setBeatToNowTime = ofGetElapsedTimeMillis();
275 double difference = (setBeatToNowTime - recentClickTime);
276 printf("SET BEAT TO NOW %f vs %f :: diff %f tatum %f :: ", setBeatToNowTime, recentClickTime, difference, beatTimes.tatum );
277
278 double beatTimeToUse = 0;
279
280 if (difference < beatTimes.tatum)//tatum is the eighth note time
281 beatTimeToUse = difference/ (2*beatTimes.tatum);
282 else
283 beatTimeToUse = -1*(2*beatTimes.tatum - difference) / (2*beatTimes.tatum);
284
285 printf("sending %f \n", beatTimeToUse);
286
287 beatDistribution.setGaussianLikelihoodForBeats((ARRAY_SIZE/2)+(beatTimeToUse*ARRAY_SIZE), 2);
288 beatDistribution.calculatePosterior();
289 beatDistribution.renormalisePosterior();
290
291 sendMaxPhase();
292
293
294 }
295
296
297 void BayesDrumTracker::newBeat(int& beatIndex){
298 ofxOscMessage m;
299 m.setAddress( "/beatInfo" );
300
301 m.addFloatArg(beatTimes.tatum);
302 m.addFloatArg(maxPhase);
303
304 beatTimes.tatum = maxTempo;
305 printf("BEAT INFO %f, %f\n", beatTimes.tatum, maxPhase);
306
307 sender.sendMessage( m );
308
309 }
310
311 void BayesDrumTracker::sendMaxTempo(){
312 ofxOscMessage m;
313 m.setAddress( "/tempo" );
314
315 //maxTempo = tempoDistribution.maximumIndex * (tempoMaximum - tempoMinimum) / ARRAY_SIZE;
316 //would be introduced new in bayesian8
317 maxTempo = tempoDistribution.getIntegratedEstimateIndex() * (tempoMaximum - tempoMinimum) / ARRAY_SIZE;
318 maxTempo += tempoMinimum;
319
320 beatTimes.tatum = maxTempo;
321 printf("SEND TATUM %f\n", beatTimes.tatum);
322
323 m.addFloatArg( maxTempo );
324 sender.sendMessage( m );
325
326 //printf("max tempo %f\n", maxTempo);
327
328 }
329
330 void BayesDrumTracker::sendMaxPhase(){
331
332
333 // maxPhase = (beatDistribution.maximumIndex - (ARRAY_SIZE/2)) / ARRAY_SIZE;
334 maxPhase = (beatDistribution.getIntegratedEstimateIndex() - (ARRAY_SIZE/2)) / ARRAY_SIZE;
335 // printf("\nphase index %f :: %f\n", (float) beatDistribution.integratedEstimate , maxPhase);
336 ofxOscMessage m;
337 m.setAddress( "/phase" );
338 m.addFloatArg( maxPhase );
339 sender.sendMessage( m );
340
341 //beatCorrection = maxPhase * beatTimes.tatum / 4;
342 }
343
344
345 void BayesDrumTracker::setNewClickIndex(const int& clickIndex, const float& clickTime){
346
347 beatTimes.lastClickIndex = clickIndex;
348 beatTimes.lastClickTime = clickTime;
349
350 int clickIndexToUse = clickIndex % 16;
351 beatTimes.clickIndex = clickIndex;
352 beatTimes.clickNumber[clickIndexToUse] = clickIndex;
353 beatTimes.clickTimes[clickIndexToUse] = clickTime;
354
355 recentClickTime = ofGetElapsedTimeMillis();
356
357 }
358
359
360 void BayesDrumTracker::doBeatCorrection(const float& beatCorrFloat){
361 beatCorrection = beatCorrFloat;
362 correctBeatBy = round(correctionFactor * beatCorrection * ARRAY_SIZE / (2 * beatTimes.tatum));
363 beatDistribution.translateDistribution(-1 * correctBeatBy);
364 }
365
366
367 bool BayesDrumTracker::filterBeatTime(double newBeatTime){
368 bool newBeatFound = false;
369 if ((newBeatTime - beatTimes.lastBeatTime) > 20 || beatTimes.lastBeatTime == 0){
370
371 crossUpdateArrays((float)(newBeatTime - beatTimes.lastBeatTime));
372 beatTimes.lastBeatTime = newBeatTime;
373 newBeatFound = true;
374 }
375 return newBeatFound;
376 }
377
378 void BayesDrumTracker::crossUpdateArrays(float timeInterval){
379
380 int finalBeatIndex, tmpTempoIndex, startBeatIndex;
381 //finalBeat has contribution from BEAT[finalBeat + INT.k] * TEMPO[Max_tempo + k] where INT = INTERVAL
382 float interval;
383 interval = timeInterval / maxTempo;//beatTimes.tatum;
384 tempoDistribution.resetMaximumPosterior();
385 beatDistribution.resetMaximumPosterior();
386
387
388 int tmpBeatIndex;
389 //&& interval > 0.8 idea?
390 if (timeInterval > 0 && timeInterval < 12000 ){//need between 0 and 12 seconds only to update
391
392 for (tmpBeatIndex = 0;tmpBeatIndex < ARRAY_SIZE;tmpBeatIndex++){
393
394 tmpArray[tmpBeatIndex] = 0;
395 float minusMsecToMakeUp = beatIndexToMsec(tmpBeatIndex) / interval;
396 float plusMsecToMakeUp = beatIndexToMsec(ARRAY_SIZE - tmpBeatIndex) / interval;
397 float convertMsecToTempoIndex = ARRAY_SIZE / (tempoMaximum - tempoMinimum) ;
398
399
400 int minTempoIndex = -1 * (int)(minusMsecToMakeUp * convertMsecToTempoIndex);
401 int maxTempoIndex = (int)(plusMsecToMakeUp * convertMsecToTempoIndex);
402
403
404 if (tmpBeatIndex == beatDistribution.maximumIndex){
405 // minTmpDebug = tempoDistribution.maximumIndex + minTempoIndex;
406 // maxTmpDebug = tempoDistribution.maximumIndex + maxTempoIndex;
407 debugArray[0] = beatDistribution.maximumIndex;//
408 debugArray[1] = timeInterval;
409 debugArray[2] = interval;//beatDistribution.maximumIndex;
410 debugArray[3] = tempoDistribution.maximumIndex;
411 }
412
413 for (tmpTempoIndex = minTempoIndex;tmpTempoIndex <= maxTempoIndex;tmpTempoIndex++){
414
415 if ((tempoDistribution.maximumIndex + tmpTempoIndex) >= 0
416 && (tempoDistribution.maximumIndex + tmpTempoIndex) < ARRAY_SIZE
417 && (tmpBeatIndex - (int)(interval*tmpTempoIndex)) >= 0
418 && (tmpBeatIndex - (int)(interval*tmpTempoIndex))< ARRAY_SIZE){
419 tmpArray[tmpBeatIndex] += beatDistribution.posterior[tmpBeatIndex - (int)(interval*tmpTempoIndex)] * tempoDistribution.posterior[(int)tempoDistribution.maximumIndex + tmpTempoIndex];
420 }
421 }//end for tmpTmepo
422
423
424
425 }
426
427 float tmpFloat;
428 for (tmpBeatIndex = 0;tmpBeatIndex < ARRAY_SIZE;tmpBeatIndex++){
429 //debug - dont actually update::
430
431 tmpFloat = beatDistribution.posterior[tmpBeatIndex];
432 beatDistribution.posterior[tmpBeatIndex] = tmpArray[tmpBeatIndex];
433 tmpArray[tmpBeatIndex] = tmpFloat;
434 }
435 beatDistribution.renormaliseArray(&beatDistribution.posterior[0], ARRAY_SIZE);
436
437 } //end if
438
439
440 }
441
442
443 void BayesDrumTracker::updateTempoProcess(const double& cpuTime, const string& onsetDescription){
444
445 if (filterBeatTime(cpuTime) == true){
446 //checks for no repeat
447
448 if (onsetDescription == "kick")
449 beatTimes.addBeatTime(cpuTime, 1);
450 else
451 beatTimes.addBeatTime(cpuTime, 2);
452
453
454 //recalculate the distribution
455 int altIndex = 0;
456
457 tempoDataString = "Tatum :";
458 tempoDataString += ofToString(beatTimes.tatum, 2);
459 tempoDataString += " BPM ";
460 tempoDataString += ofToString((double)30000/beatTimes.tatum, 2);
461
462 timeString = "Last BEAT ";
463 timeString += ofToString(beatTimes.lastBeatTime);
464 timeString += " CLICK ";
465 timeString += ofToString(beatTimes.lastClickTime);
466 timeString += " DIFDF ";
467 timeString += ofToString(beatTimes.timeDifference);
468 timeString += " segment ";
469 timeString += ofToString(beatTimes.beatSegment);
470
471
472 for (altIndex = 0;altIndex< 16;altIndex++){
473 tempoInterval = beatTimes.intervalDifferences[beatTimes.index][altIndex];
474 integerMultipleOfTatum = beatTimes.relativeIntervals[altIndex][1];
475
476
477 ///NEW VERSION
478 tempoUpdateStrings[altIndex] = "";
479 double timeInterval = beatTimes.beatTimes[beatTimes.index] - beatTimes.beatTimes[altIndex];
480 //raw time difference
481 beatTimes.intervalDifferences[beatTimes.index][altIndex] = 0;
482 beatTimes.intervalUsed[beatTimes.index][altIndex] = false;
483
484 if (onsetType == "kick")
485 beatTimes.OnsetIsKick[beatTimes.index] = true;
486 else
487 beatTimes.OnsetIsKick[beatTimes.index] = false;
488
489
490
491 if (!accompanimentStarted){
492 //if we need to find tempo and start use this method
493 //we have 'started' once std dev is sufficiently low
494
495 updateTempoIfWithinRange(timeInterval);//taken as being the tatum interval
496
497
498
499 for (int i = 1;i <= 4;i++){
500 //we test the main beats and the two bar (16 tatum intervals)
501
502 double testInterval = timeInterval / 2*i;//pow(2, i);//pow(2.0, i);
503
504 if (updateTempoIfWithinRange(testInterval)){
505 //printf("test time %f, beats %i\n", testInterval, i);
506
507 beatTimes.intervalUsed[beatTimes.index][altIndex] = true;
508 beatTimes.intervalDifferences[beatTimes.index][altIndex] = testInterval;
509 //xx what if two within range here?
510
511 tempoUpdateStrings[altIndex] = "Tempo Updates (";
512 tempoUpdateStrings[altIndex] += ofToString(beatTimes.index, 0);
513 tempoUpdateStrings[altIndex] += ") : [";
514 tempoUpdateStrings[altIndex] += ofToString(altIndex);
515 tempoUpdateStrings[altIndex] += "]] : ";
516 tempoUpdateStrings[altIndex] += ofToString(timeInterval);
517 tempoUpdateStrings[altIndex] += ", ioi:";
518 tempoUpdateStrings[altIndex] += ofToString(i);
519 //tempoUpdateStrings[altIndex] += "";
520
521 }
522
523 }
524
525 double testInterval = timeInterval / 16;//pow(2, i);//pow(2.0, i);
526 if (updateTempoIfWithinRange(testInterval)){
527 beatTimes.intervalUsed[beatTimes.index][altIndex] = true;
528 beatTimes.intervalDifferences[beatTimes.index][altIndex] = testInterval;
529 }
530
531 }else{
532 //OLD VERSON
533 //THIS USES THE CURRENT TEMPO ESTIMATE TO DECIDE WHAT THE BEST INTERVAL IS
534 //&& integerMultipleOfTatum % 2 == 0 removed below XXX put back
535 if (altIndex != beatTimes.index && integerMultipleOfTatum < 17
536 && integerMultipleOfTatum > 0 && beatTimes.startIndex > 8//beattimes.index > 8 - the start
537 && integerMultipleOfTatum%2 == 0){//mod 2 - i.e. proper beat intervals only
538
539 double testInterval = timeInterval / integerMultipleOfTatum;
540
541 if (updateTempoIfWithinRange(testInterval)){
542
543 beatTimes.intervalUsed[beatTimes.index][altIndex] = true;
544 beatTimes.intervalDifferences[beatTimes.index][altIndex] = testInterval;
545
546 if (paused == false){
547 tempoUpdateStrings[altIndex] = "Tempo Updates : (";
548 tempoUpdateStrings[altIndex] += ofToString(beatTimes.index, 0);
549 tempoUpdateStrings[altIndex] += ") : [";
550 tempoUpdateStrings[altIndex] += ofToString(altIndex, 0);
551 tempoUpdateStrings[altIndex] += "] :: ";
552 tempoUpdateStrings[altIndex] += ofToString(integerMultipleOfTatum);
553 tempoUpdateStrings[altIndex] += " intervals :: ";
554 tempoUpdateStrings[altIndex] += ofToString(tempoInterval);
555 tempoUpdateStrings[altIndex] += " ms.";
556 // tempoUpdateStrings[altIndex] += ", ioi:";
557
558 // tempoUpdateStrings[altIndex] += ofToString(integerMultipleOfTatum);
559
560
561
562
563 }//end if not paused
564
565
566 }//end if good interval to update
567
568 }//end if not same index etc
569
570
571 }
572
573
574
575 }//end for all intervals
576
577 sendMaxTempo();
578 }//end if new beat time
579 double tempoEstimate = tempoDistribution.getIntegratedEstimateIndex();
580 tempoDistribution.calculateStandardDeviation();
581 tempoStdDev = tempoDistribution.standardDeviation;
582
583 }
584
585
586 bool BayesDrumTracker::updateTempoIfWithinRange(double timeInterval){
587
588 bool updated = false;
589
590 if (timeInterval > tempoMinimum && timeInterval < tempoMaximum ){
591 calculateTempoUpdate(timeInterval);
592 updated = true;
593 }
594
595 return updated;
596 }
597
598
599 void BayesDrumTracker::calculateTempoUpdate(double tempoInterval){
600
601
602 tempoDistribution.resetPrior();
603 //need to relook at likelihood for the tempo distribution - not the same as....
604 tempoDistribution.setGaussianLikelihood(ARRAY_SIZE * (tempoInterval-tempoMinimum)/(tempoMaximum - tempoMinimum), tempoDistribution.likelihoodStdDev);
605 tempoDistribution.calculatePosterior();
606 tempoDistribution.renormalisePosterior();
607
608 //did take pic of screen here - see initialiser4
609 }
610
611
612 float BayesDrumTracker::tempoIndexToMsec(const int& index){
613 float msec;
614 msec = index * (tempoMaximum - tempoMinimum) / ARRAY_SIZE;
615 msec += tempoMinimum;
616 return msec;
617 }
618
619 float BayesDrumTracker::beatIndexToMsec(const int& index){
620 float msec;
621 msec = index * maxTempo / ARRAY_SIZE;
622 msec += tempoMinimum;
623 return msec;
624 }
625