Mercurial > hg > svgui
comparison widgets/LevelPanWidget.cpp @ 1301:e8368466fa34
Half-steps for level in level-pan widget
author | Chris Cannam |
---|---|
date | Thu, 21 Jun 2018 15:36:29 +0100 |
parents | a34a2a25907c |
children | f3d3fab250ac |
comparison
equal
deleted
inserted
replaced
1300:1589bc7528b7 | 1301:e8368466fa34 |
---|---|
28 #include <cassert> | 28 #include <cassert> |
29 | 29 |
30 using std::cerr; | 30 using std::cerr; |
31 using std::endl; | 31 using std::endl; |
32 | 32 |
33 static const int maxLevel = 4; // min is 0, may be mute or not depending on m_includeMute | 33 /** |
34 * Gain and pan scales: | |
35 * | |
36 * Gain: we have 5 circles vertically in the display, each of which | |
37 * has half-circle and full-circle versions, and we also have "no | |
38 * circles", so there are in total 11 distinct levels, which we refer | |
39 * to as "notches" and number 0-10. (We use "notch" because "level" is | |
40 * used by the external API to refer to audio gain.) | |
41 * | |
42 * i.e. the levels are represented by these (schematic, rotated to | |
43 * horizontal) displays: | |
44 * | |
45 * 0 X | |
46 * 1 [ | |
47 * 2 [] | |
48 * 3 [][ | |
49 * ... | |
50 * 9 [][][][][ | |
51 * 10 [][][][][] | |
52 * | |
53 * If we have mute enabled, then we map the range 0-10 to gain using | |
54 * AudioLevel::fader_to_* with the ShortFader type, which treats fader | |
55 * 0 as muted. If mute is disabled, then we map the range 1-10. | |
56 * | |
57 * We can also disable half-circles, which leaves the range unchanged | |
58 * but limits the notches to even values. | |
59 * | |
60 * Pan: we have 5 columns with no finer resolution, so we only have 2 | |
61 * possible pan values on each side of centre. | |
62 */ | |
63 | |
34 static const int maxPan = 2; // range is -maxPan to maxPan | 64 static const int maxPan = 2; // range is -maxPan to maxPan |
35 | 65 |
36 LevelPanWidget::LevelPanWidget(QWidget *parent) : | 66 LevelPanWidget::LevelPanWidget(QWidget *parent) : |
37 QWidget(parent), | 67 QWidget(parent), |
38 m_level(maxLevel), | 68 m_minNotch(0), |
69 m_maxNotch(10), | |
70 m_notch(m_maxNotch), | |
39 m_pan(0), | 71 m_pan(0), |
40 m_monitorLeft(-1), | 72 m_monitorLeft(-1), |
41 m_monitorRight(-1), | 73 m_monitorRight(-1), |
42 m_editable(true), | 74 m_editable(true), |
43 m_editing(false), | 75 m_editing(false), |
44 m_includeMute(true) | 76 m_includeMute(true), |
77 m_includeHalfSteps(true) | |
45 { | 78 { |
46 setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan")); | 79 setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan")); |
47 setLevel(1.0); | 80 setLevel(1.0); |
48 setPan(0.0); | 81 setPan(0.0); |
49 } | 82 } |
65 LevelPanWidget::sizeHint() const | 98 LevelPanWidget::sizeHint() const |
66 { | 99 { |
67 return WidgetScale::scaleQSize(QSize(40, 40)); | 100 return WidgetScale::scaleQSize(QSize(40, 40)); |
68 } | 101 } |
69 | 102 |
70 static int | |
71 db_to_level(double db) | |
72 { | |
73 // Only if !m_includeMute, otherwise AudioLevel is used. | |
74 // Levels are: +6 0 -6 -12 -20 | |
75 assert(maxLevel == 4); | |
76 if (db > 3.) return 4; | |
77 else if (db > -3.) return 3; | |
78 else if (db > -9.) return 2; | |
79 else if (db > -16.) return 1; | |
80 else return 0; | |
81 } | |
82 | |
83 static double | |
84 level_to_db(int level) | |
85 { | |
86 // Only if !m_includeMute, otherwise AudioLevel is used. | |
87 // Levels are: +6 0 -6 -12 -20 | |
88 assert(maxLevel == 4); | |
89 if (level >= 4) return 6.; | |
90 else if (level == 3) return 0.; | |
91 else if (level == 2) return -6.; | |
92 else if (level == 1) return -12.; | |
93 else return -20.; | |
94 } | |
95 | |
96 int | 103 int |
97 LevelPanWidget::audioLevelToLevel(float audioLevel, bool withMute) | 104 LevelPanWidget::clampNotch(int notch) const |
98 { | 105 { |
99 int level; | 106 if (notch < m_minNotch) notch = m_minNotch; |
100 if (withMute) { | 107 if (notch > m_maxNotch) notch = m_maxNotch; |
101 level = AudioLevel::multiplier_to_fader | 108 if (!m_includeHalfSteps) { |
102 (audioLevel, maxLevel, AudioLevel::ShortFader); | 109 notch = (notch / 2) * 2; |
103 } else { | 110 } |
104 level = db_to_level(AudioLevel::multiplier_to_dB(audioLevel)); | 111 return notch; |
105 } | 112 } |
106 if (level < 0) level = 0; | 113 |
107 if (level > maxLevel) level = maxLevel; | 114 int |
108 return level; | 115 LevelPanWidget::audioLevelToNotch(float audioLevel) const |
116 { | |
117 int notch = AudioLevel::multiplier_to_fader | |
118 (audioLevel, m_maxNotch, AudioLevel::ShortFader); | |
119 return clampNotch(notch); | |
109 } | 120 } |
110 | 121 |
111 float | 122 float |
112 LevelPanWidget::levelToAudioLevel(int level, bool withMute) | 123 LevelPanWidget::notchToAudioLevel(int notch) const |
113 { | 124 { |
114 if (withMute) { | 125 return float(AudioLevel::fader_to_multiplier |
115 return float(AudioLevel::fader_to_multiplier | 126 (notch, m_maxNotch, AudioLevel::ShortFader)); |
116 (level, maxLevel, AudioLevel::ShortFader)); | 127 } |
117 } else { | 128 |
118 return float(AudioLevel::dB_to_multiplier(level_to_db(level))); | 129 void |
119 } | 130 LevelPanWidget::setLevel(float level) |
120 } | 131 { |
121 | 132 int notch = audioLevelToNotch(level); |
122 void | 133 if (notch != m_notch) { |
123 LevelPanWidget::setLevel(float flevel) | 134 m_notch = notch; |
124 { | |
125 int level = audioLevelToLevel(flevel, m_includeMute); | |
126 if (level != m_level) { | |
127 m_level = level; | |
128 float convertsTo = getLevel(); | 135 float convertsTo = getLevel(); |
129 if (fabsf(convertsTo - flevel) > 1e-5) { | 136 if (fabsf(convertsTo - level) > 1e-5) { |
130 emitLevelChanged(); | 137 emitLevelChanged(); |
131 } | 138 } |
132 update(); | 139 update(); |
133 } | 140 } |
141 SVCERR << "setLevel: level " << level << " -> notch " << m_notch << " (which converts back to level " << getLevel() << ")" << endl; | |
134 } | 142 } |
135 | 143 |
136 float | 144 float |
137 LevelPanWidget::getLevel() const | 145 LevelPanWidget::getLevel() const |
138 { | 146 { |
139 float flevel = levelToAudioLevel(m_level, m_includeMute); | 147 return notchToAudioLevel(m_notch); |
140 return flevel; | |
141 } | 148 } |
142 | 149 |
143 int | 150 int |
144 LevelPanWidget::audioPanToPan(float audioPan) | 151 LevelPanWidget::audioPanToPan(float audioPan) const |
145 { | 152 { |
146 int pan = int(round(audioPan * maxPan)); | 153 int pan = int(round(audioPan * maxPan)); |
147 if (pan < -maxPan) pan = -maxPan; | 154 if (pan < -maxPan) pan = -maxPan; |
148 if (pan > maxPan) pan = maxPan; | 155 if (pan > maxPan) pan = maxPan; |
149 return pan; | 156 return pan; |
150 } | 157 } |
151 | 158 |
152 float | 159 float |
153 LevelPanWidget::panToAudioPan(int pan) | 160 LevelPanWidget::panToAudioPan(int pan) const |
154 { | 161 { |
155 return float(pan) / float(maxPan); | 162 return float(pan) / float(maxPan); |
156 } | 163 } |
157 | 164 |
158 void | 165 void |
200 | 207 |
201 void | 208 void |
202 LevelPanWidget::setIncludeMute(bool include) | 209 LevelPanWidget::setIncludeMute(bool include) |
203 { | 210 { |
204 m_includeMute = include; | 211 m_includeMute = include; |
212 if (m_includeMute) { | |
213 m_minNotch = 0; | |
214 } else { | |
215 m_minNotch = 1; | |
216 } | |
205 emitLevelChanged(); | 217 emitLevelChanged(); |
206 update(); | 218 update(); |
207 } | 219 } |
208 | 220 |
209 void | 221 void |
242 LevelPanWidget::mouseMoveEvent(QMouseEvent *e) | 254 LevelPanWidget::mouseMoveEvent(QMouseEvent *e) |
243 { | 255 { |
244 if (!m_editable) return; | 256 if (!m_editable) return; |
245 if (!m_editing) return; | 257 if (!m_editing) return; |
246 | 258 |
247 int level, pan; | 259 int notch = coordsToNotch(rect(), e->pos()); |
248 toCell(rect(), e->pos(), level, pan); | 260 int pan = coordsToPan(rect(), e->pos()); |
249 if (level == m_level && pan == m_pan) { | 261 |
262 if (notch == m_notch && pan == m_pan) { | |
250 return; | 263 return; |
251 } | 264 } |
252 if (level != m_level) { | 265 if (notch != m_notch) { |
253 m_level = level; | 266 m_notch = notch; |
254 emitLevelChanged(); | 267 emitLevelChanged(); |
255 } | 268 } |
256 if (pan != m_pan) { | 269 if (pan != m_pan) { |
257 m_pan = pan; | 270 m_pan = pan; |
258 emitPanChanged(); | 271 emitPanChanged(); |
277 update(); | 290 update(); |
278 } | 291 } |
279 } | 292 } |
280 } else { | 293 } else { |
281 if (e->delta() > 0) { | 294 if (e->delta() > 0) { |
282 if (m_level < maxLevel) { | 295 if (m_notch < m_maxNotch) { |
283 ++m_level; | 296 ++m_notch; |
284 emitLevelChanged(); | 297 emitLevelChanged(); |
285 update(); | 298 update(); |
286 } | 299 } |
287 } else { | 300 } else { |
288 if (m_level > 0) { | 301 if (m_notch > m_minNotch) { |
289 --m_level; | 302 --m_notch; |
290 emitLevelChanged(); | 303 emitLevelChanged(); |
291 update(); | 304 update(); |
292 } | 305 } |
293 } | 306 } |
294 } | 307 } |
295 } | 308 } |
296 | 309 |
297 void | 310 int |
298 LevelPanWidget::toCell(QRectF rect, QPointF loc, int &level, int &pan) const | 311 LevelPanWidget::coordsToNotch(QRectF rect, QPointF loc) const |
299 { | 312 { |
300 double w = rect.width(), h = rect.height(); | 313 double h = rect.height(); |
314 | |
315 int nnotch = m_maxNotch + 1; | |
316 double cell = h / nnotch; | |
317 | |
318 int notch = int((h - (loc.y() - rect.y())) / cell); | |
319 notch = clampNotch(notch); | |
320 | |
321 return notch; | |
322 } | |
323 | |
324 int | |
325 LevelPanWidget::coordsToPan(QRectF rect, QPointF loc) const | |
326 { | |
327 double w = rect.width(); | |
301 | 328 |
302 int npan = maxPan * 2 + 1; | 329 int npan = maxPan * 2 + 1; |
303 int nlevel = maxLevel + 1; | 330 double cell = w / npan; |
304 | 331 |
305 double wcell = w / npan, hcell = h / nlevel; | 332 int pan = int((loc.x() - rect.x()) / cell) - maxPan; |
306 | |
307 level = int((h - (loc.y() - rect.y())) / hcell); | |
308 if (level < 0) level = 0; | |
309 if (level > maxLevel) level = maxLevel; | |
310 | |
311 pan = int((loc.x() - rect.x()) / wcell) - maxPan; | |
312 if (pan < -maxPan) pan = -maxPan; | 333 if (pan < -maxPan) pan = -maxPan; |
313 if (pan > maxPan) pan = maxPan; | 334 if (pan > maxPan) pan = maxPan; |
335 | |
336 return pan; | |
314 } | 337 } |
315 | 338 |
316 QSizeF | 339 QSizeF |
317 LevelPanWidget::cellSize(QRectF rect) const | 340 LevelPanWidget::cellSize(QRectF rect) const |
318 { | 341 { |
319 double w = rect.width(), h = rect.height(); | 342 double w = rect.width(), h = rect.height(); |
320 int npan = maxPan * 2 + 1; | 343 int ncol = maxPan * 2 + 1; |
321 int nlevel = maxLevel + 1; | 344 int nrow = m_maxNotch/2; |
322 double wcell = w / npan, hcell = h / nlevel; | 345 double wcell = w / ncol, hcell = h / nrow; |
323 return QSizeF(wcell, hcell); | 346 return QSizeF(wcell, hcell); |
324 } | 347 } |
325 | 348 |
326 QPointF | 349 QPointF |
327 LevelPanWidget::cellCentre(QRectF rect, int level, int pan) const | 350 LevelPanWidget::cellCentre(QRectF rect, int row, int col) const |
328 { | 351 { |
329 QSizeF cs = cellSize(rect); | 352 QSizeF cs = cellSize(rect); |
330 return QPointF(rect.x() + cs.width() * (pan + maxPan) + cs.width() / 2., | 353 return QPointF(rect.x() + |
331 rect.y() + rect.height() - cs.height() * (level + 1) + cs.height() / 2.); | 354 cs.width() * (col + maxPan) + cs.width() / 2., |
355 rect.y() + rect.height() - | |
356 cs.height() * (row + 1) + cs.height() / 2.); | |
332 } | 357 } |
333 | 358 |
334 QSizeF | 359 QSizeF |
335 LevelPanWidget::cellLightSize(QRectF rect) const | 360 LevelPanWidget::cellLightSize(QRectF rect) const |
336 { | 361 { |
339 double m = std::min(cs.width(), cs.height()); | 364 double m = std::min(cs.width(), cs.height()); |
340 return QSizeF(m * extent, m * extent); | 365 return QSizeF(m * extent, m * extent); |
341 } | 366 } |
342 | 367 |
343 QRectF | 368 QRectF |
344 LevelPanWidget::cellLightRect(QRectF rect, int level, int pan) const | 369 LevelPanWidget::cellLightRect(QRectF rect, int row, int col) const |
345 { | 370 { |
346 QSizeF cls = cellLightSize(rect); | 371 QSizeF cls = cellLightSize(rect); |
347 QPointF cc = cellCentre(rect, level, pan); | 372 QPointF cc = cellCentre(rect, row, col); |
348 return QRectF(cc.x() - cls.width() / 2., | 373 return QRectF(cc.x() - cls.width() / 2., |
349 cc.y() - cls.height() / 2., | 374 cc.y() - cls.height() / 2., |
350 cls.width(), | 375 cls.width(), |
351 cls.height()); | 376 cls.height()); |
352 } | 377 } |
353 | 378 |
354 double | 379 double |
355 LevelPanWidget::thinLineWidth(QRectF rect) const | 380 LevelPanWidget::thinLineWidth(QRectF rect) const |
356 { | 381 { |
357 double tw = ceil(rect.width() / (maxPan * 2. * 10.)); | 382 double tw = ceil(rect.width() / (maxPan * 2. * 10.)); |
358 double th = ceil(rect.height() / (maxLevel * 10.)); | 383 double th = ceil(rect.height() / (m_maxNotch/2 * 10.)); |
359 return std::min(th, tw); | 384 return std::min(th, tw); |
360 } | 385 } |
361 | 386 |
362 static QColor | 387 QRectF |
363 level_to_colour(int level) | 388 LevelPanWidget::cellOutlineRect(QRectF rect, int row, int col) const |
364 { | 389 { |
365 assert(maxLevel == 4); | 390 QRectF clr = cellLightRect(rect, row, col); |
366 if (level == 0) return Qt::black; | 391 double adj = thinLineWidth(rect)/2; |
367 else if (level == 1) return QColor(80, 0, 0); | 392 return clr.adjusted(-adj, -adj, adj, adj); |
368 else if (level == 2) return QColor(160, 0, 0); | 393 } |
369 else if (level == 3) return QColor(255, 0, 0); | 394 |
370 else return QColor(255, 255, 0); | 395 QColor |
396 LevelPanWidget::notchToColour(int notch) const | |
397 { | |
398 if (notch < 3) return Qt::black; | |
399 if (notch < 5) return QColor(80, 0, 0); | |
400 if (notch < 7) return QColor(160, 0, 0); | |
401 if (notch < 9) return QColor(255, 0, 0); | |
402 return QColor(255, 255, 0); | |
371 } | 403 } |
372 | 404 |
373 void | 405 void |
374 LevelPanWidget::renderTo(QPaintDevice *dev, QRectF rect, bool asIfEditable) const | 406 LevelPanWidget::renderTo(QPaintDevice *dev, QRectF rect, bool asIfEditable) const |
375 { | 407 { |
379 | 411 |
380 QPen pen; | 412 QPen pen; |
381 | 413 |
382 double thin = thinLineWidth(rect); | 414 double thin = thinLineWidth(rect); |
383 | 415 |
384 pen.setColor(QColor(127, 127, 127, 127)); | 416 QColor columnBackground = QColor(180, 180, 180); |
417 pen.setColor(columnBackground); | |
385 pen.setWidthF(cellLightSize(rect).width() + thin); | 418 pen.setWidthF(cellLightSize(rect).width() + thin); |
386 pen.setCapStyle(Qt::RoundCap); | 419 pen.setCapStyle(Qt::RoundCap); |
387 paint.setPen(pen); | 420 paint.setPen(pen); |
388 paint.setBrush(Qt::NoBrush); | 421 paint.setBrush(Qt::NoBrush); |
389 | 422 |
390 for (int pan = -maxPan; pan <= maxPan; ++pan) { | 423 for (int pan = -maxPan; pan <= maxPan; ++pan) { |
391 paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan)); | 424 paint.drawLine(cellCentre(rect, 0, pan), |
392 } | 425 cellCentre(rect, m_maxNotch/2 - 1, pan)); |
393 | 426 } |
394 if (m_monitorLeft > 0.f || m_monitorRight > 0.f) { | 427 |
428 bool monitoring = (m_monitorLeft > 0.f || m_monitorRight > 0.f); | |
429 | |
430 if (isEnabled()) { | |
431 pen.setColor(Qt::black); | |
432 } else { | |
433 pen.setColor(Qt::darkGray); | |
434 } | |
435 | |
436 if (!asIfEditable && m_includeMute && m_notch == 0) { | |
437 // The X for mute takes up the whole display when we're not | |
438 // being rendered in editable style | |
439 pen.setWidthF(thin * 2); | |
440 pen.setCapStyle(Qt::RoundCap); | |
441 paint.setPen(pen); | |
442 paint.drawLine(cellCentre(rect, 0, -maxPan), | |
443 cellCentre(rect, m_maxNotch/2 - 1, maxPan)); | |
444 paint.drawLine(cellCentre(rect, m_maxNotch/2 - 1, -maxPan), | |
445 cellCentre(rect, 0, maxPan)); | |
446 } else { | |
447 // the normal case | |
448 | |
449 // pen a bit less thin than in theory, so that we can erase | |
450 // semi-circles later without leaving a faint edge | |
451 pen.setWidthF(thin * 0.8); | |
452 pen.setCapStyle(Qt::FlatCap); | |
453 paint.setPen(pen); | |
454 | |
455 if (m_includeMute && m_notch == 0) { | |
456 QRectF clr = cellLightRect(rect, 0, m_pan); | |
457 paint.drawLine(clr.topLeft(), clr.bottomRight()); | |
458 paint.drawLine(clr.bottomLeft(), clr.topRight()); | |
459 } else { | |
460 for (int notch = 1; notch <= m_notch; notch += 2) { | |
461 if (isEnabled() && !monitoring) { | |
462 paint.setBrush(notchToColour(notch)); | |
463 } | |
464 QRectF clr = cellLightRect(rect, notch/2, m_pan); | |
465 paint.drawEllipse(clr); | |
466 } | |
467 if (m_notch % 2 != 0) { | |
468 QRectF clr = cellOutlineRect(rect, (m_notch-1)/2, m_pan); | |
469 paint.save(); | |
470 paint.setPen(Qt::NoPen); | |
471 paint.setBrush(columnBackground); | |
472 paint.drawPie(clr, 0, 180 * 16); | |
473 paint.restore(); | |
474 } | |
475 } | |
476 } | |
477 | |
478 if (monitoring) { | |
395 paint.setPen(Qt::NoPen); | 479 paint.setPen(Qt::NoPen); |
396 for (int pan = -maxPan; pan <= maxPan; ++pan) { | 480 for (int pan = -maxPan; pan <= maxPan; ++pan) { |
397 float audioPan = panToAudioPan(pan); | 481 float audioPan = panToAudioPan(pan); |
398 float audioLevel; | 482 float audioLevel; |
399 if (audioPan < 0.f) { | 483 if (audioPan < 0.f) { |
400 audioLevel = m_monitorLeft + m_monitorRight * (1.f + audioPan); | 484 audioLevel = m_monitorLeft + m_monitorRight * (1.f + audioPan); |
401 } else { | 485 } else { |
402 audioLevel = m_monitorRight + m_monitorLeft * (1.f - audioPan); | 486 audioLevel = m_monitorRight + m_monitorLeft * (1.f - audioPan); |
403 } | 487 } |
404 int levelHere = audioLevelToLevel(audioLevel, false); | 488 int notchHere = audioLevelToNotch(audioLevel); |
405 for (int level = 0; level <= levelHere; ++level) { | 489 for (int notch = 1; notch <= notchHere; notch += 2) { |
406 paint.setBrush(level_to_colour(level)); | 490 paint.setBrush(notchToColour(notch)); |
407 QRectF clr = cellLightRect(rect, level, pan); | 491 QRectF clr = cellLightRect(rect, (notch-1)/2, pan); |
408 paint.drawEllipse(clr); | 492 double adj = thinLineWidth(rect)/2; |
493 clr = clr.adjusted(adj, adj, -adj, -adj); | |
494 if (notch + 2 > notchHere && notchHere % 2 != 0) { | |
495 paint.drawPie(clr, 180 * 16, 180 * 16); | |
496 } else { | |
497 paint.drawEllipse(clr); | |
498 } | |
409 } | 499 } |
410 } | 500 } |
411 paint.setPen(pen); | 501 paint.setPen(pen); |
412 paint.setBrush(Qt::NoBrush); | 502 paint.setBrush(Qt::NoBrush); |
413 } | 503 } |
414 | |
415 if (isEnabled()) { | |
416 pen.setColor(Qt::black); | |
417 } else { | |
418 pen.setColor(Qt::darkGray); | |
419 } | |
420 | |
421 if (!asIfEditable && m_includeMute && m_level == 0) { | |
422 pen.setWidthF(thin * 2); | |
423 pen.setCapStyle(Qt::RoundCap); | |
424 paint.setPen(pen); | |
425 paint.drawLine(cellCentre(rect, 0, -maxPan), | |
426 cellCentre(rect, maxLevel, maxPan)); | |
427 paint.drawLine(cellCentre(rect, maxLevel, -maxPan), | |
428 cellCentre(rect, 0, maxPan)); | |
429 return; | |
430 } | |
431 | |
432 pen.setWidthF(thin); | |
433 pen.setCapStyle(Qt::FlatCap); | |
434 paint.setPen(pen); | |
435 | |
436 for (int level = 0; level <= m_level; ++level) { | |
437 if (isEnabled()) { | |
438 paint.setBrush(level_to_colour(level)); | |
439 } | |
440 QRectF clr = cellLightRect(rect, level, m_pan); | |
441 if (m_includeMute && m_level == 0) { | |
442 paint.drawLine(clr.topLeft(), clr.bottomRight()); | |
443 paint.drawLine(clr.bottomLeft(), clr.topRight()); | |
444 } else { | |
445 paint.drawEllipse(clr); | |
446 } | |
447 } | |
448 } | 504 } |
449 | 505 |
450 void | 506 void |
451 LevelPanWidget::paintEvent(QPaintEvent *) | 507 LevelPanWidget::paintEvent(QPaintEvent *) |
452 { | 508 { |