Mercurial > hg > svgui
comparison layer/NoteLayer.cpp @ 30:ea6fe8cfcdd5
* Add the Note layer for pianoroll-type display of note-type data
* Complete the MIDI file importer (well, nearly -- it would be nice to
be able to import the non-note data as other sorts of models, and that's
not done yet).
* Minor refactoring in RealTime etc
author | Chris Cannam |
---|---|
date | Fri, 10 Feb 2006 17:51:36 +0000 |
parents | |
children | 1bdf285c4eac |
comparison
equal
deleted
inserted
replaced
29:9f55af9676b4 | 30:ea6fe8cfcdd5 |
---|---|
1 /* -*- c-basic-offset: 4 -*- vi:set ts=8 sts=4 sw=4: */ | |
2 | |
3 /* | |
4 A waveform viewer and audio annotation editor. | |
5 Chris Cannam, Queen Mary University of London, 2005-2006 | |
6 | |
7 This is experimental software. Not for distribution. | |
8 */ | |
9 | |
10 #include "NoteLayer.h" | |
11 | |
12 #include "base/Model.h" | |
13 #include "base/RealTime.h" | |
14 #include "base/Profiler.h" | |
15 #include "base/Pitch.h" | |
16 #include "base/View.h" | |
17 | |
18 #include "model/NoteModel.h" | |
19 | |
20 #include <QPainter> | |
21 #include <QPainterPath> | |
22 #include <QMouseEvent> | |
23 | |
24 #include <iostream> | |
25 #include <cmath> | |
26 | |
27 NoteLayer::NoteLayer(View *w) : | |
28 Layer(w), | |
29 m_model(0), | |
30 m_editing(false), | |
31 m_originalPoint(0, 0.0, 0, tr("New Point")), | |
32 m_editingPoint(0, 0.0, 0, tr("New Point")), | |
33 m_editingCommand(0), | |
34 m_colour(Qt::black), | |
35 m_verticalScale(MinMaxRangeScale) | |
36 { | |
37 m_view->addLayer(this); | |
38 } | |
39 | |
40 void | |
41 NoteLayer::setModel(NoteModel *model) | |
42 { | |
43 if (m_model == model) return; | |
44 m_model = model; | |
45 | |
46 connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged())); | |
47 connect(m_model, SIGNAL(modelChanged(size_t, size_t)), | |
48 this, SIGNAL(modelChanged(size_t, size_t))); | |
49 | |
50 connect(m_model, SIGNAL(completionChanged()), | |
51 this, SIGNAL(modelCompletionChanged())); | |
52 | |
53 std::cerr << "NoteLayer::setModel(" << model << ")" << std::endl; | |
54 | |
55 emit modelReplaced(); | |
56 } | |
57 | |
58 Layer::PropertyList | |
59 NoteLayer::getProperties() const | |
60 { | |
61 PropertyList list; | |
62 list.push_back(tr("Colour")); | |
63 list.push_back(tr("Vertical Scale")); | |
64 return list; | |
65 } | |
66 | |
67 Layer::PropertyType | |
68 NoteLayer::getPropertyType(const PropertyName &) const | |
69 { | |
70 return ValueProperty; | |
71 } | |
72 | |
73 int | |
74 NoteLayer::getPropertyRangeAndValue(const PropertyName &name, | |
75 int *min, int *max) const | |
76 { | |
77 //!!! factor this colour handling stuff out into a colour manager class | |
78 | |
79 int deft = 0; | |
80 | |
81 if (name == tr("Colour")) { | |
82 | |
83 if (min) *min = 0; | |
84 if (max) *max = 5; | |
85 | |
86 if (m_colour == Qt::black) deft = 0; | |
87 else if (m_colour == Qt::darkRed) deft = 1; | |
88 else if (m_colour == Qt::darkBlue) deft = 2; | |
89 else if (m_colour == Qt::darkGreen) deft = 3; | |
90 else if (m_colour == QColor(200, 50, 255)) deft = 4; | |
91 else if (m_colour == QColor(255, 150, 50)) deft = 5; | |
92 | |
93 } else if (name == tr("Vertical Scale")) { | |
94 | |
95 if (min) *min = 0; | |
96 if (max) *max = 2; | |
97 | |
98 deft = int(m_verticalScale); | |
99 | |
100 } else { | |
101 | |
102 deft = Layer::getPropertyRangeAndValue(name, min, max); | |
103 } | |
104 | |
105 return deft; | |
106 } | |
107 | |
108 QString | |
109 NoteLayer::getPropertyValueLabel(const PropertyName &name, | |
110 int value) const | |
111 { | |
112 if (name == tr("Colour")) { | |
113 switch (value) { | |
114 default: | |
115 case 0: return tr("Black"); | |
116 case 1: return tr("Red"); | |
117 case 2: return tr("Blue"); | |
118 case 3: return tr("Green"); | |
119 case 4: return tr("Purple"); | |
120 case 5: return tr("Orange"); | |
121 } | |
122 } else if (name == tr("Vertical Scale")) { | |
123 switch (value) { | |
124 default: | |
125 case 0: return tr("Note Range In Use"); | |
126 case 1: return tr("MIDI Note Range"); | |
127 case 2: return tr("Frequency"); | |
128 } | |
129 } | |
130 return tr("<unknown>"); | |
131 } | |
132 | |
133 void | |
134 NoteLayer::setProperty(const PropertyName &name, int value) | |
135 { | |
136 if (name == tr("Colour")) { | |
137 switch (value) { | |
138 default: | |
139 case 0: setBaseColour(Qt::black); break; | |
140 case 1: setBaseColour(Qt::darkRed); break; | |
141 case 2: setBaseColour(Qt::darkBlue); break; | |
142 case 3: setBaseColour(Qt::darkGreen); break; | |
143 case 4: setBaseColour(QColor(200, 50, 255)); break; | |
144 case 5: setBaseColour(QColor(255, 150, 50)); break; | |
145 } | |
146 } else if (name == tr("Vertical Scale")) { | |
147 setVerticalScale(VerticalScale(value)); | |
148 } | |
149 } | |
150 | |
151 void | |
152 NoteLayer::setBaseColour(QColor colour) | |
153 { | |
154 if (m_colour == colour) return; | |
155 m_colour = colour; | |
156 emit layerParametersChanged(); | |
157 } | |
158 | |
159 void | |
160 NoteLayer::setVerticalScale(VerticalScale scale) | |
161 { | |
162 if (m_verticalScale == scale) return; | |
163 m_verticalScale = scale; | |
164 emit layerParametersChanged(); | |
165 } | |
166 | |
167 bool | |
168 NoteLayer::isLayerScrollable() const | |
169 { | |
170 QPoint discard; | |
171 return !m_view->shouldIlluminateLocalFeatures(this, discard); | |
172 } | |
173 | |
174 NoteModel::PointList | |
175 NoteLayer::getLocalPoints(int x) const | |
176 { | |
177 if (!m_model) return NoteModel::PointList(); | |
178 | |
179 long frame = getFrameForX(x); | |
180 | |
181 NoteModel::PointList onPoints = | |
182 m_model->getPoints(frame); | |
183 | |
184 if (!onPoints.empty()) { | |
185 return onPoints; | |
186 } | |
187 | |
188 NoteModel::PointList prevPoints = | |
189 m_model->getPreviousPoints(frame); | |
190 NoteModel::PointList nextPoints = | |
191 m_model->getNextPoints(frame); | |
192 | |
193 NoteModel::PointList usePoints = prevPoints; | |
194 | |
195 if (prevPoints.empty()) { | |
196 usePoints = nextPoints; | |
197 } else if (prevPoints.begin()->frame < m_view->getStartFrame() && | |
198 !(nextPoints.begin()->frame > m_view->getEndFrame())) { | |
199 usePoints = nextPoints; | |
200 } else if (nextPoints.begin()->frame - frame < | |
201 frame - prevPoints.begin()->frame) { | |
202 usePoints = nextPoints; | |
203 } | |
204 | |
205 if (!usePoints.empty()) { | |
206 int fuzz = 2; | |
207 int px = getXForFrame(usePoints.begin()->frame); | |
208 if ((px > x && px - x > fuzz) || | |
209 (px < x && x - px > fuzz + 1)) { | |
210 usePoints.clear(); | |
211 } | |
212 } | |
213 | |
214 return usePoints; | |
215 } | |
216 | |
217 QString | |
218 NoteLayer::getFeatureDescription(QPoint &pos) const | |
219 { | |
220 int x = pos.x(); | |
221 | |
222 if (!m_model || !m_model->getSampleRate()) return ""; | |
223 | |
224 NoteModel::PointList points = getLocalPoints(x); | |
225 | |
226 if (points.empty()) { | |
227 if (!m_model->isReady()) { | |
228 return tr("In progress"); | |
229 } else { | |
230 return tr("No local points"); | |
231 } | |
232 } | |
233 | |
234 Note note(0); | |
235 NoteModel::PointList::iterator i; | |
236 | |
237 for (i = points.begin(); i != points.end(); ++i) { | |
238 | |
239 int y = getYForValue(i->value); | |
240 int h = 3; | |
241 | |
242 if (m_model->getValueQuantization() != 0.0) { | |
243 h = y - getYForValue(i->value + m_model->getValueQuantization()); | |
244 if (h < 3) h = 3; | |
245 } | |
246 | |
247 if (pos.y() >= y - h && pos.y() <= y) { | |
248 note = *i; | |
249 break; | |
250 } | |
251 } | |
252 | |
253 if (i == points.end()) return tr("No local points"); | |
254 | |
255 RealTime rt = RealTime::frame2RealTime(note.frame, | |
256 m_model->getSampleRate()); | |
257 RealTime rd = RealTime::frame2RealTime(note.duration, | |
258 m_model->getSampleRate()); | |
259 | |
260 QString text; | |
261 | |
262 if (note.label == "") { | |
263 text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nNo label")) | |
264 .arg(rt.toText(true).c_str()) | |
265 .arg(note.value) | |
266 .arg(rd.toText(true).c_str()); | |
267 } else { | |
268 text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nLabel:\t%4")) | |
269 .arg(rt.toText(true).c_str()) | |
270 .arg(note.value) | |
271 .arg(rd.toText(true).c_str()) | |
272 .arg(note.label); | |
273 } | |
274 | |
275 pos = QPoint(getXForFrame(note.frame), | |
276 getYForValue(note.value)); | |
277 return text; | |
278 } | |
279 | |
280 bool | |
281 NoteLayer::snapToFeatureFrame(int &frame, | |
282 size_t &resolution, | |
283 SnapType snap) const | |
284 { | |
285 if (!m_model) { | |
286 return Layer::snapToFeatureFrame(frame, resolution, snap); | |
287 } | |
288 | |
289 resolution = m_model->getResolution(); | |
290 NoteModel::PointList points; | |
291 | |
292 if (snap == SnapNeighbouring) { | |
293 | |
294 points = getLocalPoints(getXForFrame(frame)); | |
295 if (points.empty()) return false; | |
296 frame = points.begin()->frame; | |
297 return true; | |
298 } | |
299 | |
300 points = m_model->getPoints(frame, frame); | |
301 int snapped = frame; | |
302 bool found = false; | |
303 | |
304 for (NoteModel::PointList::const_iterator i = points.begin(); | |
305 i != points.end(); ++i) { | |
306 | |
307 if (snap == SnapRight) { | |
308 | |
309 if (i->frame > frame) { | |
310 snapped = i->frame; | |
311 found = true; | |
312 break; | |
313 } | |
314 | |
315 } else if (snap == SnapLeft) { | |
316 | |
317 if (i->frame <= frame) { | |
318 snapped = i->frame; | |
319 found = true; // don't break, as the next may be better | |
320 } else { | |
321 break; | |
322 } | |
323 | |
324 } else { // nearest | |
325 | |
326 NoteModel::PointList::const_iterator j = i; | |
327 ++j; | |
328 | |
329 if (j == points.end()) { | |
330 | |
331 snapped = i->frame; | |
332 found = true; | |
333 break; | |
334 | |
335 } else if (j->frame >= frame) { | |
336 | |
337 if (j->frame - frame < frame - i->frame) { | |
338 snapped = j->frame; | |
339 } else { | |
340 snapped = i->frame; | |
341 } | |
342 found = true; | |
343 break; | |
344 } | |
345 } | |
346 } | |
347 | |
348 frame = snapped; | |
349 return found; | |
350 } | |
351 | |
352 int | |
353 NoteLayer::getYForValue(float value) const | |
354 { | |
355 float min, max, h = m_view->height(); | |
356 | |
357 switch (m_verticalScale) { | |
358 | |
359 case MIDIRangeScale: | |
360 min = 0.0; | |
361 max = 127.0; | |
362 break; | |
363 | |
364 case MinMaxRangeScale: | |
365 min = m_model->getValueMinimum(); | |
366 max = m_model->getValueMaximum(); | |
367 break; | |
368 | |
369 case FrequencyScale: | |
370 std::cerr << "FrequencyScale: value in = " << value << std::endl; | |
371 min = m_model->getValueMinimum(); | |
372 min = Pitch::getFrequencyForPitch(lrintf(min), min - lrintf(min)); | |
373 max = m_model->getValueMaximum(); | |
374 max = Pitch::getFrequencyForPitch(lrintf(max), max - lrintf(max)); | |
375 value = Pitch::getFrequencyForPitch(lrintf(value), value - lrintf(value)); | |
376 std::cerr << "FrequencyScale: min = " << min << ", max = " << max << ", value = " << value << std::endl; | |
377 break; | |
378 } | |
379 | |
380 if (max < min) max = min; | |
381 max = max + 1.0; | |
382 | |
383 return int(h - ((value - min) * h) / (max - min)) - 1; | |
384 } | |
385 | |
386 float | |
387 NoteLayer::getValueForY(int y) const | |
388 { | |
389 float min = m_model->getValueMinimum(); | |
390 float max = m_model->getValueMaximum(); | |
391 if (max == min) max = min + 1.0; | |
392 | |
393 int h = m_view->height(); | |
394 | |
395 return min + (float(h - y) * float(max - min)) / h; | |
396 } | |
397 | |
398 void | |
399 NoteLayer::paint(QPainter &paint, QRect rect) const | |
400 { | |
401 if (!m_model || !m_model->isOK()) return; | |
402 | |
403 int sampleRate = m_model->getSampleRate(); | |
404 if (!sampleRate) return; | |
405 | |
406 // Profiler profiler("NoteLayer::paint", true); | |
407 | |
408 int x0 = rect.left(), x1 = rect.right(); | |
409 long frame0 = getFrameForX(x0); | |
410 long frame1 = getFrameForX(x1); | |
411 | |
412 NoteModel::PointList points(m_model->getPoints(frame0, frame1)); | |
413 if (points.empty()) return; | |
414 | |
415 paint.setPen(m_colour); | |
416 | |
417 QColor brushColour(m_colour); | |
418 brushColour.setAlpha(80); | |
419 | |
420 // std::cerr << "NoteLayer::paint: resolution is " | |
421 // << m_model->getResolution() << " frames" << std::endl; | |
422 | |
423 float min = m_model->getValueMinimum(); | |
424 float max = m_model->getValueMaximum(); | |
425 if (max == min) max = min + 1.0; | |
426 | |
427 int origin = int(nearbyint(m_view->height() - | |
428 (-min * m_view->height()) / (max - min))); | |
429 | |
430 QPoint localPos; | |
431 long illuminateFrame = -1; | |
432 | |
433 if (m_view->shouldIlluminateLocalFeatures(this, localPos)) { | |
434 NoteModel::PointList localPoints = | |
435 getLocalPoints(localPos.x()); | |
436 if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame; | |
437 } | |
438 | |
439 paint.save(); | |
440 paint.setRenderHint(QPainter::Antialiasing, false); | |
441 | |
442 for (NoteModel::PointList::const_iterator i = points.begin(); | |
443 i != points.end(); ++i) { | |
444 | |
445 const NoteModel::Point &p(*i); | |
446 | |
447 int x = getXForFrame(p.frame); | |
448 int y = getYForValue(p.value); | |
449 int w = getXForFrame(p.frame + p.duration) - x; | |
450 int h = 3; | |
451 | |
452 if (m_model->getValueQuantization() != 0.0) { | |
453 h = y - getYForValue(p.value + m_model->getValueQuantization()); | |
454 if (h < 3) h = 3; | |
455 } | |
456 | |
457 if (w < 1) w = 1; | |
458 paint.setPen(m_colour); | |
459 paint.setBrush(brushColour); | |
460 | |
461 if (illuminateFrame == p.frame) { | |
462 if (localPos.y() >= y - h && localPos.y() < y) { | |
463 paint.setPen(Qt::black);//!!! | |
464 paint.setBrush(Qt::black);//!!! | |
465 } | |
466 } | |
467 | |
468 paint.drawRect(x, y - h, w, h); | |
469 | |
470 /// if (p.label != "") { | |
471 /// paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label); | |
472 /// } | |
473 } | |
474 | |
475 paint.restore(); | |
476 } | |
477 | |
478 void | |
479 NoteLayer::drawStart(QMouseEvent *e) | |
480 { | |
481 std::cerr << "NoteLayer::drawStart(" << e->x() << "," << e->y() << ")" << std::endl; | |
482 | |
483 if (!m_model) return; | |
484 | |
485 long frame = getFrameForX(e->x()); | |
486 if (frame < 0) frame = 0; | |
487 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
488 | |
489 float value = getValueForY(e->y()); | |
490 | |
491 m_editingPoint = NoteModel::Point(frame, value, 0, tr("New Point")); | |
492 m_originalPoint = m_editingPoint; | |
493 | |
494 if (m_editingCommand) m_editingCommand->finish(); | |
495 m_editingCommand = new NoteModel::EditCommand(m_model, | |
496 tr("Draw Point")); | |
497 m_editingCommand->addPoint(m_editingPoint); | |
498 | |
499 m_editing = true; | |
500 } | |
501 | |
502 void | |
503 NoteLayer::drawDrag(QMouseEvent *e) | |
504 { | |
505 std::cerr << "NoteLayer::drawDrag(" << e->x() << "," << e->y() << ")" << std::endl; | |
506 | |
507 if (!m_model || !m_editing) return; | |
508 | |
509 long frame = getFrameForX(e->x()); | |
510 if (frame < 0) frame = 0; | |
511 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
512 | |
513 float value = getValueForY(e->y()); | |
514 | |
515 m_editingCommand->deletePoint(m_editingPoint); | |
516 m_editingPoint.frame = frame; | |
517 m_editingPoint.value = value; | |
518 m_editingCommand->addPoint(m_editingPoint); | |
519 } | |
520 | |
521 void | |
522 NoteLayer::drawEnd(QMouseEvent *e) | |
523 { | |
524 std::cerr << "NoteLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl; | |
525 if (!m_model || !m_editing) return; | |
526 m_editingCommand->finish(); | |
527 m_editingCommand = 0; | |
528 m_editing = false; | |
529 } | |
530 | |
531 void | |
532 NoteLayer::editStart(QMouseEvent *e) | |
533 { | |
534 std::cerr << "NoteLayer::editStart(" << e->x() << "," << e->y() << ")" << std::endl; | |
535 | |
536 if (!m_model) return; | |
537 | |
538 NoteModel::PointList points = getLocalPoints(e->x()); | |
539 if (points.empty()) return; | |
540 | |
541 m_editingPoint = *points.begin(); | |
542 m_originalPoint = m_editingPoint; | |
543 | |
544 if (m_editingCommand) { | |
545 m_editingCommand->finish(); | |
546 m_editingCommand = 0; | |
547 } | |
548 | |
549 m_editing = true; | |
550 } | |
551 | |
552 void | |
553 NoteLayer::editDrag(QMouseEvent *e) | |
554 { | |
555 std::cerr << "NoteLayer::editDrag(" << e->x() << "," << e->y() << ")" << std::endl; | |
556 | |
557 if (!m_model || !m_editing) return; | |
558 | |
559 long frame = getFrameForX(e->x()); | |
560 if (frame < 0) frame = 0; | |
561 frame = frame / m_model->getResolution() * m_model->getResolution(); | |
562 | |
563 float value = getValueForY(e->y()); | |
564 | |
565 if (!m_editingCommand) { | |
566 m_editingCommand = new NoteModel::EditCommand(m_model, | |
567 tr("Drag Point")); | |
568 } | |
569 | |
570 m_editingCommand->deletePoint(m_editingPoint); | |
571 m_editingPoint.frame = frame; | |
572 m_editingPoint.value = value; | |
573 m_editingCommand->addPoint(m_editingPoint); | |
574 } | |
575 | |
576 void | |
577 NoteLayer::editEnd(QMouseEvent *e) | |
578 { | |
579 std::cerr << "NoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl; | |
580 if (!m_model || !m_editing) return; | |
581 | |
582 if (m_editingCommand) { | |
583 | |
584 QString newName = m_editingCommand->getName(); | |
585 | |
586 if (m_editingPoint.frame != m_originalPoint.frame) { | |
587 if (m_editingPoint.value != m_originalPoint.value) { | |
588 newName = tr("Edit Point"); | |
589 } else { | |
590 newName = tr("Relocate Point"); | |
591 } | |
592 } else { | |
593 newName = tr("Change Point Value"); | |
594 } | |
595 | |
596 m_editingCommand->setName(newName); | |
597 m_editingCommand->finish(); | |
598 } | |
599 | |
600 m_editingCommand = 0; | |
601 m_editing = false; | |
602 } | |
603 | |
604 QString | |
605 NoteLayer::toXmlString(QString indent, QString extraAttributes) const | |
606 { | |
607 return Layer::toXmlString(indent, extraAttributes + | |
608 QString(" colour=\"%1\" verticalScale=\"%2\"") | |
609 .arg(encodeColour(m_colour)).arg(m_verticalScale)); | |
610 } | |
611 | |
612 void | |
613 NoteLayer::setProperties(const QXmlAttributes &attributes) | |
614 { | |
615 QString colourSpec = attributes.value("colour"); | |
616 if (colourSpec != "") { | |
617 QColor colour(colourSpec); | |
618 if (colour.isValid()) { | |
619 setBaseColour(QColor(colourSpec)); | |
620 } | |
621 } | |
622 | |
623 bool ok; | |
624 VerticalScale scale = (VerticalScale) | |
625 attributes.value("verticalScale").toInt(&ok); | |
626 if (ok) setVerticalScale(scale); | |
627 } | |
628 | |
629 |