1 //=============================================================================
2 // MuseScore
3 // Music Composition & Notation
4 //
5 // Copyright (C) 2002-2016 Werner Schweer and others
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License version 2.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 //=============================================================================
19
20 #include "realizedharmony.h"
21 #include "pitchspelling.h"
22 #include "staff.h"
23 #include "chordlist.h"
24 #include "harmony.h"
25 #include "fraction.h"
26 #include "segment.h"
27 #include "chordrest.h"
28
29 namespace Ms {
30
31 //---------------------------------------------------
32 // setVoicing
33 /// sets the voicing and dirty flag if the passed
34 /// voicing is different than current
35 //---------------------------------------------------
setVoicing(Voicing v)36 void RealizedHarmony::setVoicing(Voicing v)
37 {
38 if (_voicing == v)
39 return;
40 _voicing = v;
41 cascadeDirty(true);
42 }
43
44 //---------------------------------------------------
45 // setDuration
46 /// sets the duration and dirty flag if the passed
47 /// HDuration is different than current
48 //---------------------------------------------------
setDuration(HDuration d)49 void RealizedHarmony::setDuration(HDuration d)
50 {
51 if (_duration == d)
52 return;
53 _duration = d;
54 cascadeDirty(true);
55 }
56
57 //---------------------------------------------------
58 // setLiteral
59 /// sets literal/jazz and dirty flag if the passed
60 /// bool is different than current
61 //---------------------------------------------------
setLiteral(bool literal)62 void RealizedHarmony::setLiteral(bool literal)
63 {
64 if (_literal == literal)
65 return;
66 _literal = literal;
67 cascadeDirty(true);
68 }
69
70 //---------------------------------------------------
71 // notes
72 /// returns the list of notes
73 //---------------------------------------------------
notes() const74 const RealizedHarmony::PitchMap& RealizedHarmony::notes() const
75 {
76 Q_ASSERT(!_dirty);
77 //with the way that the code is currently structured, there should be no way to
78 //get to this function with dirty flag set although in the future it may be
79 //better to just update if dirty here
80 return _notes;
81 }
82
83 //---------------------------------------------------
84 // generateNotes
85 /// generates a note list based on the passed parameters
86 //---------------------------------------------------
generateNotes(int rootTpc,int bassTpc,bool literal,Voicing voicing,int transposeOffset) const87 const RealizedHarmony::PitchMap RealizedHarmony::generateNotes(int rootTpc, int bassTpc,
88 bool literal, Voicing voicing, int transposeOffset) const
89 {
90 //The octave which to generate the body of the harmony, this is static const for now
91 //but may be user controlled in the future
92 static const int DEFAULT_OCTAVE = 5; //octave above middle C
93
94 PitchMap notes;
95 int rootPitch = tpc2pitch(rootTpc) + transposeOffset;
96 //euclidian mod, we need to treat this new pitch as a pitch between
97 //0 and 11, so that voicing remains consistent across transposition
98 if (rootPitch < 0)
99 rootPitch += PITCH_DELTA_OCTAVE;
100 else
101 rootPitch %= PITCH_DELTA_OCTAVE;
102
103 //create root note or bass note in second octave below middle C
104 if (bassTpc != Tpc::TPC_INVALID && voicing != Voicing::ROOT_ONLY)
105 notes.insert(tpc2pitch(bassTpc) + transposeOffset
106 + (DEFAULT_OCTAVE-2)*PITCH_DELTA_OCTAVE, bassTpc);
107 else
108 notes.insert(rootPitch + (DEFAULT_OCTAVE-2)*PITCH_DELTA_OCTAVE, rootTpc);
109
110 switch (voicing) {
111 case Voicing::ROOT_ONLY: //already added root/bass so we are good
112 break;
113 case Voicing::AUTO: //auto is close voicing for now since it is the most robust
114 //but just render the root if the harmony isn't understandable
115 if (!_harmony->parsedForm()->understandable())
116 break;
117 // FALLTHROUGH
118 case Voicing::CLOSE://Voices notes in close position in the first octave above middle C
119 {
120 notes.insert(rootPitch + DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, rootTpc);
121 //ensure that notes fall under a specific range
122 //for now this range is between 5*12 and 6*12
123 PitchMap intervals = getIntervals(rootTpc, literal);
124 PitchMapIterator i(intervals);
125 while (i.hasNext()) {
126 i.next();
127 notes.insert((rootPitch + (i.key() % 128)) % PITCH_DELTA_OCTAVE +
128 DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, i.value());
129 }
130 }
131 break;
132 case Voicing::DROP_2:
133 {
134 //select 4 notes from list
135 PitchMap intervals = normalizeNoteMap(getIntervals(rootTpc, literal), rootTpc, rootPitch, 4);
136 PitchMapIterator i(intervals);
137 i.toBack();
138
139 int counter = 0; //counter to drop the second note
140 while (i.hasPrevious()) {
141 i.previous();
142 if (++counter == 2)
143 notes.insert(i.key() + (DEFAULT_OCTAVE-1)*PITCH_DELTA_OCTAVE, i.value());
144 else
145 notes.insert(i.key() + DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, i.value());
146 }
147 }
148 break;
149 case Voicing::THREE_NOTE:
150 {
151 //Insert 2 notes in the octave above middle C
152 PitchMap intervals = normalizeNoteMap(getIntervals(rootTpc, literal), rootTpc, rootPitch, 2, true);
153 PitchMapIterator i(intervals);
154
155 i.next();
156 notes.insert(i.key() + DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, i.value());
157
158 i.next();
159 notes.insert(i.key() + DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, i.value());
160 }
161 break;
162 case Voicing::FOUR_NOTE:
163 case Voicing::SIX_NOTE:
164 //FALLTHROUGH
165 {
166 //four/six note voicing, drop every other note
167 PitchMap relIntervals = getIntervals(rootTpc, literal);
168 PitchMap intervals;
169 if (voicing == Voicing::FOUR_NOTE)
170 intervals = normalizeNoteMap(relIntervals, rootTpc, rootPitch, 3, true);
171 else //voicing == Voicing::SIX_NOTE
172 intervals = normalizeNoteMap(relIntervals, rootTpc, rootPitch, 5, true);
173
174 PitchMapIterator i(intervals);
175 i.toBack();
176
177 int counter = 0; //how many notes have been added
178 while (i.hasPrevious()) {
179 i.previous();
180
181 if (counter % 2)
182 notes.insert(i.key() + (DEFAULT_OCTAVE-1)*PITCH_DELTA_OCTAVE, i.value());
183 else
184 notes.insert(i.key() + DEFAULT_OCTAVE*PITCH_DELTA_OCTAVE, i.value());
185 ++counter;
186 }
187 }
188 break;
189 default:
190 break;
191 }
192 return notes;
193 }
194
195 //---------------------------------------------------
196 // update
197 /// updates the current note map, this is where all
198 /// of the rhythm and voicing choices matter since
199 /// the voicing algorithms depend on this.
200 ///
201 /// transposeOffset -- is the necessary adjustment
202 /// that is added to the root and bass
203 /// to get the correct sounding pitch
204 ///
205 /// TODO -: Don't worry about realized harmony for
206 /// RNA harmony?
207 //---------------------------------------------------
update(int rootTpc,int bassTpc,int transposeOffset)208 void RealizedHarmony::update(int rootTpc, int bassTpc, int transposeOffset /*= 0*/)
209 {
210 //on transposition the dirty flag is set by the harmony, but it's a little
211 //bit risky design since these 3 parameters rely on the dirty bit and are not
212 //otherwise checked by RealizedHarmony. This saves us 3 ints of space, but
213 //has the added risk
214 if (!_dirty) {
215 Q_ASSERT(_harmony->harmonyType() != HarmonyType::STANDARD || (_notes.first() == rootTpc || _notes.first() == bassTpc));
216 return;
217 }
218
219 if (tpcIsValid(rootTpc))
220 _notes = generateNotes(rootTpc, bassTpc, _literal, _voicing, transposeOffset);
221 _dirty = false;
222 }
223
224 //--------------------------------------------------
225 // getActualDuration
226 /// gets the fraction duration for how long
227 /// the harmony should be realized based
228 /// on the HDuration set.
229 ///
230 /// This is opposed to RealizedHarmony::duration()
231 /// which returns the HDuration, which is the duration
232 /// setting.
233 ///
234 /// As the duration may depend on playback order the
235 /// utick is required as a reference point
236 ///
237 /// Specifying the durationType will overwrite the
238 /// setting set by the user for the specific harmony object.
239 /// Don't specify it (or as HDuration::INVALID) to honor
240 /// the user setting.
241 //--------------------------------------------------
getActualDuration(int utick,HDuration durationType) const242 Fraction RealizedHarmony::getActualDuration(int utick, HDuration durationType) const
243 {
244 HDuration dur;
245 if (durationType != HDuration::INVALID)
246 dur = durationType;
247 else
248 dur = _duration;
249 switch (dur)
250 {
251 case HDuration::UNTIL_NEXT_CHORD_SYMBOL:
252 return _harmony->ticksTillNext(utick, false);
253 break;
254 case HDuration::STOP_AT_MEASURE_END:
255 return _harmony->ticksTillNext(utick, true);
256 break;
257 case HDuration::SEGMENT_DURATION: {
258 Segment* s = _harmony->getParentSeg();
259 if (s) {
260 // TODO - use duration of chordrest on this segment / track
261 // currently, this will result in too short of a duration
262 // if there are shorter notes on other staves
263 // but, we can only do this up to the next harmony that is being realized,
264 // or elese we would get overlap
265 // (e.g., if the notes are dotted half / quarter, but chords are half / half,
266 // then we can't actually make the first chord a dotted half)
267 return s->ticks();
268 }
269 else {
270 return Fraction(0, 1);
271 }
272 break;
273 }
274 default:
275 return Fraction(0, 1);
276 }
277 }
278
279 //---------------------------------------------------
280 // getIntervals
281 /// gets a weighted map from intervals to TPCs based on
282 /// a passed root tpc (this allows for us to
283 /// keep pitches, but transpose notes on the score)
284 ///
285 /// Weighting System:
286 /// - Rank 0: 3rd, altered 5th, suspensions (characteristic notes)
287 /// - Rank 1: 7th
288 /// - Rank 2: 9ths
289 /// - Rank 3: 13ths where applicable and (in minor chords) 11ths
290 /// - Rank 3: Other alterations and additions
291 /// - Rank 4: 5th and (in major/dominant chords) 11th
292 //---------------------------------------------------
getIntervals(int rootTpc,bool literal) const293 RealizedHarmony::PitchMap RealizedHarmony::getIntervals(int rootTpc, bool literal) const
294 {
295 //RANKING SYSTEM
296 static const int RANK_MULT = 128; //used as multiplier and mod since MIDI pitch goes from 0-127
297 static const int RANK_3RD = 0;
298 static const int RANK_7TH = 1;
299 static const int RANK_9TH = 2;
300 static const int RANK_ADD = 3;
301 static const int RANK_OMIT = 4;
302
303 static const int FIFTH = 7 + RANK_MULT*RANK_OMIT;
304
305 PitchMap ret;
306
307 const ParsedChord* p = _harmony->parsedForm();
308 QString quality = p->quality();
309 int ext = p->extension().toInt();
310 const QStringList& modList = p->modifierList();
311
312 int omit = 0; //omit flags for which notes to omit (for notes that are altered
313 //or specified to be omitted as a modification) so that they
314 //are not later added
315 bool alt5 = false; //altered 5
316
317 //handle modifiers
318 for (QString s : modList) {
319 //find number, split up mods
320 bool modded = false;
321 for (int c = 0; c < s.length(); ++c) {
322 if (s[c].isDigit()) {
323 int alter = 0;
324 int cutoff = c;
325 int deg = s.rightRef(s.length() - c).toInt();
326 //account for if the flat/sharp is stuck to the end of add
327 if (c) {
328 if (s[c-1] == '#') {
329 cutoff -= 1;
330 alter = +1;
331 }
332 else if (s[c-1] == 'b') {
333 cutoff -= 1;
334 alter = -1;
335 }
336 }
337 QString extType = s.left(cutoff);
338 if (extType == "" || extType == "major") { //alteration
339 if (deg == 9)
340 ret.insert(step2pitchInterval(deg, alter) + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, deg, alter));
341 else
342 ret.insert(step2pitchInterval(deg, alter) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, deg, alter));
343 if (deg == 5)
344 alt5 = true;
345 omit |= 1 << deg;
346 modded = true;
347 }
348 else if (extType == "sus") {
349 ret.insert(step2pitchInterval(deg, alter) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, deg, alter));
350 omit |= 1 << 3;
351 modded = true;
352 }
353 else if (extType == "no") {
354 omit |= 1 << deg;
355 modded = true;
356 }
357 else if (extType == "add") {
358 ret.insert(step2pitchInterval(deg, alter) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, deg, alter));
359 omit |= 1 << deg;
360 modded = true;
361 }
362 break;
363 }
364 }
365
366 //check for special chords, if we haven't modded anything yet there is a special or incomprehensible modifier
367 if (!modded) {
368 if (s == "phryg") {
369 ret.insert(step2pitchInterval(9, -1) + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, -1));
370 omit |= 1 << 9;
371 }
372 else if (s == "lyd") {
373 ret.insert(step2pitchInterval(11, +1) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 11, +1));
374 omit |= 1 << 11;
375 }
376 else if (s == "blues") {
377 ret.insert(step2pitchInterval(9, +1) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 9, +1));
378 omit |= 1 << 9;
379 }
380 else if (s == "alt") {
381 ret.insert(step2pitchInterval(5, -1) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 5, -1));
382 ret.insert(step2pitchInterval(5, +1) + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 5, +1));
383 omit |= 1 << 5;
384 ret.insert(step2pitchInterval(9, -1) + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, -1));
385 ret.insert(step2pitchInterval(9, +1) + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, +1));
386 omit |= 1 << 9;
387 }
388 else //no easy way to realize tristan chords since they are more analytical tools
389 omit = ~0;
390 }
391 }
392
393 //handle ext = 5: power chord so omit the 3rd
394 if (ext == 5)
395 omit |= 1 << 3;
396
397 //handle chord quality
398 if (quality == "minor") {
399 if (!(omit & (1 << 3)))
400 ret.insert(step2pitchInterval(3, -1) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 3, -1)); //min3
401 if (!(omit & (1 << 5)))
402 ret.insert(step2pitchInterval(5, 0) + RANK_MULT*RANK_OMIT, tpcInterval(rootTpc, 5, 0)); //p5
403 }
404 else if (quality == "augmented") {
405 if (!(omit & (1 << 3)))
406 ret.insert(step2pitchInterval(3, 0) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 3, 0)); //maj3
407 if (!(omit & (1 << 5)))
408 ret.insert(step2pitchInterval(5, +1) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 5, +1)); //p5
409 }
410 else if (quality == "diminished" || quality == "half-diminished") {
411 if (!(omit & (1 << 3)))
412 ret.insert(step2pitchInterval(3, -1) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 3, -1)); //min3
413 if (!(omit & (1 << 5)))
414 ret.insert(step2pitchInterval(5, -1) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 5, -1)); //dim5
415 alt5 = true;
416 }
417 else { //major or dominant
418 if (!(omit & (1 << 3)))
419 ret.insert(step2pitchInterval(3, 0) + RANK_MULT*RANK_3RD, tpcInterval(rootTpc, 3, 0)); //maj3
420 if (!(omit & (1 << 5)))
421 ret.insert(step2pitchInterval(5, 0) + RANK_MULT*RANK_OMIT, tpcInterval(rootTpc, 5, 0)); //p5
422 }
423
424 //handle extension
425 switch (ext) {
426 case 13:
427 if (!(omit & (1 << 13))) {
428 ret.insert(9 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 13, 0)); //maj13
429 omit |= 1 << 13;
430 }
431 // FALLTHROUGH
432 case 11:
433 if (!(omit & (1 << 11))) {
434 if (quality == "minor")
435 ret.insert(5 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 11, 0)); //maj11
436 else if (literal)
437 ret.insert(5 + RANK_MULT*RANK_OMIT, tpcInterval(rootTpc, 11, 0)); //maj11
438 omit |= 1 << 11;
439 }
440 // FALLTHROUGH
441 case 9:
442 if (!(omit & (1 << 9))) {
443 ret.insert(2 + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, 0)); //maj9
444 omit |= 1 << 9;
445 }
446 // FALLTHROUGH
447 case 7:
448 if (!(omit & (1 << 7))) {
449 if (quality == "major")
450 ret.insert(11 + RANK_MULT*RANK_7TH, tpcInterval(rootTpc, 7, 0));
451 else if (quality == "diminished")
452 ret.insert(9 + RANK_MULT*RANK_7TH, tpcInterval(rootTpc, 7, -2));
453 else //dominant or augmented or minor
454 ret.insert(10 + RANK_MULT*RANK_7TH, tpcInterval(rootTpc, 7, -1));
455 }
456 break;
457 case 6:
458 if (!(omit & (1 << 6))) {
459 ret.insert(9 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 6, 0)); //maj6
460 omit |= 1 << 13; //no need to add/alter 6 chords
461 }
462 break;
463 case 5:
464 //omitted third already
465 break;
466 case 4:
467 if (!(omit & (1 << 4)))
468 ret.insert(5 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 4, 0)); //p4
469 break;
470 case 2:
471 if (!(omit & (1 << 2)))
472 ret.insert(2 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 2, 0)); //maj2
473 omit |= 1 << 9; //make sure we don't add altered 9 when theres a 2
474 break;
475 case 69:
476 ret.insert(9 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 6, 0)); //maj6
477 ret.insert(2 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 9, 0)); //maj6
478 omit = ~0; //no additions/alterations for a 69 chord
479 break;
480 default:
481 break;
482 }
483
484 Harmony* next = _harmony->findNext();
485 if (!literal && next && tpcIsValid(next->rootTpc())) {
486 //jazz interpretation
487 QString qNext = next->parsedForm()->quality();
488 //pitch from current to next harmony normalized to a range between 0 and 12
489 //add PITCH_DELTA_OCTAVE before modulo so that we can ensure arithmetic mod rather than computer mod
490 //int keyTpc = int(next->staff()->key(next->tick())) + 14; //tpc of key (ex. F# major would be Tpc::F_S)
491 //int keyTpcMinor = keyTpc + 3;
492 int pitchBetween = (tpc2pitch(next->rootTpc()) + PITCH_DELTA_OCTAVE - tpc2pitch(rootTpc)) % PITCH_DELTA_OCTAVE;
493 bool maj7 = qNext == "major" && next->parsedForm()->extension() >= 7; //whether or not the next chord has major 7
494
495 //commented code: dont add 9 for diminished chords
496 if (!(omit & (1 << 9))) {// && !(alt5 && (quality == "minor" || quality == "diminished" || quality == "half-diminished"))) {
497 if (quality == "dominant" && pitchBetween == 5 && (qNext == "minor" || maj7)) {
498 //flat 9 when resolving a fourth up to a minor chord or major 7th
499 ret.insert(1 + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, -1));
500 }
501 else //add major 9
502 ret.insert(2 + RANK_MULT*RANK_9TH, tpcInterval(rootTpc, 9, 0));
503 }
504
505 if (!(omit & (1 << 13)) && !alt5) {
506 if (quality == "dominant" && pitchBetween == 5 && (qNext == "minor" || false)) {
507 //flat 13 for dominant to chord a P4 up
508 //only for minor chords for now
509 ret.remove(FIFTH);
510 ret.insert(8 + RANK_MULT*RANK_ADD, tpcInterval(rootTpc, 13, -1));
511 }
512 //major 13 considered, but too dependent on melody and voicing of other chord
513 //no implementation for now
514 }
515 }
516 return ret;
517 }
518
519 //---------------------------------------------------
520 // normalizeNoteMap
521 /// normalize the pitch map from intervals to create pitches between 0 and 12
522 /// and resolve any weighting system.
523 ///
524 /// enforceMaxEquals - enforce the max as a goal so that the max is how many notes is inserted
525 //---------------------------------------------------
normalizeNoteMap(const PitchMap & intervals,int rootTpc,int rootPitch,int max,bool enforceMaxAsGoal) const526 RealizedHarmony::PitchMap RealizedHarmony::normalizeNoteMap(const PitchMap& intervals, int rootTpc, int rootPitch, int max, bool enforceMaxAsGoal) const
527 {
528 PitchMap ret;
529 PitchMapIterator itr(intervals);
530
531 for (int i = 0; i < max; ++i) {
532 if (!itr.hasNext())
533 break;
534 itr.next();
535 ret.insert((itr.key() % 128 + rootPitch) % PITCH_DELTA_OCTAVE, itr.value()); //128 is RANK_MULT
536 }
537
538 //redo insertions if we must have a specific number of notes with insertMulti
539 if (enforceMaxAsGoal) {
540 while (ret.size() < max) {
541 ret.insert(rootPitch, rootTpc); //duplicate root
542
543 int size = max - ret.size();
544 itr = PitchMapIterator(intervals); //reset iterator
545 for (int i = 0; i < size; ++i) {
546 if (!itr.hasNext())
547 break;
548 itr.next();
549 ret.insert((itr.key() % 128 + rootPitch) % PITCH_DELTA_OCTAVE, itr.value());
550 }
551 }
552 }
553 else if (ret.size() < max) //insert another root if we have room in general
554 ret.insert(rootPitch, rootTpc);
555 return ret;
556 }
557
558 //---------------------------------------------------
559 // cascadeDirty
560 /// cascades the dirty flag backwards so that everything is properly set
561 /// this is required since voicing algorithms may look ahead
562 ///
563 /// NOTE: FOR NOW ALGORITHMS DO NOT LOOK BACKWARDS AND SO THERE IS NO
564 /// REQUIREMENT TO CASCADE FORWARD, IN THE FUTURE IT MAY BECOME IMPORTANT
565 /// TO CASCADE FORWARD AS WELL
566 //---------------------------------------------------
cascadeDirty(bool dirty)567 void RealizedHarmony::cascadeDirty(bool dirty)
568 {
569 if (dirty && !_dirty) { //only cascade when we want to set our clean realized harmony to dirty
570 Harmony* prev = _harmony->findPrev();
571 if (prev)
572 prev->realizedHarmony().cascadeDirty(dirty);
573 }
574 _dirty = dirty;
575 }
576 }
577