/*************************************************************************** * Copyright (C) 2011-2021 by Tomasz Bojczuk * * seelook@gmail.com * * * * 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/>. * ***************************************************************************/ #include "tsound.h" #if defined(Q_OS_ANDROID) #include "tqtaudioin.h" #include "tqtaudioout.h" #include <QtAndroidExtras/qandroidfunctions.h> #include <QtAndroidExtras/qandroidjnienvironment.h> #else // #include "tmidiout.h" #include "tnotesbaritem.h" #include "trtaudioin.h" #include "trtaudioout.h" #include <QtCore/qfileinfo.h> #endif #include "ttickcolors.h" #include <music/tmelody.h> #include <taudioparams.h> #include <tglobals.h> #include <tnootkaqml.h> #include <QtCore/qdebug.h> #include <QtCore/qtimer.h> #include <QtGui/qevent.h> #include <QtGui/qguiapplication.h> #include <QtQml/qqmlengine.h> /* static */ Tsound *Tsound::m_instance = nullptr; QString Tsound::soundTouchVersion() { return TabstractPlayer::soundTouchVersion(); } #define INT_FACTOR (1.2) Tsound::Tsound(QObject *parent) : QObject(parent) , player(nullptr) , sniffer(nullptr) , m_tempo(60) , m_quantVal(6) { 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); } 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(); #endif } // ################################################################################################# // ################### PUBLIC ############################################ // ################################################################################################# 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); #endif 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(); } }); #endif } void Tsound::play(const Tnote ¬e) { 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; } } #endif } 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> ¬es, 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 deletePlayer(); // 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(); #else if (doParamsUpdated) { if (player && player->type() == TabstractPlayer::e_audio) { static_cast<TaudioOUT *>(player)->updateAudioParams(); static_cast<TaudioOUT *>(player)->setAudioOutParams(); } else if (sniffer) sniffer->updateAudioParams(); } #endif // if (sniffer) // restoreSniffer(); } QStringList Tsound::inputDevices() const { return TaudioIN::getAudioDevicesList(); } 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); #else Q_UNUSED(setOn) #endif } float Tsound::pitch() { if (sniffer) return sniffer->lastNotePitch(); else return 0.0f; } 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(); } } 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(); } } void Tsound::pauseSinffing() { if (sniffer) sniffer->pause(); } void Tsound::unPauseSniffing() { if (sniffer) sniffer->unPause(); } bool Tsound::isSnifferPaused() { return sniffer ? sniffer->isPaused() : false; } 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(); } } int Tsound::playingNoteId() const { return player && player->playingNoteId(); } 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); } 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(); } } void Tsound::stop() { stopPlaying(); stopListen(); } bool Tsound::isPlayable() { return player ? player->isPlayable() : false; } 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(); } } } #endif #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); } #endif // ################################################################################################# // ################### 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; } void Tsound::createSniffer() { #if !defined(Q_OS_ANDROID) if (TaudioIN::instance()) sniffer = TaudioIN::instance(); else #endif 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 } void Tsound::deletePlayer() { if (player) { player->stop(); delete player; player = nullptr; } } void Tsound::deleteSniffer() { delete sniffer; sniffer = nullptr; } 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); else QTimer::singleShot(10, this, &Tsound::volumeUpPressed); } m_volKeyTimer.start(); } } } return QObject::eventFilter(watched, event); } #endif // ################################################################################################# // ################### 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(); } void Tsound::noteStartedSlot(const TnoteStruct ¬e) { 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 ¬e) { 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); emit noteFinished(); if (!m_examMode && !m_tunerMode) NOO->noteFinished(m_detectedNote); } 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(); } }