annotate src/Finder.cpp @ 171:bb4507f24dc9 structure

Tidy up
author Chris Cannam
date Fri, 06 Feb 2015 13:07:30 +0000
parents 2b61e0cb6847
children 30d59e1e4232
rev   line source
cannam@0 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
cannam@0 2
cannam@0 3 /*
cannam@0 4 Vamp feature extraction plugin using the MATCH audio alignment
cannam@0 5 algorithm.
cannam@0 6
cannam@0 7 Centre for Digital Music, Queen Mary, University of London.
cannam@0 8 This file copyright 2007 Simon Dixon, Chris Cannam and QMUL.
cannam@0 9
cannam@0 10 This program is free software; you can redistribute it and/or
cannam@0 11 modify it under the terms of the GNU General Public License as
cannam@0 12 published by the Free Software Foundation; either version 2 of the
cannam@0 13 License, or (at your option) any later version. See the file
cannam@0 14 COPYING included with this distribution for more information.
cannam@0 15 */
cannam@0 16
cannam@0 17 #include "Finder.h"
cannam@0 18
Chris@30 19 #include "Path.h"
Chris@30 20
Chris@30 21 #include <algorithm>
Chris@92 22 #include <iomanip>
Chris@30 23
Chris@72 24 using namespace std;
cannam@0 25
Chris@140 26 //#define DEBUG_FINDER 1
Chris@140 27 //#define PERFORM_ERROR_CHECKS 1
Chris@140 28
Chris@72 29 Finder::Finder(Matcher *pm)
cannam@0 30 {
Chris@72 31 m_m = pm;
Chris@72 32 m_duration1 = -1;
Chris@72 33 m_duration2 = -1;
cannam@0 34 } // constructor
cannam@0 35
cannam@0 36 Finder::~Finder()
cannam@0 37 {
cannam@0 38 }
cannam@0 39
Chris@60 40 void
Chris@154 41 Finder::setMatcher(Matcher *pm)
Chris@154 42 {
Chris@155 43 cerr << "Finder::setMatcher: finder " << this << ", matcher " << pm << endl;
Chris@154 44 m_m = pm;
Chris@154 45 }
Chris@154 46
Chris@154 47 void
Chris@60 48 Finder::setDurations(int d1, int d2)
Chris@60 49 {
Chris@140 50 #ifdef DEBUG_FINDER
Chris@140 51 cerr << "*** setDurations: " << d1 << ", " << d2 << endl;
Chris@140 52 #endif
Chris@72 53 m_duration1 = d1;
Chris@72 54 m_duration2 = d2;
Chris@60 55 }
Chris@60 56
Chris@154 57 bool
Chris@154 58 Finder::getBestRowCost(int row, int &bestCol, double &min)
Chris@154 59 {
Chris@155 60 cerr << "Finder " << this << "::getBestRowCost(" << row << ")" << endl;
Chris@154 61 if (!m_m->isRowAvailable(row)) {
Chris@154 62 cerr << "row not available: " << row << endl;
Chris@154 63 return false;
Chris@154 64 }
Chris@154 65 pair<int, int> colRange = m_m->getColRange(row);
Chris@154 66 if (colRange.first >= colRange.second) {
Chris@154 67 cerr << "row " << row << " has invalid col range " << colRange.first
Chris@154 68 << " -> " << colRange.second << endl;
Chris@154 69 return false;
Chris@154 70 }
Chris@154 71 for (int index = colRange.first; index < colRange.second; index++) {
Chris@154 72 double tmp = m_m->getNormalisedPathCost(row, index);
Chris@154 73 if (index == colRange.first || tmp < min) {
Chris@154 74 min = tmp;
Chris@154 75 bestCol = index;
Chris@154 76 }
Chris@154 77 }
Chris@154 78 return true;
Chris@154 79 }
Chris@154 80
Chris@154 81 bool
Chris@154 82 Finder::getBestColCost(int col, int &bestRow, double &min)
Chris@154 83 {
Chris@154 84 if (!m_m->isColAvailable(col)) return false;
Chris@154 85 pair<int, int> rowRange = m_m->getRowRange(col);
Chris@154 86 if (rowRange.first >= rowRange.second) return false;
Chris@154 87 for (int index = rowRange.first; index < rowRange.second; index++) {
Chris@154 88 double tmp = m_m->getNormalisedPathCost(index, col);
Chris@154 89 if (index == rowRange.first || tmp < min) {
Chris@154 90 min = tmp;
Chris@154 91 bestRow = index;
Chris@154 92 }
Chris@154 93 }
Chris@154 94 return true;
Chris@154 95 }
Chris@154 96
Chris@147 97 void
Chris@147 98 Finder::getBestEdgeCost(int row, int col,
Chris@147 99 int &bestRow, int &bestCol,
Chris@147 100 double &min)
cannam@0 101 {
Chris@147 102 min = m_m->getPathCost(row, col);
Chris@72 103
Chris@147 104 bestRow = row;
Chris@147 105 bestCol = col;
Chris@72 106
Chris@72 107 pair<int, int> rowRange = m_m->getRowRange(col);
Chris@72 108 if (rowRange.second > row+1) {
Chris@72 109 rowRange.second = row+1; // don't cheat by looking at future :)
Chris@72 110 }
Chris@72 111 for (int index = rowRange.first; index < rowRange.second; index++) {
Chris@135 112 double tmp = m_m->getNormalisedPathCost(index, col);
cannam@0 113 if (tmp < min) {
cannam@0 114 min = tmp;
cannam@0 115 bestRow = index;
cannam@0 116 }
cannam@0 117 }
Chris@72 118
Chris@72 119 pair<int, int> colRange = m_m->getColRange(row);
Chris@72 120 if (colRange.second > col+1) {
Chris@72 121 colRange.second = col+1; // don't cheat by looking at future :)
Chris@72 122 }
Chris@72 123 for (int index = colRange.first; index < colRange.second; index++) {
Chris@135 124 double tmp = m_m->getNormalisedPathCost(row, index);
cannam@0 125 if (tmp < min) {
cannam@0 126 min = tmp;
cannam@0 127 bestCol = index;
cannam@0 128 bestRow = row;
cannam@0 129 }
cannam@0 130 }
Chris@147 131 }
Chris@72 132
Chris@147 133 Matcher::Advance
Chris@171 134 Finder::getExpandDirection()
Chris@171 135 {
Chris@171 136 return getExpandDirection(m_m->getFrameCount() - 1,
Chris@171 137 m_m->getOtherFrameCount() - 1);
Chris@171 138 }
Chris@171 139
Chris@171 140 Matcher::Advance
Chris@147 141 Finder::getExpandDirection(int row, int col)
Chris@147 142 {
Chris@147 143 // To determine which direction to expand the search area in, we
Chris@147 144 // look at the path costs along the leading edges of the search
Chris@147 145 // area (the final row and column within the area). We find the
Chris@147 146 // lowest path cost within the final row, and the lowest within
Chris@147 147 // the final column, and we compare them. If the row is cheaper
Chris@147 148 // then we expand by adding another row next to it; if the column
Chris@147 149 // is cheaper then we expand by adding another column next to
Chris@147 150 // it. (The overall lowest path cost across the row and column
Chris@147 151 // represents the best alignment we have within the entire search
Chris@147 152 // area given the data available and the assumption that the piece
Chris@147 153 // is not ending yet.)
Chris@147 154
Chris@147 155 int bestRow = row;
Chris@147 156 int bestCol = col;
Chris@147 157 double bestCost = -1;
Chris@147 158
Chris@155 159 // cerr << "Finder " << this << "::getExpandDirection: ";
Chris@155 160
Chris@147 161 getBestEdgeCost(row, col, bestRow, bestCol, bestCost);
Chris@147 162
Chris@147 163 // cerr << "at [" << row << "," << col << "] (cost " << m_m->getPathCost(row, col) << ") blocksize = " << m_m->getBlockSize() << " best is [" << bestRow << "," << bestCol << "] (cost " << bestCost << ")" << endl;
Chris@135 164
Chris@45 165 if (bestRow == row) {
Chris@45 166 if (bestCol == col) {
Chris@45 167 return Matcher::AdvanceBoth;
Chris@45 168 } else {
Chris@45 169 return Matcher::AdvanceThis;
Chris@45 170 }
Chris@45 171 } else if (bestCol == col) {
Chris@45 172 return Matcher::AdvanceOther;
Chris@45 173 } else {
Chris@46 174 return Matcher::AdvanceNone;
Chris@45 175 }
Chris@73 176 }
cannam@0 177
cannam@0 178 void
cannam@0 179 Finder::recalculatePathCostMatrix(int r1, int c1, int r2, int c2)
cannam@0 180 {
Chris@72 181 int prevRowStart = 0, prevRowStop = 0;
Chris@72 182
Chris@83 183 float diagonalWeight = m_m->getDiagonalWeight();
Chris@83 184
Chris@72 185 for (int r = r1; r <= r2; r++) {
Chris@72 186
Chris@72 187 pair<int, int> colRange = m_m->getColRange(r);
Chris@72 188
Chris@72 189 int rowStart = max(c1, colRange.first);
Chris@72 190 int rowStop = min(c2 + 1, colRange.second);
Chris@72 191
Chris@72 192 for (int c = rowStart; c < rowStop; c++) {
Chris@72 193
Chris@72 194 float newCost = m_m->getDistance(r, c);
Chris@72 195 Matcher::Advance dir = Matcher::AdvanceNone;
Chris@72 196
Chris@72 197 if (r > r1) { // not first row
Chris@72 198 double min = -1;
Chris@72 199 if ((c > prevRowStart) && (c <= prevRowStop)) {
Chris@72 200 // diagonal from (r-1,c-1)
Chris@83 201 min = m_m->getPathCost(r-1, c-1) + newCost * diagonalWeight;
Chris@72 202 dir = Matcher::AdvanceBoth;
Chris@72 203 }
Chris@72 204 if ((c >= prevRowStart) && (c < prevRowStop)) {
Chris@72 205 // vertical from (r-1,c)
Chris@72 206 double cost = m_m->getPathCost(r-1, c) + newCost;
Chris@72 207 if ((min < 0) || (cost < min)) {
Chris@72 208 min = cost;
Chris@72 209 dir = Matcher::AdvanceThis;
Chris@72 210 }
Chris@72 211 }
Chris@72 212 if (c > rowStart) {
Chris@72 213 // horizontal from (r,c-1)
Chris@72 214 double cost = m_m->getPathCost(r, c-1) + newCost;
Chris@72 215 if ((min < 0) || (cost < min)) {
Chris@72 216 min = cost;
Chris@72 217 dir = Matcher::AdvanceOther;
Chris@72 218 }
Chris@72 219 }
Chris@72 220
Chris@72 221 m_m->setPathCost(r, c, dir, min);
Chris@72 222
Chris@72 223 } else if (c > rowStart) { // first row
Chris@72 224 // horizontal from (r,c-1)
Chris@72 225 m_m->setPathCost(r, c, Matcher::AdvanceOther,
Chris@72 226 m_m->getPathCost(r, c-1) + newCost);
Chris@72 227 }
Chris@72 228 }
Chris@72 229
Chris@72 230 prevRowStart = rowStart;
Chris@72 231 prevRowStop = rowStop;
cannam@0 232 }
Chris@72 233 }
Chris@30 234
Chris@82 235 #ifdef PERFORM_ERROR_CHECKS
Chris@81 236 Finder::ErrorPosition
Chris@81 237 Finder::checkPathCostMatrix()
Chris@81 238 {
Chris@81 239 ErrorPosition err;
Chris@81 240
Chris@81 241 int r1 = 0;
Chris@81 242 int c1 = 0;
Chris@81 243 int r2 = m_m->getFrameCount() - 1;
Chris@81 244 int c2 = m_m->getOtherFrameCount() - 1;
Chris@81 245
Chris@81 246 if (r2 < r1 || c2 < c1) {
Chris@81 247 return err;
Chris@81 248 }
Chris@81 249
Chris@81 250 int prevRowStart = 0, prevRowStop = 0;
Chris@81 251
Chris@83 252 float diagonalWeight = m_m->getDiagonalWeight();
Chris@83 253
Chris@81 254 for (int r = r1; r <= r2; r++) {
Chris@81 255
Chris@81 256 pair<int, int> colRange = m_m->getColRange(r);
Chris@81 257
Chris@81 258 int rowStart = max(c1, colRange.first);
Chris@81 259 int rowStop = min(c2 + 1, colRange.second);
Chris@81 260
Chris@81 261 for (int c = rowStart; c < rowStop; c++) {
Chris@81 262
Chris@81 263 float newCost = m_m->getDistance(r, c);
Chris@81 264 double updateTo = -1.0;
Chris@81 265 Matcher::Advance dir = Matcher::AdvanceNone;
Chris@81 266
Chris@95 267 if (r > r1) { // not first row
Chris@81 268 double min = -1;
Chris@81 269 if ((c > prevRowStart) && (c <= prevRowStop)) {
Chris@81 270 // diagonal from (r-1,c-1)
Chris@83 271 min = m_m->getPathCost(r-1, c-1) + newCost * diagonalWeight;
Chris@81 272 err.prevCost = m_m->getPathCost(r-1, c-1);
Chris@83 273 err.distance = newCost * diagonalWeight;
Chris@81 274 dir = Matcher::AdvanceBoth;
Chris@81 275 }
Chris@81 276 if ((c >= prevRowStart) && (c < prevRowStop)) {
Chris@81 277 // vertical from (r-1,c)
Chris@81 278 double cost = m_m->getPathCost(r-1, c) + newCost;
Chris@81 279 if ((min < 0) || (cost < min)) {
Chris@81 280 min = cost;
Chris@81 281 err.prevCost = m_m->getPathCost(r-1, c);
Chris@81 282 err.distance = newCost;
Chris@81 283 dir = Matcher::AdvanceThis;
Chris@81 284 }
Chris@81 285 }
Chris@81 286 if (c > rowStart) {
Chris@81 287 // horizontal from (r,c-1)
Chris@81 288 double cost = m_m->getPathCost(r, c-1) + newCost;
Chris@81 289 if ((min < 0) || (cost < min)) {
Chris@81 290 min = cost;
Chris@81 291 err.prevCost = m_m->getPathCost(r, c-1);
Chris@81 292 err.distance = newCost;
Chris@81 293 dir = Matcher::AdvanceOther;
Chris@81 294 }
Chris@81 295 }
Chris@81 296
Chris@81 297 updateTo = min;
Chris@81 298
Chris@82 299 } else { // first row
Chris@82 300
Chris@82 301 if (c > rowStart) {
Chris@82 302 // horizontal from (r,c-1)
Chris@83 303 updateTo = m_m->getPathCost(r, c-1) + newCost;
Chris@83 304 err.prevCost = m_m->getPathCost(r, c-1);
Chris@83 305 err.distance = newCost;
Chris@82 306 dir = Matcher::AdvanceOther;
Chris@82 307 }
Chris@81 308 }
Chris@81 309
Chris@82 310 if (dir != Matcher::AdvanceNone) {
Chris@86 311 if (m_m->getAdvance(r, c) != dir) {
Chris@86 312 err.type = ErrorPosition::WrongAdvance;
Chris@86 313 err.r = r;
Chris@86 314 err.c = c;
Chris@86 315 err.costWas = m_m->getPathCost(r, c);
Chris@86 316 err.costShouldBe = updateTo;
Chris@86 317 err.advanceWas = m_m->getAdvance(r, c);
Chris@86 318 err.advanceShouldBe = dir;
Chris@86 319 return err;
Chris@86 320 }
Chris@84 321 if (m_m->getPathCost(r, c) != updateTo) {
Chris@84 322 err.type = ErrorPosition::WrongCost;
Chris@84 323 err.r = r;
Chris@84 324 err.c = c;
Chris@84 325 err.costWas = m_m->getPathCost(r, c);
Chris@84 326 err.costShouldBe = updateTo;
Chris@84 327 err.advanceWas = m_m->getAdvance(r, c);
Chris@84 328 err.advanceShouldBe = dir;
Chris@82 329 return err;
Chris@82 330 }
Chris@82 331 } else {
Chris@82 332 // AdvanceNone should occur only at r = r1, c = c1
Chris@82 333 if (r != r1 || c != c1) {
Chris@82 334 err.type = ErrorPosition::NoAdvance;
Chris@82 335 err.r = r;
Chris@82 336 err.c = c;
Chris@82 337 err.costWas = m_m->getPathCost(r, c);
Chris@82 338 err.costShouldBe = updateTo;
Chris@84 339 err.advanceWas = m_m->getAdvance(r, c);
Chris@84 340 err.advanceShouldBe = dir;
Chris@82 341 return err;
Chris@82 342 }
Chris@81 343 }
Chris@81 344 }
Chris@81 345
Chris@81 346 prevRowStart = rowStart;
Chris@81 347 prevRowStop = rowStop;
Chris@81 348 }
Chris@81 349
Chris@81 350 return err;
Chris@82 351 }
Chris@81 352
Chris@92 353 void
Chris@92 354 Finder::checkAndReport()
Chris@30 355 {
Chris@92 356 cerr << "Finder: Checking path-cost matrix..." << endl;
Chris@82 357 ErrorPosition err = checkPathCostMatrix();
Chris@92 358 if (err.type == ErrorPosition::NoError) {
Chris@92 359 cerr << "No errors found" << endl;
Chris@92 360 } else {
Chris@82 361 cerr << "\nWARNING: Checking path-cost matrix returned mismatch:" << endl;
Chris@92 362 cerr << "Type: " << err.type << ": ";
Chris@92 363 switch (err.type) {
Chris@92 364 case ErrorPosition::NoError: break;
Chris@92 365 case ErrorPosition::WrongCost: cerr << "WrongCost"; break;
Chris@92 366 case ErrorPosition::WrongAdvance: cerr << "WrongAdvance"; break;
Chris@92 367 case ErrorPosition::NoAdvance: cerr << "NoAdvance"; break;
Chris@92 368 }
Chris@92 369 cerr << endl;
Chris@84 370 cerr << "At row " << err.r << ", column " << err.c
Chris@84 371 << "\nShould be advancing "
Chris@84 372 << Matcher::advanceToString(err.advanceShouldBe)
Chris@84 373 << ", advance in matrix is "
Chris@84 374 << Matcher::advanceToString(err.advanceWas)
Chris@83 375 << "\nPrev cost " << err.prevCost
Chris@82 376 << " plus distance " << err.distance << " gives "
Chris@84 377 << err.costShouldBe << ", matrix contains " << err.costWas
Chris@83 378 << endl;
Chris@83 379 cerr << "Note: diagonal weight = " << m_m->getDiagonalWeight() << endl;
Chris@83 380 cerr << endl;
Chris@92 381
Chris@95 382 int w(4);
Chris@95 383 int ww(15);
Chris@92 384
Chris@92 385 cerr << "Distance matrix leading up to this point:" << endl;
Chris@95 386 cerr << setprecision(12) << setw(w) << "";
Chris@92 387 for (int i = -4; i <= 0; ++i) {
Chris@95 388 cerr << setw(ww) << i;
Chris@92 389 }
Chris@92 390 cerr << endl;
Chris@92 391 for (int j = -4; j <= 0; ++j) {
Chris@92 392 cerr << setw(w) << j;
Chris@92 393 for (int i = -4; i <= 0; ++i) {
Chris@95 394 cerr << setw(ww) << m_m->getDistance(err.r + j, err.c + i);
Chris@92 395 }
Chris@92 396 cerr << endl;
Chris@92 397 }
Chris@92 398 cerr << endl;
Chris@92 399
Chris@92 400 cerr << "Cost matrix leading up to this point:" << endl;
Chris@92 401 cerr << setw(w) << "";
Chris@92 402 for (int i = -4; i <= 0; ++i) {
Chris@95 403 cerr << setw(ww) << i;
Chris@92 404 }
Chris@92 405 cerr << endl;
Chris@92 406 for (int j = -4; j <= 0; ++j) {
Chris@92 407 cerr << setw(w) << j;
Chris@92 408 for (int i = -4; i <= 0; ++i) {
Chris@95 409 cerr << setw(ww) << m_m->getPathCost(err.r + j, err.c + i);
Chris@92 410 }
Chris@92 411 cerr << endl;
Chris@92 412 }
Chris@92 413 cerr << endl;
Chris@82 414 }
Chris@92 415 }
Chris@92 416 #endif
Chris@92 417
Chris@92 418 int
Chris@92 419 Finder::retrievePath(bool smooth, vector<int> &pathx, vector<int> &pathy)
Chris@92 420 {
Chris@92 421 pathx.clear();
Chris@92 422 pathy.clear();
Chris@92 423
Chris@92 424 #ifdef PERFORM_ERROR_CHECKS
Chris@92 425 checkAndReport();
Chris@82 426 #endif
Chris@82 427
Chris@72 428 int ex = m_m->getOtherFrameCount() - 1;
Chris@72 429 int ey = m_m->getFrameCount() - 1;
Chris@69 430
Chris@69 431 if (ex < 0 || ey < 0) {
Chris@69 432 return 0;
Chris@69 433 }
Chris@66 434
Chris@66 435 int x = ex;
Chris@66 436 int y = ey;
Chris@30 437
Chris@140 438 #ifdef DEBUG_FINDER
Chris@140 439 cerr << "*** retrievePath: smooth = " << smooth << endl;
Chris@140 440 cerr << "*** retrievePath: before: x = " << x << ", y = " << y << endl;
Chris@140 441 #endif
Chris@140 442
Chris@72 443 if (m_duration2 > 0 && m_duration2 < m_m->getOtherFrameCount()) {
Chris@72 444 x = m_duration2 - 1;
Chris@60 445 }
Chris@72 446 if (m_duration1 > 0 && m_duration1 < m_m->getFrameCount()) {
Chris@72 447 y = m_duration1 - 1;
Chris@60 448 }
Chris@60 449
Chris@72 450 if (!m_m->isAvailable(y, x)) {
Chris@66 451 // Path did not pass through the expected end point --
Chris@66 452 // probably means the pieces are substantially different in
Chris@66 453 // the later bits. Reset the expected end point to the end of
Chris@66 454 // both files including any trailing silence.
Chris@66 455 cerr << "NOTE: Path did not pass through expected end point, inputs are probably significantly different" << endl;
Chris@66 456 x = ex;
Chris@66 457 y = ey;
Chris@66 458 }
Chris@66 459
Chris@55 460 recalculatePathCostMatrix(0, 0, y, x);
Chris@55 461
Chris@140 462 #ifdef DEBUG_FINDER
Chris@140 463 cerr << "*** retrievePath: start: x = " << x << ", y = " << y << endl;
Chris@140 464 #endif
Chris@66 465
Chris@72 466 while (m_m->isAvailable(y, x) && (x > 0 || y > 0)) {
Chris@30 467
Chris@33 468 // cerr << "x = " << x << ", y = " << y;
Chris@33 469
Chris@30 470 pathx.push_back(x);
Chris@30 471 pathy.push_back(y);
Chris@30 472
Chris@72 473 switch (m_m->getAdvance(y, x)) {
Chris@45 474 case Matcher::AdvanceThis:
Chris@70 475 // cerr << ", going down (dist = " << getDistance() << ")" << endl;
Chris@33 476 y--;
Chris@33 477 break;
Chris@45 478 case Matcher::AdvanceOther:
Chris@70 479 // cerr << ", going left (dist = " << getDistance() << ")" << endl;
Chris@33 480 x--;
Chris@33 481 break;
Chris@45 482 case Matcher::AdvanceBoth:
Chris@70 483 // cerr << ", going diag (dist = " << getDistance() << ")" << endl;
Chris@33 484 x--;
Chris@33 485 y--;
Chris@33 486 break;
Chris@45 487 case Matcher::AdvanceNone: // this would indicate a bug, but we wouldn't want to hang
Chris@69 488 cerr << "WARNING: Neither matcher advanced in path backtrack at (" << x << "," << y << ")" << endl;
Chris@33 489 if (x > y) {
Chris@33 490 x--;
Chris@33 491 } else {
Chris@33 492 y--;
Chris@33 493 }
Chris@33 494 break;
Chris@30 495 }
Chris@30 496 }
Chris@30 497
Chris@72 498 if (x > 0 || y > 0) {
Chris@72 499 cerr << "WARNING: Ran out of available path at (" << y << "," << x
Chris@72 500 << ")!" << endl;
Chris@72 501 }
Chris@72 502
Chris@72 503 reverse(pathx.begin(), pathx.end());
Chris@72 504 reverse(pathy.begin(), pathy.end());
Chris@30 505
Chris@31 506 if (smooth) {
Chris@31 507 int smoothedLen = Path().smooth(pathx, pathy, pathx.size());
Chris@31 508 return smoothedLen;
Chris@31 509 } else {
Chris@31 510 return pathx.size();
Chris@31 511 }
Chris@30 512 }
Chris@30 513
Chris@30 514