aboutsummaryrefslogblamecommitdiff
path: root/qmidiplayer-desktop/qmpmainwindow.cpp
blob: cc315df8676a7dd4a31214e8e9d6389900356a6d (plain) (tree)
1
2
3
4
5
6
7
8
9
                 
                


                            
               
                    
                    
                
                       
                      
                    

                              

                             
                                                                                                                     
                                                                                                                  
             
                    
                                 
 



                                                                                      
 
      
                          
 



























                                                                        



                               



































                                                  



                          
















































                                                                                     

                                                                                 














































                                                                                                                                                

 
                              
 



















                                                                             

 




                                      

                                                  

















                                                                       

 

                                                





                                              


                                                          

                                                            
 
 

                                   





















                                                                                                               

                                  











































                                                                                                             




                                 

                                                          

                    








                                                                      

                    












                                                                                                           

                          



                                 
                                        
                                                                                                                                      
                                                                                            

                                        






                                                                                         

                                            





                                                                              
                                                                                               

                                                                           
             
                                                                                        
      


                                          
 

                                     
                                               
 

                                       
                                                




































                                                                                                               
                                                                                    
                                                                
 

                                                     






                                                                                                 
             


                                                       
     
                                                
      

                
 


                                        
                                                    
     

                                      
      






                                                                                              
             
            
      
               
 
 

                                         


                                                                                                             
                                                                                                         









                                                                                                 



                                             




                                                                                                                              

 




































                                                                             

                                            













                                               
                                            
                                                                                                                                          
                                                                                                






                                                                                             
                       

                                                





                                                                                  
                                                                                               

                                                                               
             
                                                                                            
      



                                              
                                                                          

        
                           



                                              
                    



                                               

























                                                                                        

                                            





                                               

                                                   



                         









                                                                    
                                                        









                                                                    
                                                        




                                                                         







                                                                                                         







                                                                              

 

                                                 


                                                                        



                                       
           



                                  

                                          

 

                                       
                



                                       
                



                                      
                                          
 
 

                                                                               


                                                         

 

                                 












                                                                 

 
                                                                                    
 





                                                                
 
                                                                                    
 





                                                                
 
                                                             
 

                                                                

 
                                                                                                                                                
 


                                                                    
 

                                                             
 













                                                                           

 







                                                                 
 

                                 

                                                          
                                                                                      

                                                                     

 
                                 
 
             

                                                                                                 




                                                           
     
                                             

                                                                          


                                
      





                                                                                   

 
                                 
 












                                                                                     

 



                                                               
 



























































                                                                                                  

                                 



































































                                                                                                  

 

                                           


                                    
 
 

                                                    








                                                           

 

                                           
                  
 
 

                                                                                                                     
 














                                                                                                   
 


                                      

                                           
 
#include <cstdio>
#include <cmath>
#include <unicode/ustring.h>
#include <unicode/unistr.h>
#include <unicode/ucnv.h>
#include <QUrl>
#include <QFileInfo>
#include <QMimeData>
#include <QFont>
#include <QDirIterator>
#include <QMessageBox>
#include <QCheckBox>
#include "qmpmidioutfluid.hpp"
#include "qmpmidiplay.hpp"
#include "qmpmainwindow.hpp"
#include "ui_qmpmainwindow.h"
#define setButtonHeight(x,h) {x->setMaximumHeight(h*(logicalDpiY()/96.));x->setMinimumHeight(h*(logicalDpiY()/96.));}
#define setButtonWidth(x,h) {x->setMaximumWidth(h*(logicalDpiY()/96.));x->setMinimumWidth(h*(logicalDpiY()/96.));}
#ifdef _WIN32
#include <windows.h>
char *wcsto8bit(const wchar_t *s)
{
    int size = WideCharToMultiByte(CP_OEMCP, WC_NO_BEST_FIT_CHARS, s, -1, 0, 0, 0, 0);
    char *c = (char *)calloc(size, sizeof(char));
    WideCharToMultiByte(CP_OEMCP, WC_NO_BEST_FIT_CHARS, s, -1, c, size, 0, 0);
    return c;
}
#endif
#define UPDATE_INTERVAL 66

qmpMainWindow *qmpMainWindow::ref = nullptr;

qmpMainWindow::qmpMainWindow(QCommandLineParser *_clp, QWidget *parent):
    QMainWindow(parent),
    ui(new Ui::qmpMainWindow),
    clp(_clp)
{
    ui->setupUi(this);
    ui->lbCurPoly->setText("00000");
    ui->lbMaxPoly->setText("00000");
    ui->lbFileName->setText("");
    ref = this;
    ui->verticalLayout->setAlignment(ui->pushButton, Qt::AlignRight);
    setButtonHeight(ui->pbNext, 36);
    setButtonHeight(ui->pbPlayPause, 36);
    setButtonHeight(ui->pbAdd, 36);
    setButtonHeight(ui->pbPrev, 36);
    setButtonHeight(ui->pbSettings, 36);
    setButtonHeight(ui->pbStop, 36);
    playing = false;
    stopped = true;
    dragging = false;
    fin = false;
    settings.reset(new qmpSettings());
    settingsw = new qmpSettingsWindow(settings.get(), this);
    player = nullptr;
    timer = nullptr;
    fluidrenderer = nullptr;
}

qmpMainWindow::~qmpMainWindow()
{
    QList<QAction *>a = ui->lbFileName->actions();
    for (unsigned i = 0; i < a.size(); ++i)
    {
        ui->lbFileName->removeAction(a[i]);
        delete a[i];
    }
    pmgr->deinitPlugins();
    auto rtdev = qmpRtMidiManager::getDevices();
    for (auto &i : rtdev)
        player->unregisterMidiOutDevice(i.second);
    delete pmgr;
    if (timer)
        delete timer;
    delete helpw;
    helpw = nullptr;
    delete efxw;
    efxw = nullptr;
    delete chnlw;
    chnlw = nullptr;
    delete plistw;
    plistw = nullptr;
    delete infow;
    infow = nullptr;
    delete settingsw;
    settingsw = nullptr;
    delete panicf;
    panicf = nullptr;
    delete renderf;
    renderf = nullptr;
    delete reloadsynf;
    reloadsynf = nullptr;
    if (player)
        delete player;
    internalfluid->deviceDeinit();
    delete internalfluid;
    delete ui;
}

void qmpMainWindow::init()
{
    show();
    ui->centralWidget->setEnabled(false);

    pmgr = new qmpPluginManager();
    registerMidiOptions();

    std::future<void> f = std::async(std::launch::async, [this]
    {
        player = new CMidiPlayer();
        internalfluid = new qmpMidiOutFluid();
        player->registerMidiOutDevice(internalfluid, "Internal FluidSynth");
        reloadsynf = new qmpReloadSynthFunc(this);

        internalfluid->registerOptions(pmgr->pluginAPI);
        playerSetup(internalfluid);
        internalfluid->deviceInit();
        loadSoundFont(internalfluid);
        for (int i = 0; i < 16; ++i)
            player->setChannelOutput(i, 0);

        auto rtdev = qmpRtMidiManager::getDevices();
        for (auto &i : rtdev)
            player->registerMidiOutDevice(i.first, i.second);
    });
    while (f.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout)
        QApplication::processEvents();
    ui->centralWidget->setEnabled(true);

    settingsw->registerSoundFontOption();
    registerBehaviorOptions();
    settingsw->registerCustomizeWidgetOptions();

    plistw = new qmpPlistWindow(this);
    chnlw = new qmpChannelsWindow(this);
    efxw = new qmpEfxWindow(this);
    infow = new qmpInfoWindow(this);
    helpw = new qmpHelpWindow(this);
    timer = new QTimer(this);
    renderf = new qmpRenderFunc(this);
    panicf = new qmpPanicFunc(this);
    if (argfiles.size())
    {
        plistw->emptyList();
        for (auto &i : argfiles)
            plistw->insertItem(i);
    }

    if (settings->getOptionBool("Behavior/DialogStatus"))
    {
        QRect g = settings->getOptionRaw("DialogStatus/MainW", QRect()).toRect();
        if (!g.isNull())
            setGeometry(g);
    }

    registerFunctionality(renderf, "Render", tr("Render to wave").toStdString(), getThemedIconc(":/img/render.svg"), 0, false);
    registerFunctionality(panicf, "Panic", tr("Panic").toStdString(), getThemedIconc(":/img/panic.svg"), 0, false);
    registerFunctionality(reloadsynf, "ReloadSynth", tr("Restart fluidsynth").toStdString(), getThemedIconc(":/img/repeat-base.svg"), 0, false);
    const QStringList &qpp = clp->values("plugin");
    std::vector<std::string> pp;
    for (auto &s : qpp)
        pp.push_back(s.toStdString());
    pmgr->scanPlugins(pp);
    settingsw->registerPluginOption(pmgr);
    settingsw->updatePluginList(pmgr);
    pmgr->initPlugins();

    settingsw->registerExtraMidiOptions();

    QVariant *dinif_v = static_cast<QVariant *>(settings->getOptionCustom("Midi/DeviceInitializationFiles"));
    QList<QVariant> devinif_list = dinif_v->toList();
    delete dinif_v;
    QMap<QString, QString> devinif_map;
    for (auto &i : devinif_list)
    {
        QPair<QString, QString> p = i.value<QPair<QString, QString>>();
        devinif_map[p.first] = p.second;
    }
    auto rtdev = qmpRtMidiManager::getDevices();
    for (auto &i : rtdev)
    {
        if (devinif_map.contains(QString(i.second.c_str())))
            i.first->setInitializerFile(devinif_map[QString(i.second.c_str())].toStdString().c_str());
    }
    chnlw->selectDefaultDevice();

    ui->vsMasterVol->setValue(settings->getOptionRaw("FluidSynth/Gain", 50).toInt());
    connect(timer, &QTimer::timeout, this, &qmpMainWindow::updateWidgets);
    connect(timer, &QTimer::timeout, infow, &qmpInfoWindow::updateInfo);
    ui->pbNext->setIcon(QIcon(getThemedIcon(":/img/next.svg")));
    ui->pbPrev->setIcon(QIcon(getThemedIcon(":/img/prev.svg")));
    ui->pbPlayPause->setIcon(QIcon(getThemedIcon(":/img/play.svg")));
    ui->pbStop->setIcon(QIcon(getThemedIcon(":/img/stop.svg")));
    ui->pbSettings->setIcon(QIcon(getThemedIcon(":/img/settings.svg")));
    ui->pbAdd->setIcon(QIcon(getThemedIcon(":/img/open.svg")));
    if (argfiles.size())
        on_pbPlayPause_clicked();
    setupWidget();
    settingsw->postInit();
}

int qmpMainWindow::parseArgs()
{
    bool loadfolder = clp->isSet("load-all-files");
    const QStringList &args = clp->positionalArguments();
    for (int i = 0; i < args.size(); ++i)
    {
        if (QFileInfo(args.at(i)).exists())
        {
            if (loadfolder || settings->getOptionBool("Behavior/LoadFolder"))
            {
                QDirIterator di(QFileInfo(args.at(i)).absolutePath());
                while (di.hasNext())
                {
                    QString c = di.next();
                    argfiles.push_back(c);
                }
            }
            else
                argfiles.push_back(args.at(i));
        }
    }
    return 0;
}

bool qmpMainWindow::startedWithFiles()
{
    return !argfiles.empty();
}

void qmpMainWindow::closeEvent(QCloseEvent *event)
{
    if (settings->getOptionBool("Behavior/DialogStatus"))
    {
        settings->setOptionRaw("DialogStatus/MainW", geometry());
    }
    on_pbStop_clicked();
    fin = true;
    for (auto i = mfunc.begin(); i != mfunc.end(); ++i)
    {
        i->second.i()->close();
        i->second.setAssignedControl((QReflectiveAction *)nullptr),
        i->second.setAssignedControl((QReflectivePushButton *)nullptr);
    }
    efxw->close();
    chnlw->close();
    plistw->close();
    infow->close();
    settingsw->close();
    event->accept();
}

void qmpMainWindow::dropEvent(QDropEvent *event)
{
    QList<QUrl> l = event->mimeData()->urls();
    QStringList sl;
    for (int i = 0; i < l.size(); ++i)
        sl.push_back(l.at(i).toLocalFile());
    plistw->insertItems(sl);
    switchTrack(plistw->getLastItem());
}
void qmpMainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    //if(event->mimeData()->hasFormat("application/x-midi"))
    event->acceptProposedAction();
}

void qmpMainWindow::updateWidgets()
{
    setFuncEnabled("Render", stopped);
    setFuncEnabled("ReloadSynth", stopped);
    if (player->isFinished() && playerTh)
    {
        if (!plistw->getRepeat())
        {
            timer->stop();
            stopped = true;
            playing = false;
            invokeCallback("main.stop", nullptr);
            setFuncEnabled("Render", stopped);
            setFuncEnabled("ReloadSynth", stopped);
            player->playerDeinit();
            auto f = std::async([this] {playerTh->join();});
            while (f.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout)
            {
                QApplication::processEvents();
                ui->lbCurPoly->setText(QString("%1").arg(internalfluid->getPolyphone(), 5, 10, QChar('0')));
                ui->lbMaxPoly->setText(QString("%1").arg(internalfluid->getMaxPolyphone(), 5, 10, QChar('0')));
            }
            delete playerTh;
            playerTh = nullptr;
            player->playerPanic();
            player->playerReset();
            chnlw->on_pbUnmute_clicked();
            chnlw->on_pbUnsolo_clicked();
            ui->pbPlayPause->setIcon(QIcon(getThemedIcon(":/img/play.svg")));
            ui->hsTimer->setValue(0);
            ui->lbCurPoly->setText("00000");
            ui->lbMaxPoly->setText("00000");
            ui->lbCurTime->setText("00:00");
        }
        else
            switchTrack(plistw->getNextItem(), false);
    }
    if (renderTh)
    {
        if (fluidrenderer->isFinished())
        {
            renderTh->join();
            timer->stop();
            ui->centralWidget->setEnabled(true);
            delete renderTh;
            renderTh = nullptr;
            fluidrenderer->renderDeinit();
            delete fluidrenderer;
            fluidrenderer = nullptr;
        }
    }
    while (!player->isFinished() && player->getTCeptr() > player->getStamp(ui->hsTimer->value())
        && ui->hsTimer->value() < 100 && !dragging)
        ui->hsTimer->setValue(ui->hsTimer->value() + 1);
    if (playing)
    {
        std::chrono::duration<double> elapsed =
            std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - st);
        char ts[100];
        sprintf(ts, "%02d:%02d", (int)(elapsed.count() + offset) / 60, (int)(elapsed.count() + offset) % 60);
        ui->lbCurTime->setText(ts);
        ui->lbCurPoly->setText(QString("%1").arg(internalfluid->getPolyphone(), 5, 10, QChar('0')));
        ui->lbMaxPoly->setText(QString("%1").arg(internalfluid->getMaxPolyphone(), 5, 10, QChar('0')));
    }
}

QString qmpMainWindow::getFileName()
{
    return ui->lbFileName->text();
}

QUrl qmpMainWindow::getFilePath()
{
    return filepath;
}
void qmpMainWindow::switchTrack(QString s, bool interrupt)
{
    stopped = true;
    playing = false;
    setFuncEnabled("Render", stopped);
    setFuncEnabled("ReloadSynth", stopped);
    ui->pbPlayPause->setIcon(QIcon(getThemedIcon(":/img/pause.svg")));
    if (interrupt)
    {
        player->playerDeinit();
        player->playerPanic();
    }
    invokeCallback("main.stop", nullptr);
    stopped = false;
    playing = true;
    if (playerTh)
    {
        auto f = std::async([this] {playerTh->join();});
        while (f.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout)
        {
            QApplication::processEvents();
            ui->lbCurPoly->setText(QString("%1").arg(internalfluid->getPolyphone(), 5, 10, QChar('0')));
            ui->lbMaxPoly->setText(QString("%1").arg(internalfluid->getMaxPolyphone(), 5, 10, QChar('0')));
        }
        delete playerTh;
        playerTh = nullptr;
    }
    timer->stop();
    player->playerPanic();
    player->playerReset();
    ui->hsTimer->setValue(0);
    chnlw->on_pbUnmute_clicked();
    chnlw->on_pbUnsolo_clicked();
    QString fns = s;
    filepath = QUrl::fromLocalFile(fns);
    setWindowTitle(QUrl::fromLocalFile(fns).fileName().left(QUrl::fromLocalFile(fns).fileName().lastIndexOf('.')) + " - QMidiPlayer");
    ui->lbFileName->setText(filepath.fileName().left(filepath.fileName().lastIndexOf('.')));
    if (plistw->getCurrentItem() != fns)
        plistw->setCurrentItem(fns);
    onfnChanged();
    if (!loadFile(fns))
        return;
    char ts[100];
    sprintf(ts, "%02d:%02d", (int)player->getFtime() / 60, (int)player->getFtime() % 60);
    ui->lbFinTime->setText(ts);
    player->playerInit();
    PlaybackStatus ps = getPlaybackStatus();
    invokeCallback("main.start", &ps);
    internalfluid->setGain(ui->vsMasterVol->value() / 250.);
    efxw->sendEfxChange();
    playerTh = new std::thread([this]
    {
        player->playerThread();
        if (settings->getOptionBool("Midi/WaitVoice") && player->isFinished())
            while (internalfluid->getOutputLevel() > -100 && internalfluid->getPolyphone() > 0)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
    });
#ifdef _WIN32
    SetThreadPriority((void *)playerTh->native_handle(), THREAD_PRIORITY_TIME_CRITICAL);
#endif
    st = std::chrono::steady_clock::now();
    offset = 0;
    timer->start(UPDATE_INTERVAL);
}
std::string qmpMainWindow::getTitle()
{
    return decodeStdString(player->getTitle());
}
std::wstring qmpMainWindow::getWTitle()
{
    return decodeStdWString(player->getTitle());
}

void qmpMainWindow::playerSetup(IFluidSettings *fs)
{
    fs->setOptStr("audio.driver", settings->getOptionEnumIntOptName("FluidSynth/AudioDriver").c_str());
    fs->setOptInt("audio.period-size", settings->getOptionInt("FluidSynth/BufSize"));
    fs->setOptInt("audio.periods", settings->getOptionInt("FluidSynth/BufCnt"));
    fs->setOptStr("audio.sample-format", settings->getOptionEnumIntOptName("FluidSynth/SampleFormat").c_str());
    fs->setOptNum("synth.sample-rate", settings->getOptionInt("FluidSynth/SampleRate"));
    fs->setOptInt("synth.polyphony", settings->getOptionInt("FluidSynth/Polyphony"));
    fs->setOptInt("synth.cpu-cores", settings->getOptionInt("FluidSynth/Threads"));
    std::string bsmode;
    if (settings->getOptionBool("FluidSynth/AutoBS") && player->getFileStandard())
        switch (player->getFileStandard())
        {
            case 1:
                bsmode = "gm";
                break;
            case 2:
                bsmode = "mma";
                break;
            case 3:
                bsmode = "gs";
                break;
            case 4:
                bsmode = "xg";
                break;
        }
    else
    {
        bsmode = settings->getOptionEnumIntOptName("FluidSynth/BankSelect");
        std::transform(bsmode.begin(), bsmode.end(), bsmode.begin(), [](char i)
        {
            return tolower(i);
        });
    }
    fs->setOptStr("synth.midi-bank-select", bsmode.c_str());
    fs->setOptInt("synth.device-id", settings->getOptionInt("FluidSynth/DeviceID"));
    player->sendSysX(settings->getOptionBool("Midi/SendSysEx"));
}
void qmpMainWindow::loadSoundFont(IFluidSettings *fs)
{
    QVariant *data = static_cast<QVariant *>(settings->getOptionCustom("FluidSynth/SoundFonts"));
    QList<QVariant> sflist = data->toList();
    for (auto i = sflist.rbegin(); i != sflist.rend(); ++i)
    {
        if (i->toString().startsWith('#'))
            continue;
        QString sf = i->toString();
#ifdef _WIN32
        char *c = wcsto8bit(sf.toStdWString().c_str());
        fs->loadSFont(c);
        free(c);
#else
        fs->loadSFont(sf.toStdString().c_str());
#endif
    }
    delete data;
}
int qmpMainWindow::loadFile(QString fns)
{
#ifdef _WIN32
    char *c = wcsto8bit(fns.toStdWString().c_str());
#else
    std::string s = fns.toStdString();
    const char *c = s.c_str();
#endif
    int ret = 1;
    invokeCallback("main.reset", nullptr);
    if (!player->playerLoadFile(c))
    {
        QMessageBox::critical(this, tr("Error"), tr("%1 is not a valid midi file.").arg(fns));
        ret = 0;
    }
#ifdef _WIN32
    free(c);
#endif
    return ret;
}

void qmpMainWindow::registerMidiOptions()
{
    settings->registerOptionBool("MIDI", "Disable MIDI Mapping", "Midi/DisableMapping", false);
    settings->registerOptionBool("MIDI", "Send system exclusive messages", "Midi/SendSysEx", true);
    settings->registerOptionBool("MIDI", "Wait for remaining voice before stopping", "Midi/WaitVoice", true);
    settings->registerOptionInt("MIDI", "Fluidsynth Device ID", "FluidSynth/DeviceID", 0x00, 0x7E, 0x10);
    UErrorCode e = U_ZERO_ERROR;
    auto ucnvc = ucnv_countAvailable();
    std::vector<std::string> converters;
    for (auto i = 0; i < ucnvc; ++i)
    {
        auto n = ucnv_getAvailableName(i);
        auto sn = ucnv_getStandardName(n, "MIME", &e);
        if (sn) converters.push_back(std::string(sn));
    }
    settings->registerOptionEnumInt("MIDI", "Text encoding", "Midi/TextEncoding", converters, 0);
}

void qmpMainWindow::registerBehaviorOptions()
{
    settings->registerOptionBool("Behavior", "Restore last playlist on startup", "Behavior/RestorePlaylist", false);
    settings->registerOptionBool("Behavior", "Add files in the same folder to playlist", "Behavior/LoadFolder", false);
    settings->registerOptionBool("Behavior", "Save dialog status", "Behavior/DialogStatus", false);
    settings->registerOptionBool("Behavior", "Show labels beside icon in toolbar buttons", "Behavior/ShowButtonLabel", false);
    settings->registerOptionEnumInt("Behavior", "Icon Theme", "Behavior/IconTheme", {"Auto", "Dark", "Light"}, 0);
}

QString qmpMainWindow::decodeString(const char *str)
{
    std::string enc("utf-8");
    if (ref && ref->settings)
        enc = ref->settings->getOptionEnumIntOptName("Midi/TextEncoding");
    icu::UnicodeString us(str, enc.c_str());
    std::string r;
    return QString::fromUtf8(us.toUTF8String(r).c_str());
}

std::string qmpMainWindow::decodeStdString(const char *str)
{
    std::string enc("utf-8");
    if (ref && ref->settings)
        enc = ref->settings->getOptionEnumIntOptName("Midi/TextEncoding");
    icu::UnicodeString us(str, enc.c_str());
    std::string r;
    us.toUTF8String(r);
    return r;
}

std::wstring qmpMainWindow::decodeStdWString(const char *str)
{
    std::string enc("utf-8");
    if (ref && ref->settings)
        enc = ref->settings->getOptionEnumIntOptName("Midi/TextEncoding");
    icu::UnicodeString us(str, enc.c_str());
    std::wstring r;
    int32_t sz = 0;
    UErrorCode e = U_ZERO_ERROR;
    u_strToWCS(nullptr, 0, &sz, us.getBuffer(), us.length(), &e);
    r.resize(sz + 1);
    e = U_ZERO_ERROR;
    u_strToWCS(r.data(), r.size(), nullptr, us.getBuffer(), us.length(), &e);
    return r;
}

void qmpMainWindow::on_pbPlayPause_clicked()
{
    if (stopped)
    {
        QString fns = plistw->getFirstItem();
        if (!fns.length())
        {
            if (!plistw->on_pbAdd_clicked())
            {
                playing = false;
                return;
            }
            fns = plistw->getFirstItem();
            if (!fns.length())
                return (void)(playing = false);
        }
        filepath = QUrl::fromLocalFile(fns);
        setWindowTitle(QUrl::fromLocalFile(fns).fileName().left(QUrl::fromLocalFile(fns).fileName().lastIndexOf('.')) + " - QMidiPlayer");
        ui->lbFileName->setText(filepath.fileName().left(filepath.fileName().lastIndexOf('.')));
        onfnChanged();
        if (!loadFile(fns))
            return;
        char ts[100];
        sprintf(ts, "%02d:%02d", (int)player->getFtime() / 60, (int)player->getFtime() % 60);
        ui->lbFinTime->setText(ts);
        player->playerInit();
        playing = true;
        PlaybackStatus ps = getPlaybackStatus();
        invokeCallback("main.start", &ps);
        internalfluid->setGain(ui->vsMasterVol->value() / 250.);
        efxw->sendEfxChange();
        playerTh = new std::thread([this]
        {
            player->playerThread();
            if (settings->getOptionBool("Midi/WaitVoice") && player->isFinished())
            while (internalfluid->getOutputLevel() > -100 && internalfluid->getPolyphone() > 0)
                    std::this_thread::sleep_for(std::chrono::milliseconds(10));
        });
#ifdef _WIN32
        SetThreadPriority((void *)playerTh->native_handle(), THREAD_PRIORITY_TIME_CRITICAL);
#endif
        st = std::chrono::steady_clock::now();
        offset = 0;
        timer->start(UPDATE_INTERVAL);
        stopped = false;
        ui->pbPlayPause->setIcon(QIcon(getThemedIcon(":/img/pause.svg")));
    }
    else
        setPaused(playing);
}

void qmpMainWindow::on_hsTimer_sliderPressed()
{
    dragging = true;
}

void qmpMainWindow::on_hsTimer_sliderReleased()
{
    dragging = false;
    if (playing)
    {
        if (ui->hsTimer->value() == 100)
        {
            on_pbNext_clicked();
            return;
        }
        player->playerPanic();
        player->setTCeptr(player->getStamp(ui->hsTimer->value()), ui->hsTimer->value());
        offset = ui->hsTimer->value() / 100.*player->getFtime();
        st = std::chrono::steady_clock::now();
    }
    else
    {
        if (stopped)
        {
            ui->hsTimer->setValue(0);
            return;
        }
        player->setTCeptr(player->getStamp(ui->hsTimer->value()), ui->hsTimer->value());
        offset = ui->hsTimer->value() / 100.*player->getFtime();
        char ts[100];
        sprintf(ts, "%02d:%02d", (int)(offset) / 60, (int)(offset) % 60);
        ui->lbCurTime->setText(ts);
    }
    PlaybackStatus ps = getPlaybackStatus();
    invokeCallback("main.seek", &ps);
}

uint32_t qmpMainWindow::getPlaybackPercentage()
{
    return ui->hsTimer->value();
}
void qmpMainWindow::playerSeek(uint32_t percentage)
{
    if (percentage > 100)
        percentage = 100;
    if (percentage < 0)
        percentage = 0;
    if (playing)
    {
        if (percentage == 100)
        {
            on_pbNext_clicked();
            return;
        }
        player->playerPanic();
        ui->hsTimer->setValue(percentage);
        player->setTCeptr(player->getStamp(percentage), percentage);
        offset = percentage / 100. * player->getFtime();
        st = std::chrono::steady_clock::now();
    }
    else
    {
        if (stopped)
        {
            ui->hsTimer->setValue(0);
            return;
        }
        player->setTCeptr(player->getStamp(percentage), percentage);
        offset = percentage / 100. * player->getFtime();
        ui->hsTimer->setValue(percentage);
        char ts[100];
        sprintf(ts, "%02d:%02d", (int)(offset) / 60, (int)(offset) % 60);
        ui->lbCurTime->setText(ts);
    }
    PlaybackStatus ps = getPlaybackStatus();
    invokeCallback("main.seek", &ps);
}

PlaybackStatus qmpMainWindow::getPlaybackStatus()
{
    std::chrono::duration<double> elapsed =
        std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - st);
    return {
        .paused=!playing,
        .stopped=stopped,
        .curtime_ms=stopped ? 0 : uint64_t((elapsed.count() + offset) * 1000),
        .maxtime_ms=stopped ? 0 : uint64_t(player->getFtime() * 1000),
        .curtick=stopped ? 0 : player->getTick(),
        .maxtick=stopped ? 0 : player->getMaxTick()
    };
}

void qmpMainWindow::on_vsMasterVol_valueChanged()
{
    if (!stopped)
        internalfluid->setGain(ui->vsMasterVol->value() / 250.);
    settings->setOptionRaw("FluidSynth/Gain", ui->vsMasterVol->value());
}

void qmpMainWindow::on_pbStop_clicked()
{
    stop();
}

void qmpMainWindow::dialogClosed()
{
    if (!settingsw->isVisible())
        ui->pbSettings->setChecked(false);
}

void qmpMainWindow::on_pbPrev_clicked()
{
    prevTrack();
}

void qmpMainWindow::on_pbNext_clicked()
{
    nextTrack();
}

void qmpMainWindow::selectionChanged()
{
    switchTrack(plistw->getCurrentItem());
}

void qmpMainWindow::on_lbFileName_customContextMenuRequested(const QPoint &pos)
{
    QMenu menu(ui->lbFileName);
    menu.addActions(ui->lbFileName->actions());
    menu.exec(this->pos() + ui->lbFileName->pos() + pos);
}

void qmpMainWindow::onfnChanged()
{
    if (!ui->lbFileName->text().length())
        return;
    QFont f = ui->lbFileName->font();
    f.setPointSize(18);
    QFontMetrics fm(f);
    QSize size = fm.size(0, ui->lbFileName->text());
    double fw = ui->lbFileName->width() / (double)size.width();
    double fh = ui->lbFileName->height() / (double)size.height();
    double ps = floor(f.pointSizeF() * (fw < fh ? fw : fh));
    if (ps < 6)
        ps = 6;
    f.setPointSizeF(ps > 18 ? 18 : ps);
    ui->lbFileName->setFont(f);
}

int qmpMainWindow::registerUIHook(std::string e, ICallBack *callback, void *userdat)
{
    std::map<int, std::pair<qmpCallBack, void *>> &m = muicb[e];
    int id = 0;
    if (m.size())
        id = m.rbegin()->first + 1;
    m[id] = std::make_pair(qmpCallBack(callback), userdat);
    return id;
}
int qmpMainWindow::registerUIHook(std::string e, callback_t callback, void *userdat)
{
    std::map<int, std::pair<qmpCallBack, void *>> &m = muicb[e];
    int id = 0;
    if (m.size())
        id = m.rbegin()->first + 1;
    m[id] = std::make_pair(qmpCallBack(callback), userdat);
    return id;
}
void qmpMainWindow::unregisterUIHook(std::string e, int hook)
{
    std::map<int, std::pair<qmpCallBack, void *>> &m = muicb[e];
    m.erase(hook);
}

void qmpMainWindow::registerFunctionality(qmpFuncBaseIntf *i, std::string name, std::string desc, const char *icon, int iconlen, bool checkable)
{
    if (mfunc.find(name) != mfunc.end())
        return;
    mfunc[name] = qmpFuncPrivate(i, desc, icon, iconlen, checkable);
}

void qmpMainWindow::unregisterFunctionality(std::string name)
{
    mfunc.erase(name);
    for (auto i = enabled_actions.begin(); i != enabled_actions.end(); ++i)
        if (*i == name)
        {
            enabled_actions.erase(i);
            break;
        }
    for (auto i = enabled_buttons.begin(); i != enabled_buttons.end(); ++i)
        if (*i == name)
        {
            enabled_buttons.erase(i);
            break;
        }
    setupWidget();
}

void qmpMainWindow::setFuncState(std::string name, bool state)
{
    mfunc[name].setChecked(state);
}
void qmpMainWindow::setFuncEnabled(std::string name, bool enable)
{
    mfunc[name].setEnabled(enable);
}

bool qmpMainWindow::isDarkTheme()
{
    if (!settings->getOptionEnumInt("Behavior/IconTheme"))
    {
        return ui->centralWidget->palette().color(QPalette::Window).lightness() < 128;
    }
    else return 2 - settings->getOptionEnumInt("Behavior/IconTheme");
}

void qmpMainWindow::startRender()
{
#ifdef _WIN32
    char *ofstr = wcsto8bit((plistw->getCurrentItem() + QString(".wav")).toStdWString().c_str());
    char *ifstr = wcsto8bit(plistw->getCurrentItem().toStdWString().c_str());
    fluidrenderer = new qmpFileRendererFluid(ifstr, ofstr);
    playerSetup(fluidrenderer);
    fluidrenderer->renderInit();
    free(ofstr);
    free(ifstr);
#else
    fluidrenderer = new qmpFileRendererFluid(
        plistw->getCurrentItem().toStdString().c_str(),
        (plistw->getCurrentItem() + QString(".wav")).toStdString().c_str()
    );
    playerSetup(fluidrenderer);
    fluidrenderer->renderInit();
#endif
    loadSoundFont(fluidrenderer);
    ui->centralWidget->setEnabled(false);
    fluidrenderer->setGain(ui->vsMasterVol->value() / 250.);
    efxw->sendEfxChange(fluidrenderer);
    timer->start(UPDATE_INTERVAL);
    renderTh = new std::thread(&qmpFileRendererFluid::renderWorker, fluidrenderer);
}

void qmpMainWindow::reloadSynth()
{
    ui->centralWidget->setEnabled(false);
    std::future<void> f = std::async(std::launch::async,
            [this]
    {
        internalfluid->deviceDeinit(true);
        playerSetup(internalfluid);
        internalfluid->deviceInit();
        loadSoundFont(internalfluid);
    }
        );
    while (f.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout)
        QApplication::processEvents();
    ui->centralWidget->setEnabled(true);
}

std::map<std::string, qmpFuncPrivate> &qmpMainWindow::getFunc()
{
    return mfunc;
}

void qmpMainWindow::setPaused(bool paused)
{
    if (stopped)
        return;
    playing = !paused;
    if (paused)
    {
        player->playerPanic();
        offset = ui->hsTimer->value() / 100. * player->getFtime();
    }
    else
    {
        st = std::chrono::steady_clock::now();
        player->setResumed();
    }
    player->setTCpaused(paused);
    PlaybackStatus ps = getPlaybackStatus();
    invokeCallback("main.pause", &ps);
    ui->pbPlayPause->setIcon(QIcon(getThemedIcon(paused ? ":/img/play.svg" : ":/img/pause.svg")));
}

void qmpMainWindow::nextTrack()
{
    switchTrack(plistw->getNextItem());
}

void qmpMainWindow::prevTrack()
{
    switchTrack(plistw->getPrevItem());
}

void qmpMainWindow::stop()
{
    if (!stopped)
    {
        timer->stop();
        stopped = true;
        playing = false;
        invokeCallback("main.stop", nullptr);
        player->playerDeinit();
        setFuncEnabled("Render", stopped);
        setFuncEnabled("ReloadSynth", stopped);
        player->playerPanic();
        player->playerReset();
        if (playerTh)
        {
            playerTh->join();
            delete playerTh;
            playerTh = nullptr;
        }
        chnlw->on_pbUnmute_clicked();
        chnlw->on_pbUnsolo_clicked();
        ui->pbPlayPause->setIcon(QIcon(getThemedIcon(":/img/play.svg")));
        ui->hsTimer->setValue(0);
        ui->lbCurPoly->setText("00000");
        ui->lbMaxPoly->setText("00000");
        ui->lbCurTime->setText("00:00");
    }
}

void qmpMainWindow::setupWidget()
{
    for (auto i = mfunc.begin(); i != mfunc.end(); ++i)
    {
        i->second.setAssignedControl(static_cast<QReflectiveAction *>(nullptr));
        i->second.setAssignedControl(static_cast<QReflectivePushButton *>(nullptr));
    }
    QVariant *v = static_cast<QVariant *>(settings->getOptionCustom("Behavior/Toolbar"));
    enabled_buttons.clear();
    for (auto i : v->toList())
        enabled_buttons.push_back(i.toString().toStdString());
    delete v;
    v = static_cast<QVariant *>(settings->getOptionCustom("Behavior/Actions"));
    enabled_actions.clear();
    for (auto i : v->toList())
        enabled_actions.push_back(i.toString().toStdString());
    delete v;
    QList<QWidget *>w = ui->buttonwidget->findChildren<QWidget *>("", Qt::FindDirectChildrenOnly);
    qDeleteAll(w);
    QList<QAction *>a = ui->lbFileName->actions();
    for (int i = 0; i < a.size(); ++i)
    {
        ui->lbFileName->removeAction(a[i]);
        delete a[i];
    }
    for (unsigned i = 0; i < enabled_buttons.size(); ++i)
    {
        if (mfunc.find(enabled_buttons[i]) == mfunc.end())
            continue;
        QReflectivePushButton *pb = new QReflectivePushButton(
            mfunc[enabled_buttons[i]].icon(),
            tr(mfunc[enabled_buttons[i]].desc().c_str()),
            enabled_buttons[i]
        );
        setButtonHeight(pb, 32);
        //!!TODO
        if (settings->getOptionBool("Behavior/ShowButtonLabel"))
        {
            pb->setText(tr(mfunc[enabled_buttons[i]].desc().c_str()));
            pb->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
        }
        else
            setButtonWidth(pb, 32);
        pb->setIconSize(QSize(16, 16));
        ui->buttonwidget->layout()->addWidget(pb);
        mfunc[enabled_buttons[i]].setAssignedControl(pb);
        connect(pb, &QReflectivePushButton::onClick, this, &qmpMainWindow::funcReflector);
    }
    for (unsigned i = 0; i < enabled_actions.size(); ++i)
    {
        if (mfunc.find(enabled_actions[i]) == mfunc.end())
            continue;
        QReflectiveAction *a = new QReflectiveAction(
            mfunc[enabled_actions[i]].icon(),
            tr(mfunc[enabled_actions[i]].desc().c_str()),
            enabled_actions[i]
        );
        ui->lbFileName->addAction(a);
        mfunc[enabled_actions[i]].setAssignedControl(a);
        connect(a, &QReflectiveAction::onClick, this, &qmpMainWindow::funcReflector);
    }
    ui->buttonwidget->layout()->setAlignment(Qt::AlignLeft);
}

void qmpMainWindow::invokeCallback(std::string cat, void *callerdat)
{
    std::map<int, std::pair<qmpCallBack, void *>> *mp;
    mp = &muicb[cat];
    for (auto &i : *mp)
        i.second.first(callerdat, i.second.second);
}

void qmpMainWindow::on_pbSettings_clicked()
{
    if (ui->pbSettings->isChecked())
        settingsw->show();
    else settingsw->close();
}

void qmpMainWindow::funcReflector(std::string reflt)
{
    if (mfunc[reflt].isCheckable())
    {
        mfunc[reflt].setChecked(!mfunc[reflt].isChecked());
        if (mfunc[reflt].isChecked())
            mfunc[reflt].i()->show();
        else
            mfunc[reflt].i()->close();
    }
    else mfunc[reflt].i()->show();
}

void qmpMainWindow::on_pushButton_clicked()
{
    helpw->show();
}

qmpFuncPrivate::qmpFuncPrivate(qmpFuncBaseIntf *i, std::string _desc, const char *icon, int iconlen, bool checkable):
    _i(i), des(_desc), _checkable(checkable)
{
    if (icon)
    {
        QImage img;
        if (icon[0] == ':' && icon[1] == '/' || icon[0] == 'q' && icon[1] == 'r' && icon[2] == 'c')
            img = QImage(QString(icon));
        else
            img.loadFromData((uchar *)icon, iconlen);
        QPixmap pixm;
        pixm.convertFromImage(img);
        _icon = QIcon(pixm);
    }
    else _icon = QIcon();
    checked = false;
    asgna = nullptr;
    asgnb = nullptr;
}

void qmpMainWindow::on_pbAdd_clicked()
{
    if (plistw->on_pbAdd_clicked())
        switchTrack(plistw->getLastItem());
}