Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions plugins/AudioRecorder/AudioRecorder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#include "AudioRecorder.h"
#include "AudioRecorderView.h"
#include "embed.h"
#include "plugin_export.h"

#include <QDateTime>
#include <QDir>
#include <QStandardPaths>
#include <QDebug>

#include <sndfile.h>

#ifdef __linux__
#include <alsa/asoundlib.h>
#endif

namespace lmms {

extern "C" {
Plugin::Descriptor PLUGIN_EXPORT audiorecorder_plugin_descriptor = {
LMMS_STRINGIFY(PLUGIN_NAME),
"Audio Recorder",
QT_TRANSLATE_NOOP("PluginBrowser", "Record audio from a microphone to WAV"),
"Your Name <[email protected]>",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Your Name <[email protected]>",
"Your Name <[email protected]>",

Missing

0x0100,
Plugin::Type::Tool,
new PluginPixmapLoader("logo"),
nullptr,
nullptr
};

PLUGIN_EXPORT Plugin* lmms_plugin_main(Model*, void*) { return new AudioRecorder; }
}

static QString recordingsDir() {
const QString music = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QDir dir(music.isEmpty() ? QDir::homePath() : music);
dir.mkpath("LMMS Recordings");
return dir.filePath("LMMS Recordings");
}

QString AudioRecorder::makeDefaultOutPath() {
return recordingsDir() + "/" +
QString("LMMS-Record_%1.wav").arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"));
}

AudioRecorder::AudioRecorder()
: ToolPlugin(&audiorecorder_plugin_descriptor, nullptr) {}

AudioRecorder::~AudioRecorder() { stop(); }

void AudioRecorder::start()
{
// already recording?
if (m_recording.exchange(true))
return;

// join any old thread
if (m_worker.joinable())
m_worker.join();

#ifdef __linux__
// Build save location: ~/Music/LMMS Recordings/LMMS-Record_YYYYmmdd_HHmmss.wav
const QString music = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
QDir dir(music.isEmpty() ? QDir::homePath() : music);
dir.mkpath("LMMS Recordings");
m_lastPath = dir.filePath(QString("LMMS Recordings/LMMS-Record_%1.wav")
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")));

m_worker = std::thread([this]() {
const unsigned sampleRate = 44100;
const int channels = 1;
const snd_pcm_format_t fmt = SND_PCM_FORMAT_S16_LE;

snd_pcm_t* pcm = nullptr;
if (snd_pcm_open(&pcm, "default", SND_PCM_STREAM_CAPTURE, 0) < 0) {
m_recording.store(false);
return;
}
if (snd_pcm_set_params(pcm, fmt, SND_PCM_ACCESS_RW_INTERLEAVED,
channels, sampleRate, 1, 500000) < 0) {
snd_pcm_close(pcm);
m_recording.store(false);
return;
}

SF_INFO sfinfo{};
sfinfo.channels = channels;
sfinfo.samplerate = sampleRate;
sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;

SNDFILE* sf = sf_open(m_lastPath.toUtf8().constData(), SFM_WRITE, &sfinfo);
if (!sf) {
snd_pcm_close(pcm);
m_recording.store(false);
return;
}

const size_t frames = 1024;
std::vector<int16_t> buf(frames * channels);

while (m_recording.load()) {
snd_pcm_sframes_t got = snd_pcm_readi(pcm, buf.data(), frames);
if (got < 0) { snd_pcm_prepare(pcm); continue; }
if (got > 0) sf_write_short(sf, buf.data(), got * channels);
}

sf_write_sync(sf);
sf_close(sf);
snd_pcm_close(pcm);
});
#else
// Not implemented on this platform
m_recording.store(false);
#endif
}

void AudioRecorder::stop()
{
// flip flag and join thread; writer closes the WAV
if (!m_recording.exchange(false))
return;

if (m_worker.joinable())
m_worker.join();

// optional: log where it went
qInfo() << "AudioRecorder: saved to" << m_lastPath;
}

gui::PluginView* AudioRecorder::instantiateView(QWidget* parent) {
return new gui::AudioRecorderView(this, parent);
}

QString AudioRecorder::nodeName() const {
return QString::fromLatin1(audiorecorder_plugin_descriptor.name);
}
} // namespace lmms
36 changes: 36 additions & 0 deletions plugins/AudioRecorder/AudioRecorder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once
#include <atomic>
#include <thread>
#include <vector>
#include <QString>
#include "ToolPlugin.h"


namespace lmms {

class AudioRecorder final : public ToolPlugin {
public:
AudioRecorder();
~AudioRecorder() override;
const QString& lastPath() const { return m_lastPath; }
void start();
void stop();

gui::PluginView* instantiateView(QWidget*) override;
QString nodeName() const override;

void saveSettings(QDomDocument&, QDomElement&) override {}
void loadSettings(const QDomElement&) override {}

const QString& lastPath() const { return m_lastPath; } // <— expose last path

private:
static QString makeDefaultOutPath(); // <— one place to decide path
private:
QString m_lastPath; // final file path used by the running recording
std::vector<float> m_buffer; // (optional) in-memory float buffer
std::atomic<bool> m_recording{false};
std::thread m_worker;
};

} // namespace lmms
67 changes: 67 additions & 0 deletions plugins/AudioRecorder/AudioRecorderView.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "AudioRecorderView.h"
#include "AudioRecorder.h"

#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QDesktopServices>
#include <QFileInfo>
#include <QUrl>

using namespace lmms;
using namespace lmms::gui;

AudioRecorderView::AudioRecorderView(AudioRecorder* plugin, QWidget* parent)
: ToolPluginView(plugin), // ToolPluginView takes ToolPlugin* (no QWidget* in ctor)
m_plugin(plugin)
{
auto* v = new QVBoxLayout(this);

auto* title = new QLabel(tr("Audio Recorder"), this);
title->setStyleSheet("font-weight:600;");
v->addWidget(title);

auto* row = new QHBoxLayout();
m_startBtn = new QPushButton(tr("Record"), this);
m_stopBtn = new QPushButton(tr("Stop"), this);
m_stopBtn->setEnabled(false);
row->addWidget(m_startBtn);
row->addWidget(m_stopBtn);
v->addLayout(row);

auto* prow = new QHBoxLayout();
auto* plabel = new QLabel(tr("Saved to:"), this);
m_pathVal = new QLabel(tr("(not yet)"), this);
m_pathVal->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto* openBtn = new QPushButton(tr("Open folder"), this);
prow->addWidget(plabel);
prow->addWidget(m_pathVal, 1);
prow->addWidget(openBtn);
v->addLayout(prow);

// Wire up buttons
connect(m_startBtn, &QPushButton::clicked, this, [this]{
if (!m_plugin) return;
m_plugin->start();
m_pathVal->setText(m_plugin->lastPath());
m_startBtn->setEnabled(false);
m_stopBtn->setEnabled(true);
});

connect(m_stopBtn, &QPushButton::clicked, this, [this]{
if (!m_plugin) return;
m_plugin->stop();
m_pathVal->setText(m_plugin->lastPath()); // <- add this (optional)
m_startBtn->setEnabled(true);
m_stopBtn->setEnabled(false);
});


connect(openBtn, &QPushButton::clicked, this, [this]{
if (!m_plugin) return;
const auto dir = QFileInfo(m_plugin->lastPath()).absolutePath();
if (!dir.isEmpty())
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
});
}
28 changes: 28 additions & 0 deletions plugins/AudioRecorder/AudioRecorderView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include "ToolPluginView.h"
#include <QWidget>

class QPushButton;
class QLabel;

namespace lmms {
class AudioRecorder;

namespace gui {

class AudioRecorderView final : public ToolPluginView {
Q_OBJECT
public:
explicit AudioRecorderView(AudioRecorder* plugin, QWidget* parent = nullptr);
~AudioRecorderView() override = default;

private:
AudioRecorder* m_plugin{};
QPushButton* m_startBtn{};
QPushButton* m_stopBtn{};
QLabel* m_pathVal{};
};

} // namespace gui
} // namespace lmms
4 changes: 4 additions & 0 deletions plugins/AudioRecorder/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INCLUDE(BuildPlugin)
BUILD_PLUGIN(audiorecorder AudioRecorder.cpp AudioRecorderView.cpp
MOCFILES AudioRecorder.h AudioRecorderView.h
EMBEDDED_RESOURCES logo.png)
Binary file added plugins/AudioRecorder/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ include_directories("${CMAKE_BINARY_DIR}/src")
FOREACH(PLUGIN ${PLUGIN_LIST})
ADD_SUBDIRECTORY(${PLUGIN})
ENDFOREACH()


add_subdirectory(AudioRecorder)