#include <QAction>#include <QAnyStringView>#include <QClipboard>#include <QCloseEvent>#include <QCoreApplication>#include <QElapsedTimer>#include <QEvent>#include <QGuiApplication>#include <QIcon>#include <QIODeviceBase>#include <QJsonDocument>#include <QJsonObject>#include <QMenu>#include <QSettings>#include <AL/al.h>#include <AL/alc.h>#include <AL/alext.h>#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__) // Generalstatic const QAnyStringView windowVisible = "windowVisible";// Speechstatic 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 ALsizeiaudiocb(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;} boolWindow::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;} voidWindow::stop(){ QProcess *tmp; alDeleteSources(1, &alsrc); ALERR(); alsrc = 0; alDeleteBuffers(1, &albuf); ALERR(); albuf = 0; tmp = extra; extra = startPiper(piper); piper = tmp; playing = false;} voidWindow::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;} voidWindow::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;} voidWindow::selectionChanged(){ QString newText = clip->text(QClipboard::Selection); if((textChanged = !(text == newText))) text = newText;} voidWindow::closeEvent(QCloseEvent *ev){ if(ev->spontaneous()){ settings->setValue(windowVisible, false); hide(); ev->ignore(); }} voidWindow::quit(){ piper->terminate(); piper->waitForFinished(-1); qApp->quit();} voidWindow::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 intalcheckerr(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;}
#include <QAction>
#include <QAnyStringView>
#include <QClipboard>
#include <QCloseEvent>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QEvent>
#include <QGuiApplication>
#include <QIcon>
#include <QIODeviceBase>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMenu>
#include <QSettings>
#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>
#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;
}