#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "window.hpp" static int alcheckerr(const char *file, int line); static ALCcontext *ctx; static ALCdevice *dev; static ALuint albuf, alsrc; static QElapsedTimer underrun; typedef ALsizei (*nalBufferCallbackTypeSOFT)(ALvoid *aux, ALvoid *data, ALsizei numbytes); typedef void (*nalBufferCallbackSOFTType)(ALuint buf, ALenum fmt, ALsizei freq, nalBufferCallbackTypeSOFT cb, ALvoid *aux); static nalBufferCallbackSOFTType nalBufferCallbackSOFT; #define ALERR() alcheckerr(__FILE__, __LINE__) // General static const QAnyStringView windowVisible = "windowVisible"; // Speech static const QAnyStringView speechGroup = "Speech", speechModel = "model", speechVolume = "volume", speechLengthScale = "lengthScale", speechUseCUDA = "useCUDA"; Window::Window() { settings = new QSettings(this); quitAction = new QAction(tr("&Quit"), this); connect(quitAction, &QAction::triggered, this, &Window::quit); showAction = new QAction(tr("&Configure"), this); connect(showAction, &QAction::triggered, this, &Window::makeVisible); tMenu = new QMenu(this); tMenu->addAction(showAction); tMenu->addAction(quitAction); tIcon = new QSystemTrayIcon(this); tIcon->setContextMenu(tMenu); connect(tIcon, &QSystemTrayIcon::activated, this, &Window::iconActivated); tIcon->setIcon(QIcon(":/res/systray.svg")); tIcon->show(); setWindowTitle(tr("Nublyat")); if(settings->value(windowVisible, true).toBool()) show(); clip = QGuiApplication::clipboard(); connect(clip, &QClipboard::selectionChanged, this, &Window::selectionChanged); playing = false; textChanged = false; if((piper = startPiper(nullptr)) == nullptr || (extra = startPiper(nullptr)) == nullptr || !startAudio()) qApp->quit(); } static ALsizei audiocb(ALvoid *aux, ALvoid *data, ALsizei numbytes) { QProcess *in = (QProcess*)aux; char *b; ALsizei n; for(b = (char*)data; numbytes > 0; b += n, numbytes -= n){ n = in->read(b, numbytes); if(n <= 0){ if(!underrun.isValid()) underrun.start(); memset(b, 0, numbytes); b += numbytes; break; } } return b - (char*)data; } bool Window::startAudio() { ALCint attrs[] = {0}; int e; dev = nullptr; ctx = nullptr; dev = alcOpenDevice(alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER)); ALERR(); if(dev == nullptr) return false; ctx = alcCreateContext(dev, attrs); if(ctx == nullptr){ ALERR(); closedev: alcCloseDevice(dev); ALERR(); dev = nullptr; return false; } e = alcMakeContextCurrent(ctx); ALERR(); if(!e){ closectx: alcDestroyContext(ctx); ALERR(); ctx = nullptr; goto closedev; } if(!alIsExtensionPresent("AL_SOFT_callback_buffer")){ qCritical("no AL_SOFT_callback_buffer available"); goto closectx; } settings->beginGroup(speechGroup); alListenerf(AL_GAIN, settings->value(speechVolume, 1.0f).toFloat()); ALERR(); settings->endGroup(); alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED); ALERR(); nalBufferCallbackSOFT = (nalBufferCallbackSOFTType)alGetProcAddress("alBufferCallbackSOFT"); ALERR(); alsrc = albuf = 0; return true; } void Window::stop() { QProcess *tmp; alDeleteSources(1, &alsrc); ALERR(); alsrc = 0; alDeleteBuffers(1, &albuf); ALERR(); albuf = 0; tmp = extra; extra = startPiper(piper); piper = tmp; playing = false; } void Window::play() { piper->waitForReadyRead(-1); underrun.invalidate(); alGenSources(1, &alsrc); ALERR(); alSourcei(alsrc, AL_SOURCE_SPATIALIZE_SOFT, AL_FALSE); ALERR(); alGenBuffers(1, &albuf); ALERR(); nalBufferCallbackSOFT(albuf, AL_FORMAT_MONO16, 22050, audiocb, piper); ALERR(); alSourcei(alsrc, AL_BUFFER, albuf); ALERR(); alSourcePlay(alsrc); ALERR(); playing = true; } void Window::makeVisible() { settings->setValue(windowVisible, true); show(); } QProcess * Window::startPiper(QProcess *piper) { bool result = false; settings->beginGroup(speechGroup); if(piper != nullptr){ piper->terminate(); piper->waitForFinished(-1); }else{ piper = new QProcess(this); } QString model = settings->value(speechModel).toString(); if(!model.isEmpty()){ QStringList args; args << "--output-raw" << "--json-input" << "-m" << model << "--length_scale" << settings->value(speechLengthScale, 1.0).toString(); if(settings->value(speechUseCUDA).toBool()) args << "--use-cuda"; piper->setProcessChannelMode(QProcess::ForwardedErrorChannel); piper->start("piper", args, QIODeviceBase::ReadWrite); result = true; }else { qWarning("no model configured, can't speak"); } settings->endGroup(); return result ? piper : nullptr; } void Window::selectionChanged() { QString newText = clip->text(QClipboard::Selection); if((textChanged = !(text == newText))) text = newText; } void Window::closeEvent(QCloseEvent *ev) { if(ev->spontaneous()){ settings->setValue(windowVisible, false); hide(); ev->ignore(); } } void Window::quit() { piper->terminate(); piper->waitForFinished(-1); qApp->quit(); } void Window::iconActivated(QSystemTrayIcon::ActivationReason reason) { switch(reason){ case QSystemTrayIcon::Trigger: if(playing){ stop(); if(!textChanged) break; } textChanged = false; if(!text.isEmpty()){ for(auto part : text.split('\n', Qt::SkipEmptyParts)) { QJsonObject j; j["text"] = part; qInfo(QJsonDocument(j).toJson(QJsonDocument::Compact)); piper->write(QJsonDocument(j).toJson(QJsonDocument::Compact)); piper->write("\n"); } play(); } break; case QSystemTrayIcon::MiddleClick: if(playing) stop(); break; default: break; } } static int alcheckerr(const char *file, int line) { int e, ret; const char *s; char tmp[32]; ret = 0; if(ctx != nullptr && (e = alGetError()) != AL_NO_ERROR){ switch(e){ case AL_INVALID_NAME: s = "invalid name"; break; case AL_INVALID_ENUM: s = "invalid enum"; break; case AL_INVALID_VALUE: s = "invalid value"; break; case AL_INVALID_OPERATION: s = "invalid operation"; break; case AL_OUT_OF_MEMORY: s = "out of memory"; break; default: snprintf(tmp, sizeof(tmp), "unknown (0x%x)", e); s = tmp; break; } ret |= e; qWarning("AL: %s:%d: %s", file, line, s); } if(dev != nullptr && (e = alcGetError(dev)) != ALC_NO_ERROR){ switch(e){ case ALC_INVALID_DEVICE: s = "invalid device"; break; case ALC_INVALID_ENUM: s = "invalid enum"; break; case ALC_INVALID_VALUE: s = "invalid value"; break; case ALC_INVALID_CONTEXT: s = "invalid context"; break; case ALC_OUT_OF_MEMORY: s = "out of memory"; break; default: snprintf(tmp, sizeof(tmp), "unknown error (0x%x)", e); s = tmp; break; } ret |= e; qWarning("ALC: %s:%d: %s\n", file, line, s); } return ret; }