diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e36b07b..9d216cd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,14 @@ add_includes_ldflags ("${ZLIB_LIBRARIES}" "${ZLIB_INCLUDE_DIRS}") find_package (Threads REQUIRED) set (xournalpp_LDFLAGS ${xournalpp_LDFLAGS} ${CMAKE_THREAD_LIBS_INIT}) +# portaudio +pkg_check_modules(PORTAUDIOCPP portaudiocpp) +add_includes_ldflags ("${PORTAUDIOCPP_LDFLAGS}" "${PORTAUDIOCPP_INCLUDE_DIRS}") + +# SOX +pkg_check_modules(SOX sox) +add_includes_ldflags ("${SOX_LDFLAGS}" "${SOX_INCLUDE_DIRS}") + ## Additional features ## # CppUnit diff --git a/src/control/AudioController.cpp b/src/control/AudioController.cpp index 3cbd36fa..e56e5ea4 100644 --- a/src/control/AudioController.cpp +++ b/src/control/AudioController.cpp @@ -9,12 +9,16 @@ AudioController::AudioController(Settings* settings, Control* control) XOJ_INIT_TYPE(AudioController); this->settings = settings; this->control = control; + this->audioRecorder = new AudioRecorder(settings); } AudioController::~AudioController() { XOJ_CHECK_TYPE(AudioController); + delete this->audioRecorder; + this->audioRecorder = nullptr; + XOJ_RELEASE_TYPE(AudioController); } @@ -29,8 +33,6 @@ void AudioController::recStartStop(bool rec) { XOJ_CHECK_TYPE(AudioController); - string command; - if (rec) { if (getAudioFolder().isEmpty()) @@ -42,7 +44,7 @@ void AudioController::recStartStop(bool rec) sttime = (g_get_monotonic_time() / 1000000); char buffer[50]; - time_t secs = time(0); + time_t secs = time(nullptr); tm *t = localtime(&secs); // This prints the date and time in ISO format. sprintf(buffer, "%04d-%02d-%02d_%02d-%02d-%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, @@ -53,16 +55,19 @@ void AudioController::recStartStop(bool rec) audioFilename = data; g_message("Start recording"); - command = "xopp-recording.sh start " + getAudioFolder().str() + "/" + data; + + this->audioRecorder->start(getAudioFolder().str() + "/" + data); } else if (this->recording) { this->recording = false; audioFilename = ""; sttime = 0; - command = "xopp-recording.sh stop"; + + g_message("Stop recording"); + + this->audioRecorder->stop(); } - system(command.c_str()); } void AudioController::recToggle() diff --git a/src/control/AudioController.h b/src/control/AudioController.h index 9eaf9332..ab63071f 100644 --- a/src/control/AudioController.h +++ b/src/control/AudioController.h @@ -4,6 +4,7 @@ #include "settings/Settings.h" #include "Control.h" #include +#include class AudioController { @@ -25,6 +26,7 @@ protected: gint sttime = 0; Settings* settings; Control* control; + AudioRecorder* audioRecorder; private: XOJ_TYPE_ATTRIB; diff --git a/src/util/XournalTypeList.h b/src/util/XournalTypeList.h index 491f3059..f2c7ffa3 100644 --- a/src/util/XournalTypeList.h +++ b/src/util/XournalTypeList.h @@ -268,5 +268,8 @@ XOJ_DECLARE_TYPE(ToolBase, 258); XOJ_DECLARE_TYPE(ScrollHandling, 259); XOJ_DECLARE_TYPE(ScrollHandlingGtk, 260); XOJ_DECLARE_TYPE(ScrollHandlingXournalpp, 261); - - +XOJ_DECLARE_TYPE(PortAudioProducer, 262); +XOJ_DECLARE_TYPE(SoxConsumer, 263); +XOJ_DECLARE_TYPE(AudioRecorder, 264); +XOJ_DECLARE_TYPE(AudioQueue, 265); +XOJ_DECLARE_TYPE(DeviceInfo, 266); diff --git a/src/util/audio/AudioQueue.cpp b/src/util/audio/AudioQueue.cpp new file mode 100644 index 00000000..dd7fc25c --- /dev/null +++ b/src/util/audio/AudioQueue.cpp @@ -0,0 +1,62 @@ +#include "AudioQueue.h" + +AudioQueue::AudioQueue() +{ + XOJ_INIT_TYPE(AudioQueue); +} + +AudioQueue::~AudioQueue() +{ + XOJ_CHECK_TYPE(AudioQueue); + + XOJ_RELEASE_TYPE(AudioQueue); +} + +void AudioQueue::reset() +{ + XOJ_CHECK_TYPE(AudioQueue); + + this->notified = false; + this->streamEnd = false; + this->clear(); +} + + +bool AudioQueue::empty() +{ + XOJ_CHECK_TYPE(AudioQueue); + + return deque::empty(); +} + +unsigned long AudioQueue::size() +{ + XOJ_CHECK_TYPE(AudioQueue); + + return deque::size(); +} + +void AudioQueue::push(int *samples, unsigned long nSamples) +{ + XOJ_CHECK_TYPE(AudioQueue); + + unsigned long requiredSize = this->size() + nSamples; + if (this->max_size() < requiredSize) + this->resize(requiredSize); + + for (long i = nSamples - 1; i >= 0; i--) + this->push_front(samples[i]); +} + +std::vector AudioQueue::pop(unsigned long nSamples) +{ + XOJ_CHECK_TYPE(AudioQueue); + + std::vector buffer(nSamples); + for (long i = nSamples - 1; i >= 0; i--) + { + buffer[i] = this->back(); + this->pop_back(); + } + return buffer; +} diff --git a/src/util/audio/AudioQueue.h b/src/util/audio/AudioQueue.h new file mode 100644 index 00000000..3ba9882c --- /dev/null +++ b/src/util/audio/AudioQueue.h @@ -0,0 +1,40 @@ +/* + * Xournal++ + * + * Queue to connect an audio producer and an audio consumer + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include + +#include +#include +#include +#include + +class AudioQueue : protected std::deque +{ +public: + AudioQueue(); + ~AudioQueue(); + void reset(); + bool empty(); + unsigned long size(); + void push(int *samples, unsigned long nSamples); + std::vector pop(unsigned long nSamples); + + std::mutex queueLock; + std::condition_variable lockCondition; + bool streamEnd = false; + bool notified = false; +private: + XOJ_TYPE_ATTRIB; +}; + + diff --git a/src/util/audio/AudioRecorder.cpp b/src/util/audio/AudioRecorder.cpp new file mode 100644 index 00000000..a1e44cf7 --- /dev/null +++ b/src/util/audio/AudioRecorder.cpp @@ -0,0 +1,52 @@ +#include "AudioRecorder.h" + +AudioRecorder::AudioRecorder(Settings *settings) : settings(settings) +{ + XOJ_INIT_TYPE(AudioRecorder); + + this->audioQueue = new AudioQueue(); + this->portAudioProducer = new PortAudioProducer(settings, this->audioQueue); + this->soxConsumer = new SoxConsumer(this->audioQueue); +} + +AudioRecorder::~AudioRecorder() +{ + XOJ_CHECK_TYPE(AudioRecorder); + + delete this->portAudioProducer; + this->portAudioProducer = nullptr; + + delete this->soxConsumer; + this->soxConsumer = nullptr; + + delete this->audioQueue; + this->audioQueue = nullptr; + + XOJ_RELEASE_TYPE(AudioRecorder); +} + +void AudioRecorder::start(std::string filename) +{ + XOJ_CHECK_TYPE(AudioRecorder); + + // Start the consumer for writing the data + // TODO get sample rate from settings + this->soxConsumer->start(filename, 44100.0, this->portAudioProducer->getSelectedInputDevice()); + + // Start recording + this->portAudioProducer->startRecording(); +} + +void AudioRecorder::stop() +{ + XOJ_CHECK_TYPE(AudioRecorder); + + // Stop recording audio + this->portAudioProducer->stopRecording(); + + // Wait for libsox to write all the data + this->soxConsumer->join(); + + // Reset the queue for the next recording + this->audioQueue->reset(); +} \ No newline at end of file diff --git a/src/util/audio/AudioRecorder.h b/src/util/audio/AudioRecorder.h new file mode 100644 index 00000000..4d6b71e2 --- /dev/null +++ b/src/util/audio/AudioRecorder.h @@ -0,0 +1,41 @@ +/* + * Xournal++ + * + * Class to record audio and store it as MP3-file + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include + +#include "AudioQueue.h" +#include "PortAudioProducer.h" +#include "SoxConsumer.h" + +#include + +class AudioRecorder +{ +public: + explicit AudioRecorder(Settings *settings); + ~AudioRecorder(); + void start(std::string filename); + void stop(); + +protected: + Settings* settings; + + AudioQueue *audioQueue; + PortAudioProducer *portAudioProducer; + SoxConsumer *soxConsumer; + +private: + XOJ_TYPE_ATTRIB; +}; + + diff --git a/src/util/audio/DeviceInfo.cpp b/src/util/audio/DeviceInfo.cpp new file mode 100644 index 00000000..80838ee6 --- /dev/null +++ b/src/util/audio/DeviceInfo.cpp @@ -0,0 +1,17 @@ +#include "DeviceInfo.h" + +DeviceInfo::DeviceInfo(portaudio::Device *device, bool selected) : deviceName(device->name()), + index(device->index()), + selected(selected), + inputChannels((device->isFullDuplexDevice() || device->isInputOnlyDevice()) ? device->maxInputChannels() : 0), + outputChannels((device->isFullDuplexDevice() || device->isOutputOnlyDevice()) ? device->maxOutputChannels() : 0) +{ + XOJ_INIT_TYPE(DeviceInfo); +} + +DeviceInfo::~DeviceInfo() +{ + XOJ_CHECK_TYPE(DeviceInfo); + + XOJ_RELEASE_TYPE(DeviceInfo); +} diff --git a/src/util/audio/DeviceInfo.h b/src/util/audio/DeviceInfo.h new file mode 100644 index 00000000..525cf723 --- /dev/null +++ b/src/util/audio/DeviceInfo.h @@ -0,0 +1,32 @@ +/* + * Xournal++ + * + * Class storing information about an audio device + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */#pragma once + +#include + +#include +#include + +class DeviceInfo +{ +public: + DeviceInfo(portaudio::Device *device, bool selected); + ~DeviceInfo(); + + const std::string deviceName; + const PaDeviceIndex index; + const bool selected; + const int inputChannels; + const int outputChannels; +private: + XOJ_TYPE_ATTRIB; +}; + + diff --git a/src/util/audio/PortAudioProducer.cpp b/src/util/audio/PortAudioProducer.cpp new file mode 100644 index 00000000..ef3f3dca --- /dev/null +++ b/src/util/audio/PortAudioProducer.cpp @@ -0,0 +1,123 @@ +#include "PortAudioProducer.h" + +PortAudioProducer::PortAudioProducer(Settings *settings, AudioQueue *audioQueue) : sys(portaudio::System::instance()), settings(settings), audioQueue(audioQueue) +{ + XOJ_INIT_TYPE(PortAudioProducer); + + DeviceInfo inputInfo(&sys.defaultInputDevice(), true); + this->setInputDevice(inputInfo); +} + +PortAudioProducer::~PortAudioProducer() +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + portaudio::System::terminate(); + + XOJ_RELEASE_TYPE(PortAudioProducer); +} + +std::list PortAudioProducer::getInputDevices() +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + std::list deviceList; + + for (portaudio::System::DeviceIterator i = this->sys.devicesBegin(); i != sys.devicesEnd(); ++i) + { + + if (i->isFullDuplexDevice() || i->isInputOnlyDevice()) + { + DeviceInfo deviceInfo(&(*i), this->selectedInputDevice == i->index()); + deviceList.push_back(deviceInfo); + } + + } + return deviceList; +} + +const DeviceInfo PortAudioProducer::getSelectedInputDevice() +{ + return DeviceInfo(&sys.deviceByIndex(this->selectedInputDevice), true); +} + + +void PortAudioProducer::setInputDevice(DeviceInfo deviceInfo) +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + this->selectedInputDevice = deviceInfo.index; + portaudio::Device *device = &sys.deviceByIndex(this->selectedInputDevice); + this->inputChannels = static_cast(device->maxInputChannels()); +} + +bool PortAudioProducer::isRecording() +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + return this->inputStream != nullptr && this->inputStream->isActive(); +} + +void PortAudioProducer::startRecording() +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + // Check if there already is a recording + if (this->inputStream != nullptr) + return; + + // Get the device information of our input device + portaudio::Device *device = &sys.deviceByIndex(this->selectedInputDevice); + portaudio::DirectionSpecificStreamParameters inParams(*device, device->maxInputChannels(), portaudio::INT32, true, device->defaultLowInputLatency(), nullptr); + portaudio::StreamParameters params(inParams, portaudio::DirectionSpecificStreamParameters::null(), this->sampleRate, this->framesPerBuffer, paNoFlag); + + // Specify the callback used for buffering the recorded data + this->inputStream = new portaudio::MemFunCallbackStream(params, *this, &PortAudioProducer::recordCallback); + + // Start the recording + this->inputStream->start(); +} + +int PortAudioProducer::recordCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags) +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + if (statusFlags) + { + g_message(("PortAudioProducer: statusFlag: " + std::to_string(statusFlags)).c_str()); + } + + if (inputBuffer != nullptr) + { + std::unique_lock lock(this->audioQueue->queueLock); + unsigned long providedFrames = framesPerBuffer * this->inputChannels; + + this->audioQueue->push(((int *) inputBuffer), providedFrames); + + this->audioQueue->notified = true; + this->audioQueue->lockCondition.notify_one(); + } + return paContinue; +} + +void PortAudioProducer::stopRecording() +{ + XOJ_CHECK_TYPE(PortAudioProducer); + + // Stop the recording + if (this->inputStream != nullptr) + { + this->inputStream->stop(); + this->inputStream->close(); + } + + // Notify the consumer at the other side that ther will be no more data + this->audioQueue->streamEnd = true; + this->audioQueue->notified = true; + this->audioQueue->lockCondition.notify_one(); + + // Allow new recording by removing the old one + delete this->inputStream; + this->inputStream = nullptr; +} \ No newline at end of file diff --git a/src/util/audio/PortAudioProducer.h b/src/util/audio/PortAudioProducer.h new file mode 100644 index 00000000..39a8833e --- /dev/null +++ b/src/util/audio/PortAudioProducer.h @@ -0,0 +1,62 @@ +/* + * Xournal++ + * + * Class to record audio using libportaudio + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include + +#include "DeviceInfo.h" +#include "AudioQueue.h" + +#include + +#include + +#include + +class PortAudioProducer +{ +public: + explicit PortAudioProducer(Settings *settings, AudioQueue *audioQueue); + + ~PortAudioProducer(); + + std::list getInputDevices(); + + const DeviceInfo getSelectedInputDevice(); + + void setInputDevice(DeviceInfo deviceInfo); + + bool isRecording(); + + void startRecording(); + + int recordCallback(const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo * timeInfo, PaStreamCallbackFlags statusFlags); + + void stopRecording(); +protected: + double sampleRate = 44100.0; + const unsigned long framesPerBuffer = 64; + + portaudio::AutoSystem autoSys; + portaudio::System &sys; + Settings *settings; + AudioQueue *audioQueue; + + PaDeviceIndex selectedInputDevice; + unsigned int inputChannels; + + portaudio::MemFunCallbackStream *inputStream = nullptr; +private: + XOJ_TYPE_ATTRIB; +}; + + diff --git a/src/util/audio/SoxConsumer.cpp b/src/util/audio/SoxConsumer.cpp new file mode 100644 index 00000000..b8e40eb1 --- /dev/null +++ b/src/util/audio/SoxConsumer.cpp @@ -0,0 +1,94 @@ +#include "SoxConsumer.h" + +SoxConsumer::SoxConsumer(AudioQueue *audioQueue) : audioQueue(audioQueue) +{ + XOJ_INIT_TYPE(SoxConsumer); + + sox_init(); + sox_format_init(); +} + +SoxConsumer::~SoxConsumer() +{ + XOJ_CHECK_TYPE(SoxConsumer); + + sox_format_quit(); + sox_quit(); + + XOJ_RELEASE_TYPE(SoxConsumer); +} + +void SoxConsumer::start(std::string filename, double sampleRate, const DeviceInfo &inputDevice) +{ + XOJ_CHECK_TYPE(SoxConsumer); + + this->inputSignal = new sox_signalinfo_t; + this->inputSignal->rate = sampleRate; + this->inputSignal->length = SOX_UNSPEC; + this->inputSignal->channels = (unsigned int) inputDevice.inputChannels; + this->inputSignal->mult = nullptr; + this->inputSignal->precision = 32; + + this->outputFile = sox_open_write(filename.c_str(), this->inputSignal, nullptr, nullptr, nullptr, nullptr); + + if (this->outputFile == nullptr) + { + g_message("SoxConsumer: output file could not be opened"); + return; + } + + this->consumerThread = new std::thread([&] + { + std::unique_lock lock(audioQueue->queueLock); + unsigned long availableFrames; + + + while (!(this->stopConsumer || (audioQueue->streamEnd && audioQueue->empty()))) + { + while (!audioQueue->notified) + { + audioQueue->lockCondition.wait(lock); + } + + while (!audioQueue->empty()) + { + unsigned long queueSize = audioQueue->size(); + availableFrames = std::min(queueSize - queueSize % this->inputSignal->channels, (unsigned long) (64 * this->inputSignal->channels)); + if (availableFrames > 0) + { + std::vector tmpBuffer = audioQueue->pop(availableFrames); + sox_write(this->outputFile, tmpBuffer.data(), availableFrames); + } + } + audioQueue->notified = false; + } + + sox_close(this->outputFile); + + }); +} + + +void SoxConsumer::join() +{ + XOJ_CHECK_TYPE(SoxConsumer); + + // Join the consumer thread to wait for completion + if (this->consumerThread->joinable()) + this->consumerThread->join(); +} + +void SoxConsumer::stop() +{ + XOJ_CHECK_TYPE(SoxConsumer); + + // Stop consumer + this->stopConsumer = true; + this->audioQueue->notified = true; + this->audioQueue->lockCondition.notify_one(); + + // Wait for consumer to finish + if (this->consumerThread->joinable()) + this->consumerThread->join(); + +} diff --git a/src/util/audio/SoxConsumer.h b/src/util/audio/SoxConsumer.h new file mode 100644 index 00000000..94b8b40c --- /dev/null +++ b/src/util/audio/SoxConsumer.h @@ -0,0 +1,46 @@ +/* + * Xournal++ + * + * Class to save audio data in an mp3 file + * + * @author Xournal++ Team + * https://github.com/xournalpp/xournalpp + * + * @license GNU GPLv2 or later + */ + +#pragma once + +#include + +#include "AudioQueue.h" +#include "DeviceInfo.h" + +#include + +#include +#include + + +class SoxConsumer +{ +public: + explicit SoxConsumer(AudioQueue *audioQueue); + ~SoxConsumer(); + void start(std::string filename, double sampleRate, const DeviceInfo &inputDevice); + void join(); + void stop(); + +protected:protected: + sox_signalinfo_t *inputSignal = nullptr; + sox_format_t *outputFile = nullptr; + bool stopConsumer = false; + + AudioQueue *audioQueue; + std::thread *consumerThread; + +private: + XOJ_TYPE_ATTRIB; +}; + +