#include "voicerecorder.h" #include #include #include #include #include #include #include #include #include #include "settings.h" namespace { void append16(QByteArray &b, quint16 v) { b.append(char(v & 0xff)); b.append(char((v >> 8) & 0xff)); } void append32(QByteArray &b, quint32 v) { b.append(char(v & 0xff)); b.append(char((v >> 8) & 0xff)); b.append(char((v >> 16) & 0xff)); b.append(char((v >> 24) & 0xff)); } // Picco (valore assoluto massimo) dei campioni 16 bit little-endian del blocco. int peakOf(const QByteArray &chunk) { int peak = 0; const char *d = chunk.constData(); const int n = chunk.size() - (chunk.size() % 2); for (int i = 0; i < n; i += 2) { const qint16 s = qint16(quint8(d[i]) | (quint8(d[i + 1]) << 8)); const int v = qAbs(int(s)); if (v > peak) peak = v; } return peak; } } // namespace VoiceRecorder::VoiceRecorder(Settings *settings, QObject *parent) : QObject(parent) , m_settings(settings) , m_input(nullptr) , m_device(nullptr) , m_silenceTimer(new QTimer(this)) , m_lastVoiceMs(0) , m_lastLevelEmitMs(0) , m_hasVoice(false) , m_active(false) , m_level(0.0) { m_silenceTimer->setInterval(150); connect(m_silenceTimer, &QTimer::timeout, this, &VoiceRecorder::onSilenceCheck); } void VoiceRecorder::start() { if (m_active) return; const QAudioDeviceInfo info = QAudioDeviceInfo::defaultInputDevice(); if (info.isNull()) { emit failed(tr("No audio input device")); return; } // PCM 16 bit mono: 16 kHz di solito basta per la voce; altrimenti si // ripiega su frequenze piu' comuni mantenendo il 16 bit mono. QAudioFormat format; format.setChannelCount(1); format.setSampleSize(16); format.setCodec(QStringLiteral("audio/pcm")); format.setByteOrder(QAudioFormat::LittleEndian); format.setSampleType(QAudioFormat::SignedInt); const int rates[] = { 16000, 48000, 44100, 32000, 8000 }; bool found = false; for (unsigned i = 0; i < sizeof(rates) / sizeof(rates[0]); ++i) { format.setSampleRate(rates[i]); if (info.isFormatSupported(format)) { found = true; break; } } if (!found) { emit failed(tr("Audio input format not supported")); return; } m_format = format; m_pcm.clear(); m_hasVoice = false; m_level = 0.0; m_lastVoiceMs = 0; m_lastLevelEmitMs = 0; m_input = new QAudioInput(info, m_format, this); m_device = m_input->start(); if (!m_device) { delete m_input; m_input = nullptr; emit failed(tr("Could not start audio capture")); return; } connect(m_device, &QIODevice::readyRead, this, &VoiceRecorder::onReadyRead); m_elapsed.start(); m_active = true; emit activeChanged(); emit levelChanged(); emit listening(); qDebug() << "[voice] ascolto avviato:" << m_format.sampleRate() << "Hz mono 16 bit"; m_silenceTimer->start(); } void VoiceRecorder::stop() { if (!m_active) return; finalize(); } void VoiceRecorder::onReadyRead() { if (!m_device) return; const QByteArray chunk = m_device->readAll(); if (chunk.isEmpty()) return; m_pcm.append(chunk); const int peak = peakOf(chunk); const qint64 elapsed = m_elapsed.elapsed(); if (peak > m_settings->vadThreshold()) { m_lastVoiceMs = elapsed; m_hasVoice = true; } if (elapsed - m_lastLevelEmitMs >= 150) { m_lastLevelEmitMs = elapsed; m_level = qreal(peak) / 32768.0; emit levelChanged(); } } void VoiceRecorder::onSilenceCheck() { if (!m_active) return; const qint64 elapsed = m_elapsed.elapsed(); // Nessuna voce dopo 12 s: la cattura viene scartata (failed "no-speech"). if (!m_hasVoice && elapsed >= 12000) { qDebug() << "[voice] nessuna voce rilevata, stop"; finalize(); return; } // Stop automatico: almeno un frammento di voce udito e pausa oltre soglia. if (m_hasVoice && elapsed - m_lastVoiceMs >= qint64(m_settings->vadSilenceMs()) && elapsed >= 800) { qDebug() << "[voice] stop automatico: pausa di" << (elapsed - m_lastVoiceMs) << "ms dopo voce a" << m_lastVoiceMs << "ms"; finalize(); return; } // Tetto di sicurezza anti-registrazione infinita. if (elapsed >= 60000) { qDebug() << "[voice] stop automatico: durata massima raggiunta"; finalize(); } } void VoiceRecorder::finalize() { m_silenceTimer->stop(); if (m_device) { m_pcm.append(m_device->readAll()); m_device = nullptr; } if (m_input) { m_input->stop(); m_input->deleteLater(); m_input = nullptr; } m_active = false; m_level = 0.0; emit activeChanged(); emit levelChanged(); if (!m_hasVoice) { m_pcm.clear(); emit failed(QStringLiteral("no-speech")); return; } const QString path = makeWavPath(); const int bytes = m_pcm.size(); if (!writeWav(path)) { m_pcm.clear(); emit failed(tr("Could not write the audio file")); return; } m_pcm.clear(); qDebug() << "[voice] WAV pronto:" << path << bytes << "byte"; emit finished(path); } QString VoiceRecorder::makeWavPath() const { const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/voice"); QDir().mkpath(dir); return dir + QStringLiteral("/v_") + QString::number(QDateTime::currentMSecsSinceEpoch()) + QStringLiteral(".wav"); } bool VoiceRecorder::writeWav(const QString &path) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) return false; const quint16 channels = quint16(m_format.channelCount()); const quint32 sampleRate = quint32(m_format.sampleRate()); const quint16 bits = quint16(m_format.sampleSize()); const quint32 dataSize = quint32(m_pcm.size()); const quint32 byteRate = sampleRate * channels * bits / 8; const quint16 blockAlign = quint16(channels * bits / 8); QByteArray header; header.reserve(44); header.append("RIFF"); append32(header, 36 + dataSize); header.append("WAVE"); header.append("fmt "); append32(header, 16); append16(header, 1); // PCM append16(header, channels); append32(header, sampleRate); append32(header, byteRate); append16(header, blockAlign); append16(header, bits); header.append("data"); append32(header, dataSize); f.write(header); f.write(m_pcm); f.close(); return true; }