1 module soundtab.ui.instredit; 2 3 import dlangui.platforms.common.platform; 4 import dlangui.core.logger; 5 import dlangui.core.i18n; 6 import dlangui.core.stdaction; 7 import dlangui.widgets.widget; 8 import dlangui.widgets.layouts; 9 import dlangui.widgets.controls; 10 import dlangui.widgets.toolbars; 11 import dlangui.widgets.scrollbar; 12 import dlangui.dialogs.dialog; 13 import dlangui.dialogs.filedlg; 14 import soundtab.ui.actions; 15 import soundtab.audio.audiosource; 16 import soundtab.audio.loader; 17 import soundtab.audio.mp3player; 18 19 class HRuler : Widget { 20 float _startPos = 0; 21 float _totalDuration = 0; 22 float _visibleDuration = 0; 23 24 this() { 25 super("hruler"); 26 layoutWidth = FILL_PARENT; 27 backgroundColor(0x202020); 28 textColor(0xC0C0C0); 29 fontSize = 10; 30 } 31 /** 32 Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 33 */ 34 override void measure(int parentWidth, int parentHeight) { 35 int fh = font.height; 36 measuredContent(parentWidth, parentHeight, 0, fh + 8); 37 } 38 39 void setPosition(float startPos, float totalDuration, float visibleDuration) { 40 _startPos = startPos; 41 _totalDuration = totalDuration; 42 _visibleDuration = visibleDuration; 43 invalidate(); 44 } 45 46 /// Draw widget at its position to buffer 47 override void onDraw(DrawBuf buf) { 48 import std.format; 49 import std.math : round; 50 import std.utf : toUTF32; 51 if (visibility != Visibility.Visible) 52 return; 53 super.onDraw(buf); 54 _needDraw = false; 55 auto saver = ClipRectSaver(buf, _pos, alpha); 56 int longDy = _pos.height - font.height - 2; 57 int shortDy = longDy * 40 / 100; 58 int ytext = _pos.bottom - font.height - 2; 59 if (_visibleDuration > 0.00001 && _totalDuration > 0.00001 && _pos.width > 10) { 60 double secondsPerPixel = _visibleDuration / _pos.width; 61 double scale = 0.00001; 62 for (; scale < 10000; scale = scale * 10) { 63 if (scale / secondsPerPixel >= 50) 64 break; 65 } 66 double t = (cast(int)round(_startPos / scale)) * scale - scale; 67 for(; t < _startPos + _visibleDuration + scale; t += scale) { 68 if (t < 0) 69 continue; 70 int x = cast(int)(_pos.left + (t - _startPos) / secondsPerPixel); 71 buf.fillRect(Rect(x, _pos.top, x + 1, _pos.top + longDy), 0xB0B0A0); 72 for (int xx = 1; xx < 10; xx++) { 73 double tt = t + xx * scale / 10; 74 int xxx = cast(int)(_pos.left + (tt - _startPos) / secondsPerPixel); 75 buf.fillRect(Rect(xxx, _pos.top, xxx + 1, _pos.top + shortDy), 0xB0B0A0); 76 } 77 int seconds = cast(int)t; 78 int minutes = seconds / 60; 79 seconds = seconds % 60; 80 string txt; 81 if (scale >= 1) 82 txt = "%d:%02d".format(minutes,seconds); 83 else if (scale >= 0.1) { 84 int frac = cast(int)round(((t - seconds) * 10)); 85 txt = "%d:%02d.%01d".format(minutes,seconds,frac); 86 } else if (scale >= 0.01) { 87 int frac = cast(int)round(((t - seconds) * 100)); 88 txt = "%d:%02d.%02d".format(minutes,seconds,frac); 89 } else if (scale >= 0.001) { 90 int frac = cast(int)round(((t - seconds) * 1000)); 91 txt = "%d:%02d.%03d".format(minutes,seconds,frac); 92 } else { 93 int frac = cast(int)round(((t - seconds) * 10000)); 94 txt = "%d:%02d.%04d".format(minutes,seconds,frac); 95 } 96 dstring dtxt = txt.toUTF32; 97 int w = font.textSize(dtxt).x; 98 font.drawText(buf, x - w / 2, ytext, dtxt, 0x808080); 99 } 100 } 101 } 102 103 } 104 105 struct MinMax { 106 float minvalue = 0; 107 float maxvalue = 0; 108 @property float amplitude() { 109 float n1 = minvalue < 0 ? -minvalue : minvalue; 110 float n2 = maxvalue < 0 ? -maxvalue : maxvalue; 111 return n1 > n2 ? n1 : n2; 112 } 113 } 114 115 class WaveFileWidget : WidgetGroupDefaultDrawing { 116 protected Mixer _mixer; 117 protected Mp3Player _player; 118 protected WaveFile _file; 119 protected ScrollBar _hScroll; 120 protected HRuler _hruler; 121 protected Rect _clientRect; 122 protected int _hscale = 1; 123 protected float _vscale = 1; 124 protected int _scrollPos = 0; 125 protected int _cursorPos = 0; 126 protected int _selStart = 0; 127 protected int _selEnd = 0; 128 protected MinMax[] _zoomCache; 129 protected int _zoomCacheScale; 130 protected ulong _playTimer; 131 this(Mixer mixer) { 132 _mixer = mixer; 133 _player = new Mp3Player(); 134 _mixer.addSource(_player); 135 _hruler = new HRuler(); 136 _hScroll = new ScrollBar("wavehscroll", Orientation.Horizontal); 137 _hScroll.layoutWidth = FILL_PARENT; 138 //styleId = "EDIT_BOX"; 139 backgroundImageId = "editbox_background_dark"; 140 addChild(_hruler); 141 addChild(_hScroll); 142 _hScroll.scrollEvent = &onScrollEvent; 143 focusable = true; 144 clickable = true; 145 padding = Rect(3,3,3,3); 146 margins = Rect(2,2,2,2); 147 acceleratorMap.add([ 148 ACTION_VIEW_HZOOM_1, ACTION_VIEW_HZOOM_IN, ACTION_VIEW_HZOOM_OUT, ACTION_VIEW_HZOOM_MAX, ACTION_VIEW_HZOOM_SEL, 149 ACTION_VIEW_VZOOM_1, ACTION_VIEW_VZOOM_IN, ACTION_VIEW_VZOOM_OUT, ACTION_VIEW_VZOOM_MAX, 150 ACTION_INSTRUMENT_OPEN_SOUND_FILE, ACTION_INSTRUMENT_PLAY_PAUSE, ACTION_INSTRUMENT_PLAY_PAUSE_SELECTION]); 151 } 152 ~this() { 153 if (_mixer) { 154 _player.paused = true; 155 _mixer.removeSource(_player); 156 destroy(_player); 157 } 158 } 159 @property WaveFile file() { return _file; } 160 @property void file(WaveFile f) { 161 if (_player) { 162 _player.paused = true; 163 _player.setWave(f, false); 164 } 165 _file = f; 166 _selStart = _selEnd = 0; 167 _cursorPos = 0; 168 _scrollPos = 0; 169 _zoomCache = null; 170 zoomFull(); 171 if (window) 172 window.update(); 173 } 174 @property Mp3Player player() { return _player; } 175 176 /// get data to display - override to show some other data than wave (e.g. to show excitation) 177 float[] getDisplayData() { 178 if (!_file) 179 return null; 180 return _file.data; 181 } 182 183 /// override to allow extra views 184 int getExtraViewsHeight(int parentHeight) { return 0; } 185 /// override to allow extra views 186 void layoutExtraViews(Rect rc) { } 187 /// override to allow extra views 188 void drawExtraViews(DrawBuf buf) {} 189 190 void updateZoomCache() { 191 if (!_file || _zoomCacheScale == _hscale || _hscale <= 16) 192 return; 193 int len = (_file.frames + _hscale - 1) / _hscale; 194 _zoomCache = new MinMax[len]; 195 for (int i = 0; i < len; i++) { 196 _zoomCache[i] = getDisplayValuesNoCache(i); 197 } 198 _zoomCacheScale = _hscale; 199 } 200 201 void updateView() { 202 updateScrollBar(); 203 invalidate(); 204 } 205 void zoomFull() { 206 _scrollPos = 0; 207 _hscale = 1; 208 _vscale = 1; 209 if (_file) { 210 _hscale = _file.frames / (_clientRect.width ? _clientRect.width : 1); 211 _vscale = visibleYRange().amplitude; 212 if (_vscale > 0) 213 _vscale = 1 / _vscale; 214 else 215 _vscale = 1; 216 } 217 updateView(); 218 invalidate(); 219 } 220 MinMax visibleYRange() { 221 MinMax res; 222 for (int i = 0; i < _clientRect.width; i++) { 223 MinMax m = getDisplayValues(i); 224 if (res.minvalue > m.minvalue) 225 res.minvalue = m.minvalue; 226 if (res.maxvalue < m.maxvalue) 227 res.maxvalue = m.maxvalue; 228 } 229 return res; 230 } 231 /// process key event, return true if event is processed. 232 override bool onKeyEvent(KeyEvent event) { 233 if (event.action == KeyAction.KeyDown) { 234 switch(event.keyCode) { 235 case KeyCode.HOME: 236 _scrollPos = 0; 237 updateView(); 238 return true; 239 case KeyCode.END: 240 _scrollPos = _file ? _file.frames / _hscale - _clientRect.width : 0; 241 updateView(); 242 return true; 243 case KeyCode.RIGHT: 244 _scrollPos += _clientRect.width / 5; 245 updateView(); 246 return true; 247 case KeyCode.LEFT: 248 _scrollPos -= _clientRect.width / 5; 249 updateView(); 250 return true; 251 default: 252 break; 253 } 254 } 255 if (_hScroll.onKeyEvent(event)) 256 return true; 257 return super.onKeyEvent(event); 258 } 259 260 void openSampleFile(string filename) { 261 WaveFile f = loadSoundFile(filename); 262 if (f) { 263 file = f; 264 } 265 } 266 void openSampleFile() { 267 import std.file; 268 FileDialog dlg = new FileDialog(UIString("Open Sample MP3 file"d), window, null); 269 dlg.addFilter(FileFilterEntry(UIString("Sound files (*.mp3;*.wav)"d), "*.mp3;*.wav")); 270 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 271 if (result.id == ACTION_OPEN.id) { 272 string filename = result.stringParam; 273 if (filename.exists && filename.isFile) { 274 openSampleFile(filename); 275 } 276 } 277 }; 278 dlg.show(); 279 } 280 281 /// override to handle specific actions 282 override bool handleAction(const Action a) { 283 switch(a.id) { 284 case Actions.ViewHZoomIn: 285 _hscale = _hscale * 2 / 3; 286 updateView(); 287 return true; 288 case Actions.ViewHZoomOut: 289 _hscale = _hscale < 3 ? _hscale + 1 : _hscale * 3 / 2; 290 updateView(); 291 return true; 292 case Actions.ViewHZoom1: 293 _hscale = 1; 294 updateView(); 295 return true; 296 case Actions.ViewHZoomMax: 297 zoomFull(); 298 return true; 299 case Actions.ViewVZoomMax: 300 _vscale = 1; 301 updateView(); 302 return true; 303 case Actions.ViewHZoomSel: 304 int sellen = _selEnd - _selStart; 305 if (sellen > 16) { 306 _hscale = (sellen + _clientRect.width - 1) / _clientRect.width; 307 if (_hscale < 1) 308 _hscale = 1; 309 _scrollPos = _selStart / _hscale; 310 updateView(); 311 } 312 return true; 313 case Actions.ViewVZoom1: 314 _vscale = visibleYRange().amplitude; 315 if (_vscale > 0) 316 _vscale = 1 / _vscale; 317 else 318 _vscale = 1; 319 updateView(); 320 return true; 321 case Actions.ViewVZoomIn: 322 _vscale *= 1.3; 323 updateView(); 324 return true; 325 case Actions.ViewVZoomOut: 326 _vscale /= 1.3; 327 updateView(); 328 return true; 329 case Actions.InstrumentEditorPlayPause: 330 // play/pause 331 Log.d("play/pause"); 332 if (_player) { 333 if (!_playTimer) 334 _playTimer = setTimer(50); 335 if (_player.paused) { 336 if (_cursorPos >= _file.frames - 100) 337 _cursorPos = 0; 338 setPlayPosition(); 339 _player.paused = false; 340 } else { 341 _player.paused = true; 342 getPlayPosition(); 343 _player.removeLoop(); 344 } 345 } 346 return true; 347 case Actions.InstrumentEditorPlayPauseSelection: 348 // play/pause selection 349 Log.d("play/pause selection"); 350 if (_player) { 351 if (!_playTimer) 352 _playTimer = setTimer(50); 353 if (_player.paused) { 354 if (_selEnd > _selStart || _cursorPos >= _selEnd) 355 _cursorPos = _selStart; 356 setPlayPosition(); 357 _player.setLoop(_file.frameToTime(_selStart), _file.frameToTime(_selEnd)); 358 _player.paused = false; 359 } else { 360 _player.paused = true; 361 getPlayPosition(); 362 _player.removeLoop(); 363 } 364 } 365 return true; 366 case Actions.InstrumentOpenSoundFile: 367 openSampleFile(); 368 return true; 369 default: 370 break; 371 } 372 return super.handleAction(a); 373 } 374 375 WaveFile getSelectionUpsampled() { 376 int sellen = _selEnd - _selStart; 377 if (sellen > 16) { 378 return _file.upsample4x(_selStart, _selEnd); 379 } 380 return null; 381 } 382 383 WaveFile getSelectionWave() { 384 int sellen = _selEnd - _selStart; 385 if (sellen > 16) { 386 return _file.getRange(_selStart, _selEnd, true); 387 } 388 return null; 389 } 390 391 void ensureCursorVisible() { 392 if (!_file) 393 return; 394 int p = _cursorPos / _hscale; 395 if (p < _scrollPos) { 396 _scrollPos = p; 397 updateView(); 398 } else if (p > _scrollPos + _clientRect.width * 9 / 10) { 399 _scrollPos = p - _clientRect.width / 10; 400 updateView(); 401 } 402 } 403 404 /// player position to screen 405 void getPlayPosition() { 406 if (!_file) 407 return; 408 PlayPosition p = _player.position; 409 int frame = _file.timeToFrame(p.currentPosition); 410 _cursorPos = frame; 411 ensureCursorVisible(); 412 invalidate(); 413 window.update(); 414 } 415 416 /// set cursor position to player position 417 void setPlayPosition() { 418 if (!_file) 419 return; 420 _player.position = _file.frameToTime(_cursorPos); 421 } 422 423 override bool onTimer(ulong id) { 424 if (id == _playTimer) { 425 if (!_player.paused) 426 getPlayPosition(); 427 return true; 428 } else { 429 return super.onTimer(id); 430 } 431 } 432 433 void limitPosition(ref int position) { 434 int maxx = _file ? _file.frames - 1 : 0; 435 if (position > maxx) 436 position = maxx; 437 if (position < 0) 438 position = 0; 439 } 440 void updateScrollBar() { 441 if (!_clientRect.width) 442 return; 443 limitPosition(_cursorPos); 444 limitPosition(_selStart); 445 limitPosition(_selEnd); 446 if (_hscale < 1) 447 _hscale = 1; 448 if (_vscale > 5000.0f) 449 _vscale = 5000.0f; 450 int maxScale = _file ? (_file.frames / (_clientRect.width ? _clientRect.width : 1)) : 1; 451 if (_hscale > maxScale) 452 _hscale = maxScale; 453 if (_hscale < 1) 454 _hscale = 1; 455 float amp = visibleYRange.amplitude; 456 if (amp < 0.0001) 457 amp = 0.0001f; 458 float minvscale = 1 / amp * 0.1; 459 float maxvscale = 1 / amp * 10; 460 if (minvscale > 1) 461 minvscale = 1; 462 if (_vscale < minvscale) 463 _vscale = minvscale; 464 if (_vscale > maxvscale) 465 _vscale = maxvscale; 466 if (!_file) { 467 _hScroll.maxValue = 0; 468 _hScroll.pageSize = 1; 469 _hScroll.position = 0; 470 _hruler.setPosition(0, 0, 0); 471 } else { 472 int w = _clientRect.width; 473 int fullw = _file.frames / _hscale; 474 int visiblew = w; 475 if (visiblew > fullw) 476 visiblew = fullw; 477 if (_scrollPos + visiblew > fullw) 478 _scrollPos = fullw - visiblew; 479 if (_scrollPos < 0) 480 _scrollPos = 0; 481 if (_hScroll.pageSize != visiblew) { 482 _hScroll.pageSize = visiblew; 483 _hScroll.requestLayout(); 484 } 485 if (_hScroll.maxValue != fullw) { 486 _hScroll.maxValue = fullw; //fullw - visiblew; 487 _hScroll.requestLayout(); 488 } 489 _hScroll.position = _scrollPos; 490 _hruler.setPosition(_file.frameToTime(_scrollPos * _hscale), _file.frameToTime(fullw * _hscale), _file.frameToTime(visiblew * _hscale)); 491 } 492 } 493 494 /// handle scroll event 495 bool onScrollEvent(AbstractSlider source, ScrollEvent event) { 496 _scrollPos = event.position; 497 updateView(); 498 return true; 499 } 500 501 /** 502 Measure widget according to desired width and height constraints. (Step 1 of two phase layout). 503 */ 504 override void measure(int parentWidth, int parentHeight) { 505 _hruler.measure(parentWidth, parentHeight); 506 int hRulerHeight = _hruler.measuredHeight; 507 int extraHeight = getExtraViewsHeight(parentHeight); 508 _hScroll.measure(parentWidth, parentHeight); 509 int hScrollHeight = _hScroll.measuredHeight; 510 measuredContent(parentWidth, parentHeight, 0, 200 + hRulerHeight + hScrollHeight + extraHeight); 511 } 512 513 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 514 override void layout(Rect rc) { 515 if (visibility == Visibility.Gone) { 516 return; 517 } 518 _pos = rc; 519 _needLayout = false; 520 Rect m = margins; 521 Rect p = padding; 522 rc.left += margins.left + padding.left; 523 rc.right -= margins.right + padding.right; 524 rc.top += margins.top + padding.top; 525 rc.bottom -= margins.bottom + padding.bottom; 526 int hScrollHeight = _hScroll.measuredHeight; 527 Rect hscrollRc = rc; 528 hscrollRc.top = hscrollRc.bottom - hScrollHeight; 529 _hScroll.layout(hscrollRc); 530 int hRulerHeight = _hruler.measuredHeight; 531 Rect hrulerRc = rc; 532 hrulerRc.bottom = hscrollRc.top; 533 hrulerRc.top = hrulerRc.bottom - hRulerHeight; 534 _hruler.layout(hrulerRc); 535 int extraHeight = getExtraViewsHeight(rc.height); 536 if (extraHeight) { 537 Rect extraRc = rc; 538 extraRc.bottom = hrulerRc.top; 539 extraRc.top = extraRc.bottom - extraHeight; 540 layoutExtraViews(extraRc); 541 } 542 _clientRect = rc; 543 _clientRect.bottom = hrulerRc.top - 1 - extraHeight; 544 _clientRect.top += 1; 545 updateView(); 546 } 547 548 549 MinMax getDisplayValuesNoCache(int offset) { 550 MinMax res; 551 if (!_file) 552 return res; 553 int p0 = cast(int)((offset) * _hscale); 554 int p1 = cast(int)((offset + 1) * _hscale); 555 float[] data = getDisplayData(); 556 for (int i = p0; i < p1; i++) { 557 if (i >= 0 && i < _file.frames) { 558 float v = data.ptr[i * _file.channels]; 559 if (i == p0) { 560 res.minvalue = res.maxvalue = v; 561 } else { 562 if (res.minvalue > v) 563 res.minvalue = v; 564 if (res.maxvalue < v) 565 res.maxvalue = v; 566 } 567 } 568 } 569 return res; 570 } 571 572 MinMax getDisplayValues(int offset) { 573 offset += _scrollPos; 574 if (_hscale > 16) { 575 MinMax res; 576 updateZoomCache(); 577 if (offset < 0 || offset >= _zoomCache.length) 578 return res; 579 return _zoomCache.ptr[offset]; 580 } 581 return getDisplayValuesNoCache(offset); 582 } 583 584 bool getDisplayPos(int offset, ref int y0, ref int y1) { 585 MinMax v = getDisplayValues(offset); 586 int my = _clientRect.middley; 587 int dy = _clientRect.height / 2; 588 y0 = my - cast(int)((v.maxvalue * _vscale) * dy); 589 y1 = my + 1 - cast(int)((v.minvalue * _vscale) * dy); 590 if (y0 >= _clientRect.bottom || y1 <= _clientRect.top) 591 return false; 592 return true; 593 } 594 595 void setCursorPos(int x) { 596 limitPosition(x); 597 if (_cursorPos != x) { 598 _cursorPos = x; 599 invalidate(); 600 if (!_player.paused) 601 setPlayPosition(); 602 } 603 } 604 605 void updateSelection(int x) { 606 limitPosition(x); 607 if (_cursorPos < x) { 608 _selStart = _cursorPos; 609 _selEnd = x; 610 invalidate(); 611 } else { 612 _selStart = x; 613 _selEnd = _cursorPos; 614 invalidate(); 615 } 616 } 617 618 619 void updateHScale(int newScale, int preserveX) { 620 int maxScale = _file ? (_file.frames / (_clientRect.width ? _clientRect.width : 1)) : 1; 621 if (newScale > maxScale) 622 newScale = maxScale; 623 if (newScale < 1) 624 newScale = 1; 625 int oldxpos = preserveX / _hscale - _scrollPos; 626 _hscale = newScale; 627 _scrollPos = preserveX / _hscale - oldxpos; 628 updateView(); 629 } 630 631 /// process mouse event; return true if event is processed by widget. 632 override bool onMouseEvent(MouseEvent event) { 633 if (event.action == MouseAction.ButtonDown && !focused && canFocus) 634 setFocus(); 635 if (_clientRect.isPointInside(event.x, event.y)) { 636 int x = (_scrollPos + (event.x - _clientRect.left)) * _hscale; 637 if ((event.action == MouseAction.ButtonDown || event.action == MouseAction.Move) && (event.flags & MouseFlag.LButton)) { 638 setCursorPos(x); 639 return true; 640 } 641 if ((event.action == MouseAction.ButtonDown || event.action == MouseAction.Move) && (event.flags & MouseFlag.RButton)) { 642 updateSelection(x); 643 return true; 644 } 645 if (event.action == MouseAction.Wheel) { 646 if (event.flags & MouseFlag.Control) { 647 // vertical zoom 648 handleAction(event.wheelDelta > 0 ? ACTION_VIEW_VZOOM_IN : ACTION_VIEW_VZOOM_OUT); 649 } else { 650 // horizontal zoom 651 int newScale = _hscale * 2 / 3; 652 if (event.wheelDelta < 0) { 653 newScale = _hscale < 3 ? _hscale + 1 : _hscale * 3 / 2; 654 } 655 updateHScale(newScale, x); 656 } 657 return true; 658 } 659 return false; 660 } 661 return super.onMouseEvent(event); 662 } 663 664 /// Draw widget at its position to buffer 665 override void onDraw(DrawBuf buf) { 666 if (visibility != Visibility.Visible) 667 return; 668 super.onDraw(buf); 669 _needDraw = false; 670 { 671 auto saver = ClipRectSaver(buf, _clientRect, alpha); 672 // erase background 673 buf.fillRect(_clientRect, 0x102010); 674 int my = _clientRect.middley; 675 int dy = _clientRect.height / 2; 676 // draw wave 677 if (_file) { 678 int cursorx = _cursorPos / _hscale - _scrollPos; 679 int selstartx = _selStart / _hscale - _scrollPos; 680 int selendx = _selEnd / _hscale - _scrollPos; 681 for (int i = 0; i < _clientRect.width; i++) { 682 int x = _clientRect.left + i; 683 int y0, y1; 684 if (getDisplayPos(i, y0, y1)) { 685 buf.fillRect(Rect(x, y0, x + 1, y1), 0x4020C020); 686 if (_hscale <= 10) { 687 int exty0 = y0; 688 int exty1 = y1; 689 int prevy0, prevy1; 690 int nexty0, nexty1; 691 if (getDisplayPos(i - 1, prevy0, prevy1)) { 692 if (prevy0 < exty0) 693 exty0 = (prevy0 + y0) / 2; 694 if (prevy1 > exty1) 695 exty1 = (prevy1 + y1) / 2; 696 } 697 if (getDisplayPos(i + 1, nexty0, nexty1)) { 698 if (nexty0 < exty0) 699 exty0 = (nexty0 + y0) / 2; 700 if (nexty1 > exty1) 701 exty1 = (nexty1 + y1) / 2; 702 } 703 if (exty0 < y0) 704 buf.fillRect(Rect(x, exty0, x + 1, y0), 0xE040FF40); 705 if (exty1 > y1) 706 buf.fillRect(Rect(x, y1, x + 1, exty1), 0xE040FF40); 707 } 708 } 709 if (x >= selstartx && x <= selendx) 710 buf.fillRect(Rect(x, _clientRect.top, x + 1, _clientRect.bottom), 0xD00000FF); 711 if (x == cursorx) 712 buf.fillRect(Rect(x, _clientRect.top, x + 1, _clientRect.bottom), 0x40FFFFFF); 713 } 714 if (_file.marks.length) { 715 for (int i = 0; i < _file.marks.length; i++) { 716 int markSample = _file.timeToFrame(_file.marks[i]); 717 int x = (markSample / _hscale) - _scrollPos + _clientRect.left; 718 if (x >= _clientRect.left && x < _clientRect.right) { 719 buf.fillRect(Rect(x, _clientRect.top, x + 1, _clientRect.bottom), 0xA0FF0000); 720 } 721 } 722 } 723 if (_file.negativeMarks.length) { 724 for (int i = 0; i < _file.negativeMarks.length; i++) { 725 int markSample = _file.timeToFrame(_file.negativeMarks[i]); 726 int x = (markSample / _hscale) - _scrollPos + _clientRect.left; 727 if (x >= _clientRect.left && x < _clientRect.right) { 728 buf.fillRect(Rect(x, _clientRect.top, x + 1, _clientRect.bottom), 0xA00000FF); 729 } 730 } 731 } 732 } 733 // draw y==0 734 buf.fillRect(Rect(_clientRect.left, my, _clientRect.right, my + 1), 0x80606030); 735 } 736 drawExtraViews(buf); 737 } 738 739 } 740 741 class SourceWaveFileWidget : WaveFileWidget { 742 this(Mixer mixer) { 743 super(mixer); 744 } 745 } 746 747 class LoopWaveWidget : WaveFileWidget { 748 protected Rect _ampRect; 749 protected Rect _freqRect; 750 protected Rect _fftRect; 751 protected bool _hasAmps; 752 protected bool _hasFreqs; 753 protected bool _hasFft; 754 protected float _minAmp = 0; 755 protected float _maxAmp = 0; 756 protected float _maxFftAmp = 0; 757 protected float _minFreq = 0; 758 protected float _maxFreq = 0; 759 this(Mixer mixer) { 760 super(mixer); 761 } 762 763 /// get data to display - override to show some other data than wave (e.g. to show excitation) 764 override float[] getDisplayData() { 765 if (!_file) 766 return null; 767 if (_file.excitation) 768 return _file.excitation; 769 return _file.data; 770 } 771 772 773 774 @property override void file(WaveFile f) { 775 super.file(f); 776 if (_file && _file.amplitudes.length == _file.frames) { 777 _minAmp = _maxAmp = _file.amplitudes[0]; 778 foreach(v; _file.amplitudes) { 779 if (_minAmp > v) 780 _minAmp = v; 781 if (_maxAmp < v) 782 _maxAmp = v; 783 } 784 _hasAmps = _maxAmp > _minAmp; 785 } 786 if (_file && _file.frequencies.length == _file.frames) { 787 _minFreq = _maxFreq = _file.frequencies[0]; 788 foreach(v; _file.frequencies) { 789 if (_minFreq > v) 790 _minFreq = v; 791 if (_maxFreq < v) 792 _maxFreq = v; 793 } 794 _hasFreqs = _maxFreq > _minFreq; 795 } 796 if (_file && _file.periods.length > 0) { 797 _maxFftAmp = 0; 798 foreach (p; _file.periods) { 799 foreach(amp; p.fftAmp) { 800 if (_maxFftAmp < amp) 801 _maxFftAmp = amp; 802 } 803 } 804 _hasFft = _maxFftAmp > 0; 805 } 806 } 807 808 immutable int LPC_HEIGHT = 12 * LPC_SIZE; 809 810 /// override to allow extra views 811 override int getExtraViewsHeight(int parentHeight) { 812 int h = 0; 813 if (_hasAmps) { 814 h += 32; 815 } 816 if (_hasFreqs) { 817 h += 32; 818 } 819 if (_hasFft) 820 h += LPC_HEIGHT; 821 return h; 822 } 823 /// override to allow extra views 824 override void layoutExtraViews(Rect rc) { 825 _fftRect = rc; 826 if (_hasFft) { 827 _fftRect.top = rc.bottom - LPC_HEIGHT; 828 rc.bottom = _fftRect.top; 829 } else { 830 _fftRect.bottom = _fftRect.top; 831 } 832 _ampRect = rc; 833 _freqRect = rc; 834 if (_hasAmps && _hasFreqs) { 835 _ampRect.bottom = _freqRect.top = rc.middley; 836 } 837 } 838 839 protected void drawExtraArray(DrawBuf buf, Rect rc, float[] data, float minValue, float maxValue, string title, uint bgColor = 0, uint foreColor = 0x808080) { 840 auto saver = ClipRectSaver(buf, rc, alpha); 841 // erase background 842 buf.fillRect(rc, bgColor); 843 buf.fillRect(Rect(rc.left, rc.top, rc.right, rc.top + 1), 0x404040); 844 for (int i = 0; i < rc.width; i++) { 845 int index = (_scrollPos + i) * _hscale; 846 int x = rc.left + i; 847 if (index < data.length) { 848 float value = data[index]; 849 int y = cast(int)(rc.bottom - rc.height * (value - minValue) / (maxValue - minValue)); 850 buf.fillRect(Rect(x, y, x + 1, rc.bottom), foreColor); 851 } 852 } 853 import std.format; 854 import std.utf; 855 font.drawText(buf, rc.left + 2, rc.top + 2, "%s: min=%f max=%f".format(title, minValue, maxValue).toUTF32, 0xFFFFFF); 856 } 857 858 protected void drawFft(DrawBuf buf, ref PeriodInfo period, Rect rc) { 859 import std.math : PI, sqrt, log2; 860 for (int i = 0; i < 128; i++) { 861 float amp = (period.fftAmp[i] / _maxFftAmp); // range is 0..1 862 amp = log2(amp + 1); // range is 0..1, but log scaled 863 amp = log2(amp + 1); // range is 0..1, but log scaled 864 amp = log2(amp + 1); // range is 0..1, but log scaled 865 amp = log2(amp + 1); // range is 0..1, but log scaled 866 amp = log2(amp + 1); // range is 0..1, but log scaled 867 amp = log2(amp + 1); // range is 0..1, but log scaled 868 float phase = (period.fftPhase[i] + PI) / (2 * PI); 869 uint iamp = cast(int)(amp * 256); 870 uint iphase = cast(int)(phase * 256); 871 uint ampcolor = (iamp << 16) | (iamp << 8)| (iamp); 872 uint phcolor = (iphase << 16) | (iphase << 8)| (iphase); 873 buf.fillRect(Rect(rc.left, rc.top + i, rc.right, rc.top + i + 1), ampcolor); 874 buf.fillRect(Rect(rc.left, rc.top + i + 128, rc.right, rc.top + i + 1 + 128), phcolor); 875 } 876 } 877 878 protected void drawLspPart(DrawBuf buf, float[] lsp, Rect rc) { 879 import std.math: PI; 880 for (int i = 0; i < lsp.length; i++) { 881 Rect rect = rc; 882 float value = rc.height * lsp[i] / PI; 883 int y = rc.top + cast(int)value; 884 int delta = cast(int)((value - cast(int)value) * 256); 885 rect.top = y; 886 rect.bottom = y + 1; 887 uint iamp = 255 - delta; 888 uint ampcolor = (iamp << 16) | (iamp << 8)| (iamp); 889 buf.fillRect(rect, ampcolor); 890 rect.top++; 891 rect.bottom++; 892 iamp = delta; 893 ampcolor = (iamp << 16) | (iamp << 8)| (iamp); 894 buf.fillRect(rect, ampcolor); 895 } 896 } 897 898 protected void drawLsp(DrawBuf buf, PeriodInfo[] periods, int periodIndex, Rect rc) { 899 Rect rect = rc; 900 float[LPC_SIZE] lsp; 901 float[LPC_SIZE] prevLsp; 902 float[LPC_SIZE] nextLsp; 903 lsp[0..$] = periods[periodIndex].lsp[0..$]; 904 prevLsp[0..$] = periods[periodIndex > 0 ? periodIndex - 1 : 0].lsp[0..$]; 905 nextLsp[0..$] = periods[periodIndex + 1 < periods.length ? periodIndex + 1 : periodIndex].lsp[0..$]; 906 for (int i = 0; i < LPC_SIZE; i++) { 907 prevLsp[i] = (prevLsp[i] + 2*lsp[i]) / 3; 908 nextLsp[i] = (nextLsp[i] + 2*lsp[i]) / 3; 909 } 910 drawLspPart(buf, prevLsp, Rect(rc.left, rc.top, rc.left + rc.width / 3, rc.bottom)); 911 drawLspPart(buf, lsp, Rect(rc.left + rc.width/3, rc.top, rc.left + rc.width * 2 / 3, rc.bottom)); 912 drawLspPart(buf, nextLsp, Rect(rc.left + rc.width*2/3, rc.top, rc.right, rc.bottom)); 913 } 914 915 /// override to allow extra views 916 override void drawExtraViews(DrawBuf buf) { 917 if (_hasAmps) { 918 drawExtraArray(buf, _ampRect, _file.amplitudes, _minAmp, _maxAmp, "Amplitude", 0x202000, 0x604000); 919 } 920 if (_hasFreqs) { 921 drawExtraArray(buf, _freqRect, _file.frequencies, _minFreq, _maxFreq, "Frequency", 0x002000, 0x0000C0); 922 } 923 if (_hasFft) { 924 auto saver = ClipRectSaver(buf, _fftRect, alpha); 925 buf.fillRect(_fftRect, 0x100000); 926 for(int index = 0; index < _file.periods.length; index++) { 927 int startFrame = _file.timeToFrame(_file.periods[index].startTime); 928 int endFrame = _file.timeToFrame(_file.periods[index].endTime); 929 int startx = startFrame / _hscale - _scrollPos + _fftRect.left; 930 int endx = endFrame / _hscale - _scrollPos + _fftRect.left; 931 if (startx < _fftRect.right && endx > _fftRect.left) { 932 // frame is visible 933 drawLsp(buf, _file.periods, index, Rect(startx, _fftRect.top, endx, _fftRect.bottom)); 934 //drawFft(buf, period, Rect(startx, _fftRect.top, endx, _fftRect.bottom)); 935 } 936 } 937 font.drawText(buf, _fftRect.left + 2, _fftRect.top + 2, "LSP", 0x20FFFF80); 938 } 939 } 940 941 } 942 943 class InstrEditorBody : VerticalLayout { 944 SourceWaveFileWidget _wave; 945 LoopWaveWidget _loop; 946 protected Mixer _mixer; 947 this(Mixer mixer) { 948 super("instrEditBody"); 949 _mixer = mixer; 950 layoutWidth = FILL_PARENT; 951 layoutHeight = FILL_PARENT; 952 backgroundColor(0x000000); 953 _wave = new SourceWaveFileWidget(_mixer); 954 addChild(_wave); 955 _loop = new LoopWaveWidget(_mixer); 956 addChild(_loop); 957 addChild(new VSpacer()); 958 } 959 960 ~this() { 961 } 962 963 /// override to handle specific actions 964 override bool handleAction(const Action a) { 965 switch(a.id) { 966 case Actions.InstrumentCreateLoop: 967 WaveFile tmp = _wave.getSelectionUpsampled(); 968 WaveFile selWave = _wave.getSelectionWave(); 969 if (tmp) { 970 float baseFrequency = tmp.calcBaseFrequency(); 971 int lowpassFilterSize = tmp.timeToFrame((1/baseFrequency) / 16) | 1; 972 int highpassFilterSize = tmp.timeToFrame((1/baseFrequency) * 1.5) | 1; 973 float[] lowpassFirFilter = blackmanWindow(lowpassFilterSize); 974 float[] highpassFirFilter = blackmanWindow(highpassFilterSize); //makeLowpassBlackmanFirFilter(highpassFilterSize); 975 WaveFile lowpass = tmp.firFilter(lowpassFirFilter); 976 WaveFile highpass = lowpass.firFilterInverse(highpassFirFilter); 977 int lowpassSign = lowpass.getMaxAmplitudeSign(); 978 //float[] zeroPhasePositionsLowpass = lowpass.findZeroPhasePositions(lowpassSign); 979 int highpassSign = highpass.getMaxAmplitudeSign(); 980 float[] zeroCrossHighpass = highpass.findZeroCrossingPositions(highpassSign); 981 982 //float[] zeroPhasePositionsHighpassPositive = highpass.findZeroPhasePositions(1); 983 //float[] zeroPhasePositionsHighpassNegative = highpass.findZeroPhasePositions(-1); 984 //smoothTimeMarksShifted(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 985 //smoothTimeMarksShifted(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 986 //smoothTimeMarksShifted(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 987 //smoothTimeMarks(zeroPhasePositionsHighpassPositive); 988 //smoothTimeMarks(zeroPhasePositionsHighpassPositive); 989 //smoothTimeMarks(zeroPhasePositionsHighpassNegative); 990 //smoothTimeMarks(zeroPhasePositionsHighpassNegative); 991 992 int normalSign = tmp.getMaxAmplitudeSign(); 993 //float[] zeroPhasePositionsNormal = tmp.findZeroPhasePositions(normalSign); 994 //Log.d("Zero phase positions for lowpass filtered data: ", zeroPhasePositionsLowpass); 995 //Log.d("Zero phase positions for lowpass+highpass filtered data: ", zeroPhasePositionsHighpassPositive); 996 //Log.d("Zero phase positions for non filtered data: ", zeroPhasePositionsNormal); 997 //tmp.setMarks(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 998 //lowpass.setMarks(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 999 //highpass.setMarks(zeroPhasePositionsHighpassPositive, zeroPhasePositionsHighpassNegative); 1000 for (int i = 0; i < 10; i++) 1001 smoothTimeMarks(zeroCrossHighpass); 1002 highpass.setMarks(zeroCrossHighpass); 1003 highpass.fillPeriodsFromMarks(); 1004 highpass.fillAmplitudesFromPeriods(); 1005 highpass.normalizeAmplitude(); 1006 //highpass.correctMarksForNormalizedAmplitude(); 1007 //highpass.smoothMarks(); 1008 //highpass.smoothMarks(); 1009 highpass.generateFrequenciesFromMarks(); 1010 1011 tmp.marks = highpass.marks; 1012 tmp.negativeMarks = highpass.negativeMarks; 1013 tmp.frequencies = highpass.frequencies; 1014 tmp.amplitudes = highpass.amplitudes; 1015 tmp.normalizeAmplitude; 1016 tmp.fillPeriodsFromMarks(); 1017 for (int i = 0; i < 20; i++) 1018 tmp.smoothLSP(); 1019 1020 1021 selWave.marks = highpass.marks; 1022 selWave.negativeMarks = highpass.negativeMarks; 1023 selWave.fillPeriodsFromMarks(); 1024 selWave.fillAmplitudesFromPeriods(); 1025 selWave.normalizeAmplitude(); 1026 //highpass.correctMarksForNormalizedAmplitude(); 1027 //highpass.smoothMarks(); 1028 //highpass.smoothMarks(); 1029 selWave.generateFrequenciesFromMarks(); 1030 selWave.normalizeAmplitude; 1031 selWave.fillPeriodsFromMarks(); 1032 for (int i = 0; i < 5; i++) 1033 selWave.smoothLSP(); 1034 1035 //if (zeroPhasePositionsNormal.length > 1) { 1036 // tmp.removeDcOffset(zeroPhasePositionsHighpass[0], zeroPhasePositionsHighpass[$-1]); 1037 // tmp.generateFrequenciesFromMarks(); 1038 //} 1039 //_loop.file = lowpass; 1040 //_loop.file = highpass; 1041 tmp.excitation = null; 1042 //_loop.file = tmp; 1043 _loop.file = selWave; 1044 } 1045 return true; 1046 default: 1047 break; 1048 } 1049 return super.handleAction(a); 1050 } 1051 1052 /// map key to action 1053 override Action findKeyAction(uint keyCode, uint flags) { 1054 Action action = _wave.findKeyAction(keyCode, flags); 1055 if (action) 1056 return action; 1057 return super.findKeyAction(keyCode, flags); 1058 } 1059 } 1060 1061 class InstrumentEditorDialog : Dialog { 1062 protected ToolBarHost _toolbarHost; 1063 protected InstrEditorBody _body; 1064 protected Mixer _mixer; 1065 1066 this(Window parentWindow, Mixer mixer, int initialWidth = 0, int initialHeight = 0) { 1067 _mixer = mixer; 1068 super(UIString("Instrument Editor"d), parentWindow, DialogFlag.Modal | DialogFlag.Resizable, initialWidth, initialHeight); 1069 } 1070 1071 ToolBarHost createToolbars() { 1072 ToolBarHost tbhost = new ToolBarHost(); 1073 ToolBar tb = tbhost.getOrAddToolbar("toolbar1"); 1074 tb.addButtons(ACTION_INSTRUMENT_OPEN_SOUND_FILE, 1075 ACTION_SEPARATOR, 1076 ACTION_INSTRUMENT_PLAY_PAUSE, ACTION_INSTRUMENT_PLAY_PAUSE_SELECTION, 1077 ACTION_SEPARATOR, 1078 ACTION_INSTRUMENT_CREATE_LOOP, 1079 ACTION_SEPARATOR, 1080 ACTION_VIEW_HZOOM_1, ACTION_VIEW_HZOOM_IN, ACTION_VIEW_HZOOM_OUT, ACTION_VIEW_HZOOM_MAX, ACTION_VIEW_HZOOM_SEL, 1081 ACTION_SEPARATOR, 1082 ACTION_VIEW_VZOOM_1, ACTION_VIEW_VZOOM_IN, ACTION_VIEW_VZOOM_OUT, ACTION_VIEW_VZOOM_MAX 1083 ); 1084 acceleratorMap.add([ACTION_INSTRUMENT_OPEN_SOUND_FILE, 1085 ACTION_INSTRUMENT_PLAY_PAUSE, ACTION_INSTRUMENT_PLAY_PAUSE_SELECTION, ACTION_INSTRUMENT_CREATE_LOOP, 1086 ACTION_VIEW_HZOOM_1, ACTION_VIEW_HZOOM_IN, ACTION_VIEW_HZOOM_OUT, ACTION_VIEW_HZOOM_MAX, ACTION_VIEW_HZOOM_SEL, 1087 ACTION_VIEW_VZOOM_1, ACTION_VIEW_VZOOM_IN, ACTION_VIEW_VZOOM_OUT, ACTION_VIEW_VZOOM_MAX]); 1088 1089 return tbhost; 1090 } 1091 1092 InstrEditorBody createBody() { 1093 InstrEditorBody res = new InstrEditorBody(_mixer); 1094 return res; 1095 } 1096 1097 /// map key to action 1098 override Action findKeyAction(uint keyCode, uint flags) { 1099 Action action = _toolbarHost.findKeyAction(keyCode, flags); 1100 if (action) 1101 return action; 1102 action = _body.findKeyAction(keyCode, flags); 1103 if (action) 1104 return action; 1105 return super.findKeyAction(keyCode, flags); 1106 } 1107 /// override to implement creation of dialog controls 1108 override void initialize() { 1109 Log.d("InstrumentEditorDialog.initialize"); 1110 _toolbarHost = createToolbars(); 1111 _body = createBody(); 1112 addChild(_toolbarHost); 1113 addChild(_body); 1114 } 1115 1116 }