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 }