1 package org.herac.tuxguitar.gui.editors.chord; 2 /* Created on 05-March-2007 3 */ 4 5 import java.util.ArrayList; 6 import java.util.Iterator; 7 8 import org.eclipse.swt.SWT; 9 import org.eclipse.swt.events.SelectionAdapter; 10 import org.eclipse.swt.events.SelectionEvent; 11 import org.eclipse.swt.layout.GridData; 12 import org.eclipse.swt.layout.GridLayout; 13 import org.eclipse.swt.widgets.Composite; 14 import org.eclipse.swt.widgets.List; 15 import org.herac.tuxguitar.song.models.TGChord; 16 import org.herac.tuxguitar.util.TGSynchronizer; 17 18 /** 19 * @author Nikola Kolarovic 20 * 21 */ 22 public class ChordRecognizer extends Composite { 23 24 // index for parameter array 25 protected static final int TONIC_INDEX = 0; 26 protected static final int CHORD_INDEX = 1; 27 protected static final int ALTERATION_INDEX = 2; 28 protected static final int PLUSMINUS_INDEX = 3; 29 protected static final int BASS_INDEX = 4; 30 protected static final int ADDCHK_INDEX = 5; 31 protected static final int I5_INDEX = 6; 32 protected static final int I9_INDEX = 7; 33 protected static final int I11_INDEX = 8; 34 35 private ChordDialog dialog; 36 private List proposalList; 37 private java.util.List proposalParameters; 38 39 // this var keep a control to running threads. 40 private long runningProcess; 41 ChordRecognizer(ChordDialog dialog, Composite parent,int style)42 public ChordRecognizer(ChordDialog dialog, Composite parent,int style) { 43 super(parent,style); 44 this.setLayout(dialog.gridLayout(1,false,0,0)); 45 this.setLayoutData(makeGridData()); 46 this.runningProcess = 0; 47 this.dialog = dialog; 48 this.init(); 49 } 50 makeGridData()51 public GridData makeGridData(){ 52 GridData data = new GridData(SWT.FILL,SWT.FILL,true,true); 53 data.minimumWidth = 180; 54 return data; 55 } 56 init()57 public void init(){ 58 Composite composite = new Composite(this,SWT.NONE); 59 composite.setLayout(new GridLayout()); 60 composite.setLayoutData(new GridData(SWT.FILL,SWT.FILL,true,true)); 61 62 this.proposalParameters = new ArrayList(); 63 64 this.proposalList = new List(composite,SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); 65 this.proposalList.setLayoutData(new GridData(SWT.FILL,SWT.FILL,true,true)); 66 this.proposalList.addSelectionListener(new SelectionAdapter() { 67 public void widgetSelected(SelectionEvent e) { 68 if(getDialog().getEditor() != null){ 69 showChord(getProposalList().getSelectionIndex()); 70 } 71 } 72 }); 73 74 } 75 76 /** sets the current chord to be selected proposal */ showChord(int index)77 protected void showChord(int index) { 78 int[] params = (int[])this.proposalParameters.get(index); 79 this.dialog.getSelector().adjustWidgets(params[TONIC_INDEX], 80 params[CHORD_INDEX], 81 params[ALTERATION_INDEX], 82 params[BASS_INDEX], 83 params[PLUSMINUS_INDEX], 84 params[ADDCHK_INDEX], 85 params[I5_INDEX], 86 params[I9_INDEX], 87 params[I11_INDEX]); 88 String chordName = this.proposalList.getItem(index); 89 chordName = chordName.substring(0, chordName.indexOf('(')-1); 90 this.dialog.getEditor().getChordName().setText(chordName); 91 this.dialog.getEditor().redraw(); 92 } 93 94 /** 95 * - Recognizes the chord string 96 * - Fills the component's list with alternative names 97 * - Sets all the ChordSelector fields into recognized chord (tonic, bass, chord, alterations) 98 * - Makes the alternatives and puts them into ChordList 99 * - Writes the chord formula into appropriate label 100 * @param chord chord structure (frets, strings) 101 * @param redecorate is the Chord Editor in editing mode, or it is just changed by ChordSelector 102 */ 103 recognize(final TGChord chord,final boolean redecorate,final boolean setChordName)104 public void recognize(final TGChord chord,final boolean redecorate,final boolean setChordName) { 105 106 final long processId = (++ this.runningProcess); 107 final boolean sharp = this.dialog.getSelector().getSharpButton().getSelection(); 108 109 this.clearProposals(); 110 111 new Thread( new Runnable() { 112 public void run() { 113 if(!getDialog().isDisposed() && isValidProcess(processId)){ 114 115 final int params[] = makeProposals(processId, chord,sharp); 116 117 if (params == null) { // could not recognize anything!? 118 if (isValidProcess(processId) && setChordName) { 119 try { 120 TGSynchronizer.instance().addRunnable(new TGSynchronizer.TGRunnable() { 121 public void run() { 122 if(!getDialog().isDisposed() && isValidProcess(processId)){ 123 getDialog().getEditor().setChordName(""); 124 } 125 } 126 }); 127 } catch (Throwable e) { 128 e.printStackTrace(); 129 } 130 } 131 return; 132 } 133 134 final String chordName = getChordName(params,sharp); 135 136 // Sets all the ChordSelector fields into recognized chord (tonic, bass, chord, alterations) 137 if (isValidProcess(processId) && redecorate) { 138 try { 139 TGSynchronizer.instance().addRunnable(new TGSynchronizer.TGRunnable() { 140 public void run() { 141 if(!getDialog().isDisposed()){ 142 redecorate(params); 143 } 144 } 145 }); 146 } catch (Throwable e) { 147 e.printStackTrace(); 148 } 149 } 150 151 if (isValidProcess(processId) && setChordName) { 152 try { 153 TGSynchronizer.instance().addRunnable(new TGSynchronizer.TGRunnable() { 154 public void run() { 155 if(!getDialog().isDisposed()){ 156 getDialog().getEditor().setChordName( (chordName != null ? chordName : "" ) ); 157 } 158 } 159 }); 160 } catch (Throwable e) { 161 e.printStackTrace(); 162 } 163 } 164 } 165 } 166 } ).start(); 167 } 168 169 /** Fills the component's list with alternative names 170 * @param chord TGChord to be recognized 171 * @return parameters for adjustWidgets and getChordName methods 172 */ makeProposals(final long processId, TGChord chord,final boolean sharp)173 protected int[] makeProposals(final long processId, TGChord chord,final boolean sharp) { 174 175 int[] tuning = this.dialog.getSelector().getTuning(); 176 java.util.List notesInside = new ArrayList(); 177 178 // find and put in all the distinct notes 179 for (int i=0; i<tuning.length; i++) { 180 int fret = chord.getStrings()[i]; 181 if (fret!=-1) { 182 Integer note = new Integer((tuning[tuning.length-1-i] + fret) % 12); 183 Iterator it = notesInside.iterator(); 184 boolean found=false; 185 while (it.hasNext()) 186 if (it.next().equals(note)) 187 found=true; 188 if (!found) 189 notesInside.add(note); 190 } 191 } 192 193 // Now search: 194 // go through all the possible tonics 195 // it is required because tonic isn't mandatory in a chord 196 java.util.List allProposals = new ArrayList(10); 197 198 for (int tonic=0; tonic<12; tonic++) { 199 200 Proposal currentProp = null; 201 202 // first check for the basic chord tones 203 for (int chordIndex = 0; chordIndex < ChordDatabase.length(); chordIndex ++) { 204 ChordDatabase.ChordInfo info = ChordDatabase.get(chordIndex); 205 206 currentProp = new Proposal(notesInside); 207 // it is more unusual the more we go down the index 208 // except chords "5" and "m", they are quite usual :) 209 currentProp.unusualGrade-=(chordIndex!=ChordDatabase.length() && chordIndex!=4 ? 2*chordIndex : 0); 210 211 //ChordDatabase.ChordInfo info = (ChordDatabase.ChordInfo)chordItr.next(); 212 boolean foundNote = false; 213 for (int i=0; i<info.getRequiredNotes().length; i++) { // go through all the requred notes 214 Iterator nit = notesInside.iterator(); 215 while (nit.hasNext()) // go through all the needed notes 216 if (((Integer)nit.next()).intValue() == (tonic+info.getRequiredNotes()[i]-1)%12) { 217 foundNote=true; 218 if (tonic+info.getRequiredNotes()[i]-1 == tonic) 219 currentProp.dontHaveGrade+=15; // this means penalty for not having tonic is -65 220 currentProp.foundNote(tonic+info.getRequiredNotes()[i]-1); // found a note in a chord 221 } 222 223 } 224 // if something found, add it into a proposal if it's worth 225 if (foundNote) { 226 currentProp.params[TONIC_INDEX] = tonic; 227 currentProp.params[CHORD_INDEX] = chordIndex;//possibleChords.indexOf(info); 228 int foundNotesCount = currentProp.missingNotes.length-currentProp.missingCount; 229 230 // it is worth if it is missing 1 essential note and/or fifth 231 if (!info.getName().startsWith("dim") && !info.getName().startsWith("aug")) 232 if (!currentProp.isFound(tonic+8-1)) { 233 234 // hmmm. maybe it's altered fifth? Create a branch for it. 235 if (currentProp.isNeeded(tonic+7-1) || currentProp.isNeeded(tonic+9-1)) { 236 Proposal branchProp = (Proposal)currentProp.clone(); 237 if (branchProp.isNeeded(tonic+9-1)) { 238 branchProp.params[I5_INDEX] = 1; 239 branchProp.foundNote(tonic+8); 240 } 241 else { 242 branchProp.params[I5_INDEX] = 2; 243 branchProp.foundNote(tonic+6); 244 } 245 branchProp.unusualGrade-=35; 246 if (foundNotesCount+1>=info.getRequiredNotes().length-1) { 247 branchProp.dontHaveGrade-=(info.getRequiredNotes().length-(foundNotesCount+1))*50; 248 allProposals.add(branchProp); 249 } 250 251 } 252 else { 253 currentProp.params[I5_INDEX] = 0; 254 currentProp.dontHaveGrade+=30; 255 } 256 } 257 258 currentProp.params[I5_INDEX] = 0; 259 if (foundNotesCount>=info.getRequiredNotes().length-1 ) { 260 currentProp.dontHaveGrade-=(info.getRequiredNotes().length-foundNotesCount)*50; 261 allProposals.add(currentProp); 262 } 263 } 264 currentProp=null; 265 } 266 } 267 268 Iterator props = allProposals.iterator(); 269 java.util.List unsortedProposals = new ArrayList(5); 270 while (props.hasNext()) { 271 // place the still missing alterations notes accordingly... bass also 272 /////////////////////////////////////////////////////////////// 273 274 final Proposal current = (Proposal)props.next(); 275 276 boolean bassIsOnlyInBass = true; 277 // ---------------- bass tone ---------------- 278 for (int i=chord.getStrings().length-1; i>=0; i--) { 279 if (chord.getStrings()[i]!=-1) { 280 if (current.params[BASS_INDEX]==-1) {// if we still didn't determine bass 281 current.params[BASS_INDEX] = (tuning[tuning.length-1-i] + chord.getStrings()[i]) % 12; 282 if (current.params[BASS_INDEX]!=current.params[TONIC_INDEX]) 283 current.unusualGrade-=20; 284 } 285 if (current.params[BASS_INDEX]==(tuning[tuning.length-1-i] + chord.getStrings()[i]) % 12 ) 286 bassIsOnlyInBass=false; // if we stumbled upon bass tone again 287 } 288 } 289 290 if (current.isNeeded(current.params[BASS_INDEX]) && bassIsOnlyInBass) { 291 // do not mark as FOUND if bass is somewhere other than in bass only 292 current.foundNote(current.params[BASS_INDEX]); 293 current.unusualGrade-=20; 294 } 295 // <=11 means "not DIM or AUG or 5" 296 if (current.missingCount>0 && current.params[CHORD_INDEX]<=11) { 297 // ---------------- alteration tones ---------------- 298 // determine seventh -->> 2 is HARDCODED! 299 int seventh; 300 if (current.params[CHORD_INDEX] == 2) seventh=current.params[TONIC_INDEX]+12-1; // plain 7 301 else seventh=current.params[TONIC_INDEX]+11-1; // b7 302 if (current.isExisting(seventh)) { 303 if (!current.isFound(seventh)) { 304 current.filled[3]=true; 305 current.foundNote(seventh); 306 } 307 } 308 for (int plusminus=0; plusminus<=2; plusminus++) { 309 for (int i=2; i>=0; i--) // 13, 11, 9 310 if (current.isNeeded(current.params[TONIC_INDEX]+getAddNote(i, plusminus)) && !current.filled[i]) { 311 current.filled[i]=true; 312 current.plusminusValue[i]=plusminus; 313 if (plusminus!=0) 314 current.unusualGrade-=15; 315 current.foundNote(current.params[TONIC_INDEX]+getAddNote(i, plusminus)); 316 } 317 318 } 319 } 320 321 // fill in the list 322 /////////////////////////////////////////////////////////////// 323 if (!(current.filled[3] && !(current.filled[0] || current.filled[1] || current.filled[2])) && // if just found seventh, cancel it 324 current.missingCount==0 && // we don't tollerate notes in chord that are not used in the ChordName 325 current.dontHaveGrade>-51) { 326 findChordLogic(current); 327 unsortedProposals.add(current); 328 } 329 330 } 331 // first, sort by DontHaveGrade 332 shellsort(unsortedProposals,1); 333 334 int cut=-1; 335 int howManyIncomplete = ChordSettings.instance().getIncompleteChords(); 336 337 for (int i=0; i<unsortedProposals.size() && cut==-1; i++) { 338 int prior = ((Proposal)unsortedProposals.get(i)).dontHaveGrade; 339 if (prior<0) 340 cut=i+howManyIncomplete; 341 } 342 // cut the search 343 unsortedProposals=unsortedProposals.subList(0, (cut>0 && cut<unsortedProposals.size() ? cut : unsortedProposals.size())); 344 // sort by unusualGrade 345 shellsort(unsortedProposals,2); 346 347 int firstNegative = 0; 348 for (int i=0; i<unsortedProposals.size(); i++) { 349 final Proposal current = (Proposal)unsortedProposals.get(i); 350 if (firstNegative==0 && current.unusualGrade<0) 351 firstNegative=current.unusualGrade; 352 353 if (current.unusualGrade > (firstNegative>=0 ? 0 : firstNegative)-60){ 354 try { 355 TGSynchronizer.instance().addRunnable(new TGSynchronizer.TGRunnable() { 356 public void run() { 357 if(!getDialog().isDisposed() && isValidProcess(processId)){ 358 addProposal(current.params, getChordName(current.params,sharp)+" ("+Math.round(100+current.dontHaveGrade*7/10)+"%)" ); 359 } 360 } 361 }); 362 } catch (Throwable e) { 363 e.printStackTrace(); 364 } 365 } 366 } 367 if (this.proposalParameters.size()==0) 368 return null; 369 return (int[])this.proposalParameters.get(0); 370 } 371 372 /** adjusts widgets on the Recognizer combo */ redecorate(int params[])373 protected void redecorate(int params[]){ 374 this.dialog.getSelector().adjustWidgets(params[TONIC_INDEX], 375 params[CHORD_INDEX], 376 params[ALTERATION_INDEX], 377 params[BASS_INDEX], 378 params[PLUSMINUS_INDEX], 379 params[ADDCHK_INDEX], 380 params[I5_INDEX], 381 params[I9_INDEX], 382 params[I11_INDEX]); 383 } 384 385 /** Assembles chord name according to ChordNamingConvention */ getChordName(int[] param, boolean sharp)386 protected String getChordName(int[] param, boolean sharp) { 387 return new ChordNamingConvention().createChordName(param[TONIC_INDEX], 388 param[CHORD_INDEX], 389 param[ALTERATION_INDEX], 390 param[PLUSMINUS_INDEX], 391 param[ADDCHK_INDEX] != 0, 392 param[I5_INDEX], 393 param[I9_INDEX], 394 param[I11_INDEX], 395 param[BASS_INDEX], 396 sharp); 397 } 398 399 /** Return required interval in semitones for add type and +- modificator 400 * @param type 0=add9, 1=add11, 2=add13 401 * @param selectionIndex 0=usual, 1="+", 2="-" 402 */ getAddNote(int type, int selectionIndex)403 protected int getAddNote(int type, int selectionIndex) { 404 405 int wantedNote = 0; 406 407 switch (type) { 408 case 0: 409 wantedNote = 3; // add9 410 break; 411 case 1: 412 wantedNote = 6; // add11 413 break; 414 case 2: 415 wantedNote = 10; // add13 416 break; 417 } 418 419 switch (selectionIndex) { 420 case 1: 421 wantedNote++; 422 break; 423 case 2: 424 wantedNote--; 425 break; 426 default: 427 break; 428 } 429 430 return --wantedNote; 431 432 } 433 findChordLogic(Proposal current)434 void findChordLogic(Proposal current) { 435 boolean[] found = current.filled; 436 int[] plusMinus = current.plusminusValue; 437 /*if (!found[3]) 438 current.unusualGrade-=50;*/ 439 current.params[ALTERATION_INDEX]=0; 440 current.params[I9_INDEX]=plusMinus[0]; 441 current.params[I11_INDEX]=plusMinus[1]; 442 current.params[ADDCHK_INDEX]=0; 443 current.params[PLUSMINUS_INDEX]=0; 444 445 if (found[2]) { // -------------- 13 446 current.params[ALTERATION_INDEX]=3; 447 current.params[PLUSMINUS_INDEX]=plusMinus[2]; 448 if (!found[1] || !found[0] || !found[3]) { // b7 or 9 or 11 not inside 449 current.unusualGrade-=10; 450 if (!found[1] && !found[0] && !found[3]) 451 current.params[ADDCHK_INDEX]=1; 452 else { // just penalty if something's missing 453 if (!found[3]) // don't-have penalty if seventh is missing 454 current.dontHaveGrade-=25; 455 if (!found[1]) { // if 9 or 11 is missing, it is more unusual 456 current.unusualGrade-=30; 457 current.dontHaveGrade-=10; 458 } 459 if (!found[0]) { 460 current.unusualGrade-=30; 461 current.dontHaveGrade-=10; 462 } 463 } 464 } 465 } 466 else 467 if (found[1]) { // -------------- 11 468 current.params[ALTERATION_INDEX]=2; 469 current.params[PLUSMINUS_INDEX]=plusMinus[1]; 470 current.params[I11_INDEX]=0; 471 current.unusualGrade-=10; 472 473 if (!found[0] || !found[3]) { // b7 or 9 not inside 474 if (!found[0] && !found[3]) 475 current.params[ADDCHK_INDEX]=1; 476 else{ 477 if (!found[3]) 478 current.dontHaveGrade-=25; 479 if (!found[0]) { 480 current.unusualGrade-=30; 481 current.dontHaveGrade-=10; 482 } 483 } 484 } 485 } 486 else 487 if (found[0]) { // 9 488 current.params[ALTERATION_INDEX]=1; 489 current.params[I9_INDEX]=0; 490 current.params[I11_INDEX]=0; 491 current.params[PLUSMINUS_INDEX]=plusMinus[0]; 492 current.unusualGrade-=10; 493 if (!found[3]) 494 current.params[ADDCHK_INDEX]=1; 495 496 } 497 } 498 499 /** 500 * Shellsort, using a sequence suggested by Gonnet. 501 * -- a little adopted 502 * @param a List of Proposals, unsorted 503 * @param sortIndex 1 to sort by don'tHaveGrade, 2 to sort by unusualGrade 504 * @return sorted list by selected criteria 505 */ shellsort( java.util.List a, int sortIndex )506 public void shellsort( java.util.List a, int sortIndex ){ 507 int length = a.size(); 508 for( int gap = length / 2; gap > 0; 509 gap = gap == 2 ? 1 : (int) ( gap / 2.2 ) ) 510 for( int i = gap; i < length; i++ ){ 511 Proposal tmp = (Proposal)a.get(i); 512 int j = i; 513 514 for( ; j >= gap && 515 ( sortIndex == 1 ? 516 tmp.dontHaveGrade > ((Proposal)a.get(j - gap)).dontHaveGrade : 517 tmp.unusualGrade > ((Proposal)a.get(j - gap)).unusualGrade ) 518 ; 519 j -= gap ) 520 a.set(j, a.get(j - gap)); 521 a.set( j , tmp); 522 } 523 } 524 addProposal(int[] params, String name)525 protected void addProposal(int[] params, String name){ 526 this.proposalParameters.add(params); 527 this.proposalList.add(name); 528 } 529 clearProposals()530 protected void clearProposals(){ 531 this.proposalList.removeAll(); 532 this.proposalParameters.clear(); 533 } 534 getDialog()535 protected ChordDialog getDialog(){ 536 return this.dialog; 537 } 538 getProposalList()539 protected List getProposalList(){ 540 return this.proposalList; 541 } 542 isValidProcess(long processId)543 protected boolean isValidProcess(long processId){ 544 return (this.runningProcess == processId); 545 } 546 547 protected class Proposal implements Cloneable{ 548 int[] params; 549 550 /** grade for chord "unusualness" - Cm is less unusual than E7/9+/C */ 551 int unusualGrade = 0; 552 /** penalty for notes that chord doesn't have */ 553 int dontHaveGrade = -15; 554 555 /** counts the notes that are in chord but still not recognized */ 556 int missingCount; 557 int[] missingNotes; 558 559 boolean filled[]={false,false,false,false}; 560 int plusminusValue[]={0,0,0}; 561 Proposal()562 private Proposal() { 563 super(); 564 this.params = new int[9]; 565 for (int i=0; i<9; i++) 566 this.params[i]=-1; 567 } 568 569 /** initialize with needed notes */ Proposal(java.util.List notes)570 public Proposal(java.util.List notes) { 571 this.params = new int[9]; 572 for (int i=0; i<9; i++) 573 this.params[i]=-1; 574 575 int length = notes.size(); 576 this.missingNotes = new int[length]; 577 for (int i = 0; i< length; i++){ // deep copy, because of clone() method 578 this.missingNotes[i] = ((Integer)notes.get(i)).intValue(); 579 } 580 this.missingCount = length; 581 } 582 583 /** if note is found, mark it as found in the Missing array*/ foundNote(int value)584 void foundNote(int value) { 585 int note = (value % 12); 586 if (this.missingCount!=0) 587 for (int i=0; i<this.missingCount; i++) 588 if (this.missingNotes[i] == note) { 589 // put the found one on the end, switch positions 590 this.missingCount--; 591 int temp = this.missingNotes[i]; 592 this.missingNotes[i]=this.missingNotes[this.missingCount]; 593 this.missingNotes[this.missingCount]=temp; 594 return; 595 } 596 } 597 598 /** is note already found? */ isFound(int value)599 boolean isFound(int value) { 600 int note = (value % 12); 601 for (int i=this.missingCount; i<this.missingNotes.length; i++) 602 if (this.missingNotes[i] == note) 603 return true; 604 return false; 605 } 606 607 /** is note required to be found? */ isNeeded(int value)608 boolean isNeeded(int value) { 609 int note = (value % 12); 610 if (this.missingCount!=0) 611 for (int i=0; i<this.missingCount; i++) 612 if (this.missingNotes[i] == note) 613 return true; 614 return false; 615 } 616 617 /** does note exist in a chord? (found or not found) */ isExisting(int value)618 boolean isExisting(int value) { 619 int note = (value % 12); 620 for (int i=0; i<this.missingNotes.length; i++) 621 if (this.missingNotes[i] == note) 622 return true; 623 return false; 624 } 625 626 /** calls the Object.clone() method, since it is private (?!!??) */ clone()627 public Object clone() { 628 Proposal proposal = new Proposal(); 629 for (int i=0; i<9; i++) 630 proposal.params[i] = this.params[i]; 631 proposal.unusualGrade = this.unusualGrade; 632 proposal.dontHaveGrade = this.dontHaveGrade; 633 proposal.missingCount = this.missingCount; 634 proposal.missingNotes = new int[this.missingNotes.length]; 635 for(int i = 0; i < proposal.missingNotes.length; i ++){ 636 proposal.missingNotes[i] = this.missingNotes[i]; 637 } 638 proposal.filled = new boolean[this.filled.length]; 639 for (int i=0; i<proposal.filled.length; i++) 640 proposal.filled[i] = this.filled[i]; 641 642 proposal.plusminusValue = new int[this.plusminusValue.length]; 643 for (int i=0; i<proposal.plusminusValue.length; i++) 644 proposal.plusminusValue[i] = this.plusminusValue[i]; 645 646 return proposal; 647 } 648 equals(Object o)649 public boolean equals(Object o) { 650 Proposal another = (Proposal)o; 651 for (int i=0; i<9; i++) 652 if (this.params[i]!=another.params[i]) 653 return false; 654 // not all attributes, but the rest is not needed YET! 655 return true; 656 } 657 } 658 }