Skip to content
Snippets Groups Projects
tsound.cpp 23 KiB
Newer Older
  • Learn to ignore specific revisions
  • SeeLook's avatar
    SeeLook committed
    /***************************************************************************
    
     *   Copyright (C) 2011-2021 by Tomasz Bojczuk                             *
    
     *   seelook@gmail.com                                                     *
    
    SeeLook's avatar
    SeeLook committed
     *                                                                         *
     *   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 3 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/>.  *
     ***************************************************************************/
    
    SeeLook's avatar
    SeeLook committed
    #include "tsound.h"
    
    #if defined(Q_OS_ANDROID)
    #include "tqtaudioin.h"
    #include "tqtaudioout.h"
    #include <QtAndroidExtras/qandroidfunctions.h>
    #include <QtAndroidExtras/qandroidjnienvironment.h>
    
    #include "tnotesbaritem.h"
    #include "trtaudioin.h"
    #include "trtaudioout.h"
    #include <QtCore/qfileinfo.h>
    
    #include <taudioparams.h>
    #include <tglobals.h>
    #include <tnootkaqml.h>
    
    #include <QtCore/qtimer.h>
    
    #include <QtGui/qevent.h>
    #include <QtGui/qguiapplication.h>
    
    #include <QtQml/qqmlengine.h>
    
    Tsound *Tsound::m_instance = nullptr;
    
    QString Tsound::soundTouchVersion()
    {
        return TabstractPlayer::soundTouchVersion();
    }
    
    SeeLook's avatar
    SeeLook committed
    
    
    Tsound::Tsound(QObject *parent)
        : QObject(parent)
        , player(nullptr)
        , sniffer(nullptr)
        , m_tempo(60)
        , m_quantVal(6)
    
    SeeLook's avatar
    SeeLook committed
    {
    
        if (m_instance) {
            qDebug() << "Tsound instance already exists!";
            return;
        }
    
        m_instance = this;
    
        qRegisterMetaType<Tchunk>("Tchunk");
        qRegisterMetaType<TnoteStruct>("TnoteStruct");
        qmlRegisterType<TtickColors>("Nootka", 1, 0, "TtickColors");
    #if !defined(Q_OS_ANDROID)
        qmlRegisterType<TnotesBarItem>("Nootka", 1, 0, "TnotesBarItem");
    
    #endif
    
        setQuantization(GLOB->audioParams->quantization);
    
    SeeLook's avatar
    SeeLook committed
    }
    
    Tsound::~Tsound()
    
    { // They have not a parent
        deleteSniffer();
        deletePlayer();
        m_instance = nullptr;
    #if !defined(Q_OS_ANDROID)
        if (!m_dumpPath.isEmpty())
            GLOB->audioParams->dumpPath.clear();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    // #################################################################################################
    // ###################                PUBLIC            ############################################
    // #################################################################################################
    
    SeeLook's avatar
    SeeLook committed
    
    
    void Tsound::init()
    {
        QTimer::singleShot(500, this, [=] {
    #if !defined(Q_OS_ANDROID) && (defined(Q_OS_LINUX) || defined(Q_OS_WIN))
            TrtAudio::initJACKorASIO(GLOB->audioParams->JACKorASIO);
    
            if (GLOB->audioParams->OUTenabled)
                createPlayer();
            if (GLOB->audioParams->INenabled)
                createSniffer();
    
            connect(NOO, &TnootkaQML::playNote, this, &Tsound::play);
            setDefaultAmbitus();
            if (sniffer)
                sniffer->startListening();
            emit initialized();
    #if !defined(Q_OS_ANDROID)
            emit GLOB->showNotesDiffChanged(); // trigger this option - MainWindow.qml handles this signal
    
    #endif
    
        });
    #if defined(Q_OS_ANDROID)
        m_currVol = currentVol();
        m_maxVol = maxVolRange();
        qApp->installEventFilter(this);
        m_volKeyTimer.start();
        connect(qApp, &QGuiApplication::applicationStateChanged, this, [=] {
            if (qApp->applicationState() == Qt::ApplicationActive)
                startListen();
            else if (qApp->applicationState() == Qt::ApplicationInactive)
                stop();
            else {
                stop();
                auto qtSniff = qobject_cast<TaudioIN *>(sniffer);
                if (qtSniff)
                    qtSniff->stopDevice();
            }
        });
    
    void Tsound::play(const Tnote &note)
    {
        if (player && note.isValid()) {
            m_stopSniffOnce = true;
            stopMetronome();
            player->playNote(note.chromatic());
        }
    
    #if defined(Q_OS_ANDROID)
        if (sniffer) { // stop sniffer
            if (!m_stopSniffOnce) { // stop listening just once
                sniffer->stopListening();
                m_stopSniffOnce = true;
            }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::playMelody(Tmelody *mel, int transposition)
    {
        if (player && player->isPlayable()) {
            if (player->isPlaying()) {
                stopPlaying();
            } else {
                if (mel->length()) {
                    m_stopSniffOnce = true;
                    player->playMelody(mel, transposition, 0);
                }
    
    void Tsound::playNoteList(QList<Tnote> &notes, int firstNote, int countdownDuration)
    {
        if (player) {
            if (!player->isPlaying()) {
                if (!notes.isEmpty()) {
                    runMetronome(firstNote == 0 && tickBeforePlay() ? Tmeter(static_cast<Tmeter::Emeter>(m_currentMeter)).countTo() : 0);
                    m_stopSniffOnce = true;
                    player->playNotes(std::addressof(notes), Tmeter::quarterTempo(m_tempo, m_beatUnit), firstNote, countdownDuration);
                }
                //         if (player->isPlaying()) {
                //           if (sniffer) { // stop sniffer if midi output was started
                //             if (!m_stopSniffOnce) { // stop listening just once
                // //               sniffer->stopListening();
                //               m_stopSniffOnce = true;
                //             }
                //           }
                // //           emit playingChanged();
                //         }
            } else {
                stopPlaying();
    
    qreal Tsound::inputVol()
    {
        return sniffer ? sniffer->volume() : 0.0;
    
    qreal Tsound::pitchDeviation()
    {
        if (sniffer)
            return static_cast<qreal>(qBound(-0.49, (sniffer->lastChunkPitch() - static_cast<float>(qRound(sniffer->lastChunkPitch()))) * INT_FACTOR, 0.49));
        else
            return 0.0;
    
    void Tsound::acceptSettings()
    {
        bool doParamsUpdated = false;
        // for output
        if (GLOB->audioParams->OUTenabled) {
            if (!player)
                createPlayer();
            else {
    #if !defined(Q_OS_ANDROID)
                if (GLOB->audioParams->midiEnabled) {
                    deletePlayer(); // it is safe to delete midi
                    createPlayer(); // and create it again
                } else
    #endif
                { // avoids deleting TaudioOUT instance and loading ogg file every acceptSettings call
                    if (player->type() == TabstractPlayer::e_midi) {
                        deletePlayer(); // player was midi so delete
                        createPlayer();
                    } else { // just set new params to TaudioOUT
                        doParamsUpdated = true;
                    }
    
                if (player) {
                    if (!player->isPlayable())
                        deletePlayer();
                }
            }
        } else
    
        // for input
        if (GLOB->audioParams->INenabled) {
            if (!sniffer) {
                createSniffer();
            } else {
                //       m_userState = sniffer->stoppedByUser();
                setDefaultAmbitus();
                doParamsUpdated = true;
            }
        } else {
            if (sniffer)
                deleteSniffer();
        }
    #if defined(Q_OS_ANDROID)
        if (player)
            static_cast<TaudioOUT *>(player)->setAudioOutParams();
        if (sniffer)
            sniffer->updateAudioParams();
    
        if (doParamsUpdated) {
            if (player && player->type() == TabstractPlayer::e_audio) {
                static_cast<TaudioOUT *>(player)->updateAudioParams();
                static_cast<TaudioOUT *>(player)->setAudioOutParams();
            } else if (sniffer)
                sniffer->updateAudioParams();
        }
    
        //   if (sniffer)
        //     restoreSniffer();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    QStringList Tsound::inputDevices() const
    {
        return TaudioIN::getAudioDevicesList();
    }
    
    SeeLook's avatar
    SeeLook committed
    
    
    QStringList Tsound::outputDevices() const
    {
        return TaudioOUT::getAudioDevicesList();
    }
    
    QString Tsound::currentInDevName() const
    {
        return TaudioIN::inputName();
    }
    
    QString Tsound::currentOutDevName() const
    {
        return TaudioOUT::outputName();
    }
    
    void Tsound::setJACKorASIO(bool setOn)
    {
    #if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID)
        TaudioIN::setJACKorASIO(setOn);
    
    SeeLook's avatar
    SeeLook committed
    #endif
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    float Tsound::pitch()
    {
        if (sniffer)
            return sniffer->lastNotePitch();
        else
            return 0.0f;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::setTempo(int t)
    {
        if (t != m_tempo && t > 39 && t < qMin(240, qRound(181.0 * Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit))))) {
            m_tempo = t;
            emit tempoChanged();
        }
    
    void Tsound::setBeatUnit(int bu)
    {
        if (bu > -1 && bu < 4) {
            if (bu != m_beatUnit) {
                int oldBu = m_beatUnit;
                m_beatUnit = bu;
                m_tempo = qMin(240,
                               qRound(static_cast<qreal>(m_tempo) * Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit))
                                      / Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(oldBu))));
                emit tempoChanged();
            }
    
    void Tsound::setCurrentMeter(int curMet)
    {
        if (curMet != m_currentMeter) {
            m_currentMeter = curMet;
            setBeatUnit(static_cast<int>(Tmeter(static_cast<Tmeter::Emeter>(m_currentMeter)).optimalBeat()));
        }
    
    void Tsound::setMetronome(int metronomeTempo, int metronomeBeat)
    {
        if (metronomeBeat != m_beatUnit || metronomeTempo != m_tempo) {
            int quarterTempo = Tmeter::quarterTempo(metronomeTempo, metronomeBeat);
            if (quarterTempo >= 40 && quarterTempo <= 180) {
                m_tempo = metronomeTempo;
                m_beatUnit = metronomeBeat;
                emit tempoChanged();
            } else
                qDebug() << "[Tsound] Can't set tempo" << metronomeTempo << "with" << static_cast<Tmeter::EbeatUnit>(metronomeBeat);
    
    void Tsound::runMetronome(int preTicksNr)
    {
        if (!GLOB->isSingleNote() && player && !m_metronomeIsRun && player->doTicking()) {
            player->setMetronome(m_tempo);
            if (player->tickBeforePlay() && preTicksNr) {
                qreal preTicksSeconds = static_cast<qreal>(preTicksNr) * (60.0 / static_cast<qreal>(m_tempo));
                while (preTicksSeconds < 2.0) { // Multiple number of countdown ticks if it is to short (less than 2 sec) - to give user time to catch up
                    preTicksNr += preTicksNr;
                    preTicksSeconds += preTicksSeconds;
                }
                player->setTicksCountBefore(preTicksNr);
                emit countdownPrepare(preTicksNr);
            }
            m_metronomeIsRun = true;
            emit metroRunningChanged();
        }
    }
    
    /**
     * @p m_quantVal is expressed in @p Trhythm duration of: Sixteenth triplet -> 4 or just Sixteenth -> 6 or Eighth -> 12
    
     * TODO: Triplets are not yet supported, so enable the code when they will be...
    
    void Tsound::setQuantization(int q)
    {
        if ((/*q == 4 || */ q == 6 || q == 12) && q != m_quantVal) {
            m_quantVal = q;
            GLOB->audioParams->quantization = m_quantVal;
            emit quantizationChanged();
        }
    
    bool Tsound::stoppedByUser() const
    {
        return sniffer ? sniffer->stoppedByUser() : false;
    
    void Tsound::setStoppedByUser(bool sbu)
    {
        if (sniffer && sniffer->stoppedByUser() != sbu) {
            sniffer->setStoppedByUser(sbu);
            emit stoppedByUserChanged();
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tsound::listening() const
    {
        return sniffer ? sniffer->detectingState() == TcommonListener::e_detecting : false;
    
    bool Tsound::playing() const
    {
        return player && player->isPlaying();
    
    void Tsound::stopListen()
    {
        if (sniffer)
            sniffer->stopListening();
        stopMetronome();
    
    /**
     * Starts pitch detection.
     * But in fact, only when user didn't stop it before.
     * By default skips initial count down (@p skipPreTicking)
     * - it avoids counting down after dialog windows are closed,
     * so exams executor should call it with @p FALSE
     */
    
    void Tsound::startListen(bool skipPreTicking)
    {
        if (sniffer) {
            if (!sniffer->stoppedByUser())
                runMetronome(skipPreTicking ? 0 : Tmeter(static_cast<Tmeter::Emeter>(m_currentMeter)).countTo());
            sniffer->startListening();
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::pauseSinffing()
    {
        if (sniffer)
            sniffer->pause();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::unPauseSniffing()
    {
        if (sniffer)
            sniffer->unPause();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tsound::isSnifferPaused()
    {
        return sniffer ? sniffer->isPaused() : false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tsound::isSniferStopped()
    {
        return sniffer ? sniffer->isStoped() : true;
    
    bool Tsound::tickBeforePlay() const
    {
        return player && player->tickBeforePlay();
    
    void Tsound::setTickBeforePlay(bool tbp)
    {
        if (player && tbp != player->tickBeforePlay()) {
            player->setTickBeforePlay(tbp);
            emit tickStateChanged();
        }
    
    bool Tsound::tickDuringPlay() const
    {
        return player && player->tickDuringPlay();
    
    void Tsound::setTickDuringPlay(bool tdp)
    {
        if (player && tdp != player->tickDuringPlay()) {
            player->setTickDuringPlay(tdp);
            emit tickStateChanged();
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    int Tsound::playingNoteId() const
    {
        return player && player->playingNoteId();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::prepareToExam(Tnote loNote, Tnote hiNote)
    {
        m_examMode = true;
        if (sniffer) {
            m_prevLoNote = sniffer->loNote();
            m_prevHiNote = sniffer->hiNote();
            sniffer->setAmbitus(loNote, hiNote);
        }
        if (player)
            disconnect(player, &TaudioOUT::nextNoteStarted, this, &Tsound::selectNextNote);
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::restoreAfterExam()
    {
        m_examMode = false;
        if (sniffer) {
            //     sniffer->setAmbitus(m_prevLoNote, m_prevHiNote); // acceptSettings() has already invoked setDefaultAmbitus()
            unPauseSniffing();
            startListen();
        }
        if (player)
            connect(player, &TaudioOUT::nextNoteStarted, this, &Tsound::selectNextNote);
    
    void Tsound::stopPlaying()
    {
        if (player) {
            stopMetronome();
            player->stop();
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::stop()
    {
        stopPlaying();
        stopListen();
    
    bool Tsound::isPlayable()
    {
        return player ? player->isPlayable() : false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tsound::melodyIsPlaying()
    {
        return player && player->isPlaying();
    
    void Tsound::setDefaultAmbitus()
    {
        if (sniffer)
            sniffer->setAmbitus(Tnote(GLOB->loString().chromatic() - 5), // range extended about 4th up and down
                                Tnote(GLOB->hiString().chromatic() + GLOB->GfretsNumber + 5));
    
    void Tsound::setTunerMode(bool isTuner)
    {
        if (isTuner != m_tunerMode) {
            m_tunerMode = isTuner;
            emit tunerModeChanged();
            if (!m_tunerMode && player) // approve changed middle A frequency (if any)
                player->setPitchOffset(GLOB->audioParams->a440diff - static_cast<qreal>(static_cast<int>(GLOB->audioParams->a440diff)));
        }
    
    #if defined(Q_OS_ANDROID)
    int Tsound::maxVolRange() const
    {
        return QAndroidJniObject::callStaticMethod<jint>("net/sf/nootka/ToutVolume", "maxStreamVolume");
    }
    
    int Tsound::currentVol() const
    {
        return QAndroidJniObject::callStaticMethod<jint>("net/sf/nootka/ToutVolume", "streamVolume");
    
    void Tsound::setVol(int v)
    {
        QAndroidJniObject::callStaticMethod<void>("net/sf/nootka/ToutVolume", "setStreamVolume", "(I)V", v);
    }
    
    void Tsound::setTouchHandling(bool th)
    {
        if (sniffer) {
            auto audioIn = qobject_cast<TaudioIN *>(sniffer);
            if (audioIn) {
                GLOB->setTouchStopsSniff(th);
                if (th)
                    audioIn->startTouchHandle();
                else
                    audioIn->stopTouchHandle();
            }
    
    #if !defined(Q_OS_ANDROID)
    void Tsound::changeDumpPath(const QString &path)
    {
        if (QFileInfo(path).exists()) {
            m_dumpPath = path;
            GLOB->audioParams->dumpPath = path;
        } else
            qDebug() << "[Tsound] dump path" << path << "does not exist!";
    
    void Tsound::setDumpFileName(const QString &fName)
    {
        if (sniffer && !GLOB->audioParams->dumpPath.isEmpty())
            sniffer->setDumpFileName(fName);
    
    // #################################################################################################
    // ###################                PRIVATE           ############################################
    // #################################################################################################
    
    void Tsound::createPlayer()
    {
        player = new TaudioOUT(GLOB->audioParams);
        connect(player, &TaudioOUT::playingStarted, this, &Tsound::playingStartedSlot);
        connect(player, &TaudioOUT::nextNoteStarted, this, &Tsound::selectNextNote);
        connect(player, &TaudioOUT::playingFinished, this, &Tsound::playingFinishedSlot);
        m_stopSniffOnce = false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::createSniffer()
    {
    #if !defined(Q_OS_ANDROID)
        if (TaudioIN::instance())
            sniffer = TaudioIN::instance();
        else
    
            sniffer = new TaudioIN(GLOB->audioParams);
        setDefaultAmbitus();
        //   sniffer->setAmbitus(Tnote(-31), Tnote(82)); // fixed ambitus bounded Tartini capacities
        connect(sniffer, &TaudioIN::noteStarted, this, &Tsound::noteStartedSlot);
        connect(sniffer, &TaudioIN::noteFinished, this, &Tsound::noteFinishedSlot);
        connect(sniffer, &TaudioIN::stateChanged, this, &Tsound::listeningChanged);
        m_userState = false; // user didn't stop sniffing yet
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::deletePlayer()
    {
        if (player) {
            player->stop();
            delete player;
            player = nullptr;
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::deleteSniffer()
    {
        delete sniffer;
        sniffer = nullptr;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::restoreSniffer()
    {
        sniffer->setStoppedByUser(m_userState);
        //   blockSignals(false);
        //   sniffer->startListening();
    
    // #################################################################################################
    // ###################                PROTECTED         ############################################
    // #################################################################################################
    #if defined(Q_OS_ANDROID)
    bool Tsound::eventFilter(QObject *watched, QEvent *event)
    {
        if (event->type() == QEvent::KeyPress) {
            auto ke = static_cast<QKeyEvent *>(event);
            if (ke->key() == Qt::Key_VolumeDown || ke->key() == Qt::Key_VolumeUp) {
                if (m_volKeyTimer.elapsed() > 100) {
                    if (!m_tunerMode) {
                        if (playing()) {
                            QAndroidJniObject::callStaticMethod<void>("net/sf/nootka/ToutVolume", "show");
                            if (ke->key() == Qt::Key_VolumeDown)
                                m_currVol--;
                            else
                                m_currVol++;
                            m_currVol = qBound(0, m_currVol, m_maxVol);
                            setVol(m_currVol);
                            m_currVol = QAndroidJniObject::callStaticMethod<jint>("net/sf/nootka/ToutVolume", "streamVolume");
                        } else if (!GLOB->isExam())
                            QTimer::singleShot(10, this, &Tsound::volumeKeyPressed);
                    } else {
    
                        if (ke->key() == Qt::Key_VolumeDown)
    
                            QTimer::singleShot(10, this, &Tsound::volumeDownPressed);
    
                            QTimer::singleShot(10, this, &Tsound::volumeUpPressed);
                    }
                    m_volKeyTimer.start();
    
        }
        return QObject::eventFilter(watched, event);
    
    // #################################################################################################
    // ###################            PRIVATE SLOTS         ############################################
    // #################################################################################################
    
    void Tsound::playingStartedSlot()
    {
        emit playingChanged();
        if (sniffer)
            sniffer->stopListening();
    
    void Tsound::playingFinishedSlot()
    {
        if (!m_examMode && sniffer) {
            if (m_stopSniffOnce) {
                sniffer->startListening();
                m_stopSniffOnce = false;
            }
    
        emit plaingFinished();
        emit playingChanged();
        stopMetronome();
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tsound::noteStartedSlot(const TnoteStruct &note)
    {
        m_detectedNote = note.pitch;
        m_detectedNote.setRhythm(GLOB->rhythmsEnabled() ? Trhythm::Sixteenth : Trhythm::NoRhythm, !m_detectedNote.isValid());
        if (!m_examMode && !m_tunerMode)
            NOO->noteStarted(m_detectedNote);
        emit noteStarted(m_detectedNote);
        emit noteStartedEntire(note);
    
    void Tsound::noteFinishedSlot(const TnoteStruct &note)
    {
        if (note.pitch.isValid())
            m_detectedNote = note.pitch;
        if (GLOB->rhythmsEnabled()) {
            qreal rFactor = 2500.0 / Tmeter::quarterTempo(m_tempo, m_beatUnit);
            qreal dur = (note.duration * 1000.0) / rFactor;
            int quant = dur > 20.0 ? 12 : m_quantVal; // avoid sixteenth dots
            int normDur = qRound(dur / static_cast<qreal>(quant)) * quant;
            Trhythm r(normDur, m_detectedNote.isRest());
            if (r.isValid()) {
                m_detectedNote.setRhythm(r);
    
                if (!m_examMode && !m_tunerMode)
    
            } else {
                int rtmRest = 0;
                TrhythmList notes = Trhythm::resolve(normDur, &rtmRest);
                for (int n = 0; n < notes.count(); ++n) {
                    Trhythm &rr = notes[n];
                    if (!m_detectedNote.isRest()) {
                        if (n == 0)
                            rr.setTie(Trhythm::e_tieStart);
                        else if (n == notes.count() - 1)
                            rr.setTie(Trhythm::e_tieEnd);
                        else
                            rr.setTie(Trhythm::e_tieCont);
                    }
                    m_detectedNote.setRhythm(rr.rhythm(), m_detectedNote.isRest(), rr.hasDot(), rr.isTriplet());
                    m_detectedNote.rtm.setTie(rr.tie());
                    //             qDebug() << "Split note detected" << note.duration << normDur << n << note.pitchF << m_detectedNote.rtm.string();
                    emit noteFinished();
                    if (!m_examMode && !m_tunerMode) {
                        if (n == 0) // update rhythm of the last note
                            NOO->noteFinished(m_detectedNote);
                        else { // but create others
                            NOO->noteStarted(m_detectedNote);
                            NOO->noteFinished(m_detectedNote);
                        }
                    }
    
            }
        } else {
            if (!m_examMode && !m_tunerMode)
                emit noteFinished();
        }
        emit noteFinishedEntire(note);
    
    void Tsound::selectNextNote()
    {
        if (player->playingNoteId() > -1 && player->playingNoteId() != NOO->selectedNoteId())
            NOO->selectPlayingNote(player->playingNoteId());
        emit playingNoteIdChanged();
    
    void Tsound::stopMetronome()
    {
        if (m_metronomeIsRun) {
            if (player)
                player->stopMetronome();
            m_metronomeIsRun = false;
            emit metroRunningChanged();
        }