/************************************************************************
 *
 * Copyright 2010-2012 Jakob Leben (jakob.leben@gmail.com)
 *
 * This file is part of SuperCollider Qt GUI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 ************************************************************************/

#include "view.hpp"
#include "../../QcWidgetFactory.h"

#include <QGridLayout>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QPaintEvent>
#include <QCursor>

#include <climits>
#include <cmath>
#include <cstring>

QC_DECLARE_QWIDGET_FACTORY(QcWaveform);

const int kMaxRawFrames = 300000;
const int kMaxFramesPerCacheUnit = 128;

QcWaveform::QcWaveform(QWidget* parent):
    QWidget(parent),
    sf(0),

    _rangeBeg(0),
    _rangeDur(0),
    _rangeEnd(0),

    _cache(0),

    _curSel(0),

    _showCursor(false),
    _cursorPos(0),
    _cursorEditable(true),

    _showGrid(true),
    _gridResolution(1.0),
    _gridOffset(0.0),

    _beg(0.0),
    _dur(0.0),
    _yZoom(1.f),
    _yOffset(0.f),
    _spacing(0.1f),

    pixmap(0),
    _bkgColor(QColor(0, 0, 0)),
    _peakColor(QColor(242, 178, 0)),
    _rmsColor(QColor(255, 255, 0)),
    _cursorColor(QColor(255, 0, 0)),
    _gridColor(QColor(100, 100, 200)),
    dirty(false),
    _drawWaveform(true),
    _drawRMS(true),
    // _antialiasing(false), //TODO
    _drawsCenterLine(true),
    _drawsBoundingLines(true) {
    memset(&sfInfo, 0, sizeof(SF_INFO));

    setFocusPolicy(Qt::StrongFocus);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    setAttribute(Qt::WA_OpaquePaintEvent, true);
}

QcWaveform::~QcWaveform() {
    delete _cache;
    delete pixmap;
    if (sf)
        sf_close(sf);
}

void QcWaveform::load(const QString& filename) {
    qcDebugMsg(1, "QcWaveform::load( filename )");
    load(filename, 0, 0, true);
}

void QcWaveform::load(const QString& filename, int beg, int dur) {
    qcDebugMsg(1, "QcWaveform::load( filename, beg, dur )");
    load(filename, beg, dur, false);
}

static SNDFILE* sndfileOpenQString(const QString& filename, int mode, SF_INFO* info) {
#ifdef _WIN32
    auto name = filename.toStdWString();
#else
    auto name = filename.toStdString();
#endif // _WIN32

    return sndfileOpen(name.c_str(), mode, info);
}

// Negative dur is considered a failure, otherwise we could use dur == -1 to mean "load the whole file."
void QcWaveform::load(const QString& filename, int beg, int dur, bool allFrames) {
    SF_INFO new_info;
    memset(&new_info, 0, sizeof(SF_INFO));

    SNDFILE* new_sf = sndfileOpenQString(filename, SFM_READ, &new_info);

    if (!new_sf) {
        qcErrorMsg(QStringLiteral("Could not open soundfile: ") + filename);
        return;
    }

    dur = allFrames ? new_info.frames : dur;
    doLoad(new_sf, new_info, beg, dur);
}


void QcWaveform::load(const QVector<double>& data, int offset, int ch, int sr) {
    qcDebugMsg(1, "QcWaveform::load( data, offset, channels )");

    if (ch < 1) {
        qcWarningMsg("QSoundFileView: invalid number of channels!");
        return;
    }

    int ns = data.count();
    int nf = ns / ch;

    if (nf * ch != ns) {
        qcWarningMsg("QSoundFileView: size of data not a multiple of channel count!");
        return;
    }

    if (offset < 0 || nf - offset < 1) {
        qcWarningMsg("QSoundFileView: invalid range of data!");
        return;
    }

    delete _cache;
    if (sf)
        sf_close(sf);
    sf = 0;

    SF_INFO new_info;
    memset(&new_info, 0, sizeof(SF_INFO));
    new_info.channels = ch;
    new_info.samplerate = sr;
    sfInfo = new_info;

    _beg = _rangeBeg = 0;
    _dur = _rangeDur = _rangeEnd = nf - offset;

    updateFPP();

    _cache = new SoundCacheStream();
    connect(_cache, SIGNAL(loadingDone()), this, SIGNAL(loadingDone()));
    connect(_cache, SIGNAL(loadingDone()), this, SLOT(redraw()));

    _cache->load(data, _rangeDur, offset, ch);
}

void QcWaveform::allocate(int frames, int ch, int sr) {
    if (ch < 1) {
        qcWarningMsg("QSoundFileView: invalid number of channels!");
        return;
    }

    delete _cache;
    if (sf)
        sf_close(sf);
    sf = 0;

    SF_INFO new_info;
    memset(&new_info, 0, sizeof(SF_INFO));
    new_info.channels = ch;
    new_info.samplerate = sr;
    sfInfo = new_info;

    _beg = _rangeBeg = 0;
    _dur = _rangeDur = _rangeEnd = frames;

    updateFPP();

    _cache = new SoundCacheStream();
    _cache->allocate(frames, ch);

    redraw();
}

void QcWaveform::write(const QVector<double>& data, int offset) {
    if (sf) {
        qcWarningMsg("QSoundFileView: can not write data while displaying a sound file!");
        return;
    }

    if (!_cache || !_cache->ready()) {
        qcWarningMsg("QSoundFileView: can not write data; memory has not been allocated yet!");
        return;
    }

    int ch = sfInfo.channels;
    int ns = data.size();
    int nf = ns / ch;

    if (nf * ch != ns) {
        qcWarningMsg("QSoundFileView: can not write data; size not a multiple of channels!");
        return;
    }

    if (offset < 0 || offset + nf > _rangeEnd) {
        qcWarningMsg("QSoundFileView: can not write data; either offset is wrong or data size is too large.");
        return;
    }

    _cache->write(data, offset, nf);

    redraw();
}

void QcWaveform::doLoad(SNDFILE* new_sf, const SF_INFO& new_info, sf_count_t beg, sf_count_t dur) {
    // check beginning and duration validity

    if (beg < 0 || dur < 1 || beg + dur > new_info.frames) {
        qcErrorMsg("Invalid beginning and/or duration.");
        sf_close(new_sf);
        return;
    }

    // cleanup previous state

    // NOTE we have to delete SoundCacheStream before closing the soundfile, as it might be still
    // loading it
    // TODO: should SoundCacheStream open the soundfile on its own?

    delete _cache;
    if (sf)
        sf_close(sf);

    sf = new_sf;
    sfInfo = new_info;
    _beg = _rangeBeg = beg;
    _dur = _rangeDur = dur;
    _rangeEnd = _rangeBeg + _rangeDur;

    updateFPP();

    _cache = new SoundCacheStream();
    connect(_cache, SIGNAL(loadProgress(int)), this, SIGNAL(loadProgress(int)));
    connect(_cache, SIGNAL(loadProgress(int)), this, SLOT(update()));
    connect(_cache, SIGNAL(loadingDone()), this, SIGNAL(loadingDone()));
    connect(_cache, SIGNAL(loadingDone()), this, SLOT(redraw()));

    _cache->load(sf, sfInfo, beg, dur, kMaxFramesPerCacheUnit, kMaxRawFrames);

    redraw();
}

float QcWaveform::loadProgress() { return _cache ? _cache->loadProgress() : 1.f; }

float QcWaveform::zoom() {
    // NOTE We have limited _rangeDur to 1 minimum.
    return _dur / _rangeDur;
}

float QcWaveform::xZoom() { return (sfInfo.samplerate ? _dur / sfInfo.samplerate : 0); }

float QcWaveform::yZoom() { return _yZoom; }

float QcWaveform::yOffset() { return _yOffset; }

float QcWaveform::spacing() { return _spacing; }

QVariantList QcWaveform::selections() const {
    QVariantList slist;
    for (int i = 0; i < 64; ++i) {
        slist << QVariant(selection(i));
    }
    return slist;
}

void QcWaveform::setCurrentSelection(int i) {
    if (i < 0 || i > 63)
        return;
    _curSel = i;
    update();
}

QVariantList QcWaveform::selection(int i) const {
    QVariantList list;
    if (i < 0 || i > 63)
        return list;
    const Selection& s = _selections[i];
    list << QVariant(static_cast<int>(s.start - _rangeBeg));
    list << QVariant(static_cast<int>(s.size));
    return list;
}

void QcWaveform::setSelection(int i, sf_count_t a, sf_count_t b) {
    if (i < 0 || i > 63)
        return;
    Selection& s = _selections[i];
    s.start = qMin(a, b);
    s.size = qMax(a, b) - s.start;
    update();
}

void QcWaveform::setSelection(int i, QVariantList list) {
    if (list.count() < 2)
        return;
    sf_count_t start = list[0].toInt() + _rangeBeg;
    sf_count_t end = start + list[1].toInt();
    setSelection(i, start, end);
}

void QcWaveform::setSelectionStart(int i, sf_count_t frame) {
    if (i < 0 || i > 63)
        return;
    Selection& s = _selections[i];
    sf_count_t frame2 = s.start + s.size;
    s.start = qMin(frame, frame2);
    s.size = qMax(frame, frame2) - s.start;
    update();
}

void QcWaveform::setSelectionEnd(int i, sf_count_t frame) {
    if (i < 0 || i > 63)
        return;
    Selection& s = _selections[i];
    sf_count_t frame2 = s.start;
    s.start = qMin(frame, frame2);
    s.size = qMax(frame, frame2) - s.start;
    update();
}

void QcWaveform::setSelectionEditable(int i, bool editable) {
    if (i < 0 || i > 63)
        return;
    _selections[i].editable = editable;
    update();
}

void QcWaveform::setSelectionColor(int i, const QColor& c) {
    if (i < 0 || i > 63)
        return;
    _selections[i].color = c;
    update();
}

QVariantList QcWaveform::waveColors() const {
    QVariantList clist;
    Q_FOREACH (QColor clr, _waveColors)
        clist << QVariant(clr);
    return clist;
}

void QcWaveform::setWaveColors(const QVariantList& list) {
    _waveColors.clear();
    Q_FOREACH (const QVariant& var, list)
        _waveColors << var.value<QColor>();
    redraw();
}

void QcWaveform::zoomTo(double z) {
    z = qMax(0.0, qMin(1.0, z));

    _dur = qMax(_rangeDur * z, 1.0);

    // printf("dur: %Li view: %Li\n", sfInfo.frames, _dur);
    if (_beg + _dur > _rangeEnd)
        _beg = _rangeEnd - _dur;

    updateFPP();
    redraw();
}

void QcWaveform::zoomBy(double factor) { zoomTo(zoom() * factor); }

void QcWaveform::zoomAllOut() {
    _beg = _rangeBeg;
    _dur = _rangeDur;
    updateFPP();
    redraw();
}

void QcWaveform::scrollTo(double startFrame) {
    _beg = qBound((double)_rangeBeg, startFrame, _rangeEnd - _dur);
    redraw();
}

void QcWaveform::scrollBy(double f) { scrollTo(_beg + f); }

float QcWaveform::scrollPos() {
    double scrollRange = _rangeDur - _dur;
    return scrollRange > 0.0 ? (_beg - _rangeBeg) / scrollRange : 0.f;
}
void QcWaveform::setScrollPos(double fraction) { scrollTo(fraction * (_rangeDur - _dur) + _rangeBeg); }

void QcWaveform::scrollToStart() { scrollTo(_rangeBeg); }

void QcWaveform::scrollToEnd() { scrollTo(_rangeEnd - _dur); }

void QcWaveform::setXZoom(double seconds) {
    // NOTE We have limited _rangeDur to 1 minimum.
    double frac = seconds * sfInfo.samplerate / _rangeDur;
    zoomTo(frac);
}

void QcWaveform::setYZoom(double factor) {
    _yZoom = factor;
    redraw();
}

void QcWaveform::setYOffset(double offset) {
    _yOffset = offset;
    redraw();
}

void QcWaveform::setSpacing(double factor) {
    _spacing = qBound(0.0, factor, 1.0);
    redraw();
}

void QcWaveform::zoomSelection(int i) {
    if (i < 0 || i > 63)
        return;

    Selection& s = _selections[i];

    if (s.start >= _rangeEnd || s.size < 1 || s.start + s.size <= _rangeBeg)
        return;

    _beg = qMax(s.start, _rangeBeg);
    double end = qMin(s.start + s.size, _rangeEnd);
    _dur = end - _beg;

    // clear the selection
    s.size = 0;

    updateFPP();
    redraw();
}

void QcWaveform::resizeEvent(QResizeEvent*) {
    delete pixmap;
    pixmap = new QPixmap(size());
    updateFPP();
    redraw();
}

void QcWaveform::paintEvent(QPaintEvent* ev) {
    Q_ASSERT(pixmap != 0);

    QPainter p(this);

    // if loading draw progress
    if (_cache && _cache->loading()) {
        QRect r(rect());
        p.fillRect(r, QColor(100, 100, 100));
        r.setRight(r.right() * _cache->loadProgress() / 100);
        p.fillRect(r, QColor(0, 0, 0));
        p.setPen(QColor(255, 255, 255));
        QTextOption opt;
        opt.setAlignment(Qt::AlignCenter);
        p.drawText(rect(), "loading...", opt);
        return;
    }

    p.fillRect(rect(), _bkgColor);

    // draw waveform on pixmap

    if (_drawWaveform && dirty) {
        draw(pixmap, 0, width(), _beg, _dur);
        dirty = false;
    }

    // draw time grid

    if (_showGrid && sfInfo.samplerate > 0 && _gridResolution > 0.f) {
        p.save();

        // Since _gridResolution and _gridOffset are in seconds, scale using
        // duration in seconds.
        double dur_in_secs = _dur / sfInfo.samplerate;

        p.setPen(Qt::NoPen);
        p.setBrush(_gridColor);
        p.scale((double)width() / dur_in_secs, 1.0);

        // Wrap the offset to (-100%, 100%) of the grid resolution. Subtract by
        // beg_in_secs to account for zoom+timeshift.
        double beg_in_secs = _beg / sfInfo.samplerate;
        double offset = std::fmod(_gridOffset - beg_in_secs, _gridResolution * 2);

        // Catch the case where the grid is [50% - 100%) offset from the start
        // in order to draw the portion off the left side of the view.
        if (offset > _gridResolution)
            offset -= _gridResolution * 2;

        while (offset < dur_in_secs) {
            QRectF r(offset, 0, _gridResolution, height());
            p.drawRect(r);
            offset += _gridResolution * 2;
        }

        p.restore();
    }

    // draw selections

    p.save();

    p.scale((double)width() / _dur, 1.0);
    p.translate(_beg * -1.0, 0.0);
    p.setPen(Qt::NoPen);

    int i;
    for (i = 0; i < 64; ++i) {
        const Selection& s = _selections[i];
        if (s.size > 0) {
            QRectF r(s.start, 0, s.size, height());
            p.setBrush(s.color);
            p.drawRect(r);
        }
    }

    p.restore();

    // paste the waveform pixmap on screen

    if (_drawWaveform)
        p.drawPixmap(ev->rect(), *pixmap, ev->rect());

    // draw cursor

    if (_showCursor && _cursorPos >= _beg && _cursorPos < _beg + _dur) {
        double cursorX = (_cursorPos - _beg) / _fpp;
        p.setPen(_cursorColor);
        p.drawLine(QLineF(cursorX, 0, cursorX, height()));
    }
}

void QcWaveform::keyPressEvent(QKeyEvent* ev) {
    if (ev->key() == Qt::Key_Shift && _dragAction == Navigate) {
        _dragPoint = mapFromGlobal(QCursor::pos());
        _dragData2 = zoom();
    } else
        ev->ignore();
}

void QcWaveform::mousePressEvent(QMouseEvent* ev) {
    _dragAction = NoDragAction;
    _dragPoint = ev->pos();
    _dragFrame = ev->pos().x() * _fpp + _beg;

    Qt::KeyboardModifiers mods = ev->modifiers();
    Qt::MouseButton btn = ev->button();
#ifdef Q_OS_MAC
    Qt::KeyboardModifier CTRL = Qt::MetaModifier;
#else
    Qt::KeyboardModifier CTRL = Qt::ControlModifier;
#endif

    if (btn == Qt::LeftButton) {
        if ((mods & Qt::ShiftModifier) && (mods & CTRL)) {
            _dragFrame = _selections[_curSel].start;
            _dragAction = MoveSelection;
        } else if (mods & Qt::ShiftModifier) {
            _dragAction = Select;
            const Selection& s = _selections[_curSel];
            if (_dragFrame < s.start + (s.size * 0.5)) {
                setSelectionStart(_curSel, _dragFrame);
                _dragFrame = s.start + s.size;
            } else {
                setSelectionEnd(_curSel, _dragFrame);
                _dragFrame = s.start;
            }
            Q_EMIT(action());
        } else {
            if (!(mods & CTRL)) {
                _dragAction = Select;
                _selections[_curSel].start = _dragFrame;
                _selections[_curSel].size = 0;
                update();
                Q_EMIT(action());
            }
            if (_cursorEditable) {
                _cursorPos = _dragFrame;
                if (mods & CTRL)
                    _dragAction = MoveCursor;
                update();
                Q_EMIT(metaAction());
            }
        }

    } else if (btn == Qt::RightButton) {
        _dragAction = Navigate;
        _dragData = ev->pos().x() * _fpp + _beg;
        _dragData2 = zoom();
    }
}

void QcWaveform::mouseDoubleClickEvent(QMouseEvent*) {
    setSelection(_curSel, _rangeBeg, _rangeEnd);
    Q_EMIT(action());
}

void QcWaveform::mouseMoveEvent(QMouseEvent* ev) {
    if (!ev->buttons())
        return;

    if (_dragAction == Navigate) {
        Qt::KeyboardModifiers mods = ev->modifiers();
        if (mods & Qt::ShiftModifier) {
            double factor = pow(2, (ev->pos().y() - _dragPoint.y()) * 0.008);
            // zoom to the initial zoom times the factor based on distance from initial position
            zoomTo(_dragData2 * factor);
        }
        // scroll to the clicked frame minus the current mouse position in frames
        // _fpp has been adjusted by zooming
        scrollTo(_dragData - (ev->pos().x() * _fpp));
    } else if (_dragAction == Select) {
        sf_count_t frame = qBound(0, ev->pos().x(), width()) * _fpp + _beg;
        setSelection(_curSel, _dragFrame, frame);
        update();
        Q_EMIT(action());
    } else if (_dragAction == MoveSelection) {
        double dpos = ev->pos().x() - _dragPoint.x();
        Selection& s = _selections[_curSel];
        s.start = _dragFrame + (dpos * _fpp);
        s.start = qBound(_rangeBeg, s.start, _rangeEnd - s.size);
        update();
        Q_EMIT(action());
    } else if (_dragAction == MoveCursor) {
        _cursorPos = qBound(0, ev->pos().x(), width()) * _fpp + _beg;
        update();
        Q_EMIT(metaAction());
    }
}

void QcWaveform::rebuildCache(int maxFPU, int maxRawFrames) {}

void QcWaveform::draw(QPixmap* pix, int x, int width, double f_beg, double f_dur) {
    // FIXME anomaly: when _fpp reaching 1.0 rms can go outside min-max!

    pix->fill(QColor(0, 0, 0, 0));

    QPainter p(pix);

    // if( _antialiasing ) p.setRenderHint( QPainter::Antialiasing ); //TODO

    if (!_cache || !_cache->ready())
        return;

    // check for sane situation:
    if (f_beg < _rangeBeg || f_beg + f_dur > _rangeEnd)
        return;

    // data indexes
    sf_count_t i_beg = floor(f_beg); // data beginning;
    sf_count_t i_count = ceil(f_beg + f_dur) - i_beg; // data count;
    bool haveOneMore;
    if (i_beg + i_count < _rangeEnd) {
        ++i_count;
        haveOneMore = true;
    } else {
        haveOneMore = false;
    }

    // data source - choose according to horiz. zoom (data-display resolution)
    bool canUseCache = _fpp < 1.0 ? _cache->fpu() == 1.0 : _fpp >= _cache->fpu();

    SoundStream* soundStream;
    SoundFileStream sfStream;

    if (canUseCache) {
        qcDebugMsg(2, QStringLiteral("using cache"));
        soundStream = _cache;
    } else if (sf) {
        qcDebugMsg(2, QStringLiteral("using file"));
        soundStream = &sfStream;
        sfStream.load(sf, sfInfo, i_beg, i_count);
    } else {
        qcWarningMsg("QSoundFileView: can't paint waveform: view resolution exceeds data cache resolution,"
                     " and soundfile is not given.");
        return;
    }

    // geometry
    float spacing = pix->height() * _spacing / (float)sfInfo.channels;
    float chHeight = pix->height() * (1.0f - _spacing) / (float)sfInfo.channels;
    float yOffsetPx = -chHeight / 2.0f * _yOffset;
    float yScale = -chHeight / 2.0f * _yZoom;

    // initial painter setup
    QPen minMaxPen;
    QPen rmsPen;

    minMaxPen.setWidth(0);
    rmsPen.setWidth(0);

    float halfChH = chHeight * 0.5;
    float halfChHScaled = halfChH * _yZoom;

    p.translate(0.f, halfChH + (spacing / 2.0f) + yOffsetPx);

    int waveColorN = _waveColors.count();
    int ch;
    for (ch = 0; ch < soundStream->channels(); ++ch) {
        if (ch < waveColorN && _waveColors[ch].isValid()) {
            QColor clr(_waveColors[ch]);
            rmsPen.setColor(_rmsColor);
            minMaxPen.setColor(clr);
        } else {
            rmsPen.setColor(_rmsColor);
            minMaxPen.setColor(_peakColor);
        }

        if (_drawsCenterLine) {
            // draw center line
            p.setPen(QColor(90, 90, 90));
            p.drawLine(x, 0, x + width, 0);
        }

        if (_drawsBoundingLines) {
            // draw bounding lines
            p.setPen(QColor(100, 100, 100));

            p.drawLine(x, halfChHScaled, x + width, halfChHScaled);
            p.drawLine(x, -halfChHScaled, x + width, -halfChHScaled);
        }

        p.save();

        p.scale(1.f, yScale);

        if (_fpp > 1.0) {
            // draw min-max regions and RMS
            float* minBuffer = (float*)alloca(width * sizeof(float));
            float* maxBuffer = (float*)alloca(width * sizeof(float));
            ;
            float* minRMS = (float*)alloca(width * sizeof(float));
            ;
            float* maxRMS = (float*)alloca(width * sizeof(float));
            ;

            bool ok = soundStream->displayData(ch, f_beg, f_dur, minBuffer, maxBuffer, minRMS, maxRMS, width);

            Q_ASSERT(ok);

            int rmsAlpha = rmsPen.color().alpha();

            int i;
            for (i = 0; i < width; ++i) {
                float min = minBuffer[i];
                float max = maxBuffer[i];
                int minMaxPxDiff = int((max - min) * -yScale);
                if (minMaxPxDiff != 0) {
                    p.setPen(minMaxPen);
                    p.drawLine(QPointF(x + i, min), QPointF(x + i, max));

                    if (_drawRMS && rmsAlpha > 0 && minMaxPxDiff > 4) {
                        if (_fpp > _rmsMaxFpp) {
                            p.setPen(rmsPen);
                            p.drawLine(QPointF(x + i, minRMS[i]), QPointF(x + i, maxRMS[i]));
                        } else {
                            if (_fpp > _rmsMinFpp) {
                                QColor thisColor = rmsPen.color();
                                thisColor.setAlpha(rmsAlpha * (_fpp - _rmsMinFpp) / (_rmsMaxFpp - _rmsMinFpp));
                                rmsPen.setColor(thisColor);
                                p.setPen(rmsPen);
                                p.drawLine(QPointF(x + i, minRMS[i]), QPointF(x + i, maxRMS[i]));
                            }
                        }
                    }

                } else {
                    p.setPen(minMaxPen);
                    p.drawPoint(QPointF(x + i, min));
                }
            }
        } else {
            // draw lines between actual values

            qreal ppf = 1.0 / _fpp;
            qreal dx = (i_beg - f_beg) * ppf;

            bool interleaved = false;
            float* data = soundStream->rawFrames(ch, i_beg, i_count, &interleaved);

            Q_ASSERT(data != 0);
            int step = interleaved ? soundStream->channels() : 1;

            QPainterPath path;

            if (i_count) {
                QPointF pt(dx, (qreal)*data);
                path.moveTo(pt);
            }
            int f; // frame
            for (f = 1; f < i_count; ++f) {
                data += step;
                QPointF pt(f * ppf + dx, (qreal)*data);
                path.lineTo(pt);
            }
            if (i_count && !haveOneMore) {
                path.lineTo(QPointF(f * ppf + dx, (qreal)*data));
            }

            p.setPen(minMaxPen);
            p.drawPath(path);
        }

        p.restore();

        p.translate(0.f, chHeight + spacing);
    }
}
