#include "rack.hpp" #include "Simple.hpp" #include #include #include #include #include #include #include #include #include #include <../ext/osdialog/osdialog.h> #include // DEBUG #include #include #include class Recorder : public rack::Module { public: static char const* const NoneLabel; enum InputIds { INPUT_LEFT_IN = 0, INPUT_RIGHT_IN, INPUT_START_STOP, NUM_INPUTS }; enum ParamIds { PARAM_RECORD_ARM = 0, PARAM_START_STOP, PARAM_INPUT_VOLUME, PARAM_SELECT_FILE, NUM_PARAMS }; enum OutputIds { OUTPUT_START_STOP = 0, OUTPUT_RECORD_ARM, NUM_OUTPUTS }; enum LightIds { MAIN_LIGHT = 0, FILE_LIGHT, NUM_LIGHTS }; enum StateIds { INITIAL_STATE = 0u, ARMED_STATE, RECORD_STATE }; Recorder() : rack::Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { m_stateMachine.addState(INITIAL_STATE, [this](StateMachine& machine) { auto const armValue = params[PARAM_RECORD_ARM].value; if (hasOutputFilePath() && m_armTrigger.process(armValue)) { machine.change(ARMED_STATE); } }); m_stateMachine.addStateBegin(INITIAL_STATE, [this]() { m_redLightControl.setState(); }); m_stateMachine.addState(ARMED_STATE, [this](StateMachine& machine) { auto const& startStopInput = inputs[INPUT_START_STOP]; auto const armValue = params[PARAM_RECORD_ARM].value; auto const startStopValue = params[PARAM_START_STOP].value + getInputValue(startStopInput); if (m_armTrigger.process(armValue)) { machine.change(INITIAL_STATE); } if (m_startStopTrigger.process(startStopValue)) { machine.change(RECORD_STATE); outputs[OUTPUT_START_STOP].value = 1.f; } }); m_stateMachine.addStateBegin(ARMED_STATE, [this]() { m_redLightControl.setState(0.5f, false); }); m_stateMachine.addState(RECORD_STATE, [this](StateMachine& machine) { auto const& startStopInput = inputs[INPUT_START_STOP]; auto const startStopValue = params[PARAM_START_STOP].value + getInputValue(startStopInput); auto const& leftInput = inputs[INPUT_LEFT_IN]; auto const& rightInput = inputs[INPUT_RIGHT_IN]; if (m_startStopTrigger.process(startStopValue)) { machine.change(INITIAL_STATE); outputs[OUTPUT_START_STOP].value = 1.f; } WavWriter::Frame frame; frame.samples[0u] = getInputValue(leftInput) / 10.f; frame.samples[1u] = getInputValue(rightInput) / 10.f; m_writer.push(frame); if (m_writer.haveError()) { // TODO: error notification std::cerr << "Recorder error: " << WavWriter::getErrorText(m_writer.error()) << std::endl; m_writer.clearError(); machine.change(INITIAL_STATE); } }); m_stateMachine.addStateBegin(RECORD_STATE, [this]() { startRecording(); m_redLightControl.setState(); }); m_stateMachine.addStateEnd(RECORD_STATE, [this]() { stopRecording(); }); m_stateMachine.change(INITIAL_STATE); } float leftInputValue()const { return inputs.at(INPUT_LEFT_IN).active ? inputs.at(INPUT_LEFT_IN).value : 0.f; } float rightInputValue()const { return inputs.at(INPUT_RIGHT_IN).active ? inputs.at(INPUT_RIGHT_IN).value : 0.f; } void setOutputFilePath(std::string const& path) { m_outputFilePath = path; } bool hasOutputFilePath()const { return !m_outputFilePath.empty(); } bool isArmed()const { return m_stateMachine.currentIndex() == ARMED_STATE; } bool isRecording()const { return m_writer.isRunning(); } void startRecording() { m_writer.start(m_outputFilePath); } void stopRecording() { m_writer.stop(); } void onSampleRateChange() override { m_writer.stop(); // TODO: error notification std::cerr << "Recorder error: the sample rate has changed during the recording" << std::endl; } void step() override { outputs.at(OUTPUT_START_STOP).value = 0.f; m_stateMachine.step(); m_redLightControl.step(); lights.at(FILE_LIGHT).value = (m_outputFilePath.empty() || m_outputFilePath == Recorder::NoneLabel) ? 0.f : 1.f; lights.at(MAIN_LIGHT).value = m_redLightControl.lightValue(); } private: WavWriter m_writer; StateMachine m_stateMachine; LightControl m_redLightControl; rack::SchmittTrigger m_startStopTrigger; rack::SchmittTrigger m_armTrigger; std::string m_outputFilePath; }; char const* const Recorder::NoneLabel = ""; namespace Helpers { template static rack::Port* addAudioInput(rack::ModuleWidget* const widget, rack::Module* const module, int const inputId, rack::Vec const& position, std::string const& label) { auto* const port = rack::createInput(position, module, inputId); auto* const labelWidget = new rack::Label; labelWidget->text = label; widget->addInput(port); widget->addChild(labelWidget); float const portSize = port->box.size.x; labelWidget->box.pos.x = position.x; labelWidget->box.pos.y = position.y + portSize; return port; } } RecorderWidget::RecorderWidget() : m_recorder(new Recorder), m_label(new rack::Label), m_leftMeter(new VuMeter({20.f, 180.f}, {15.f, 130.f})), m_rightMeter(new VuMeter({15.f * 6.f - 15.f - 20.f, 180.f}, {15.f, 130.f})) { static constexpr float const PortSize = 24.6146f; static constexpr float const Spacing = 10.f; static constexpr float const Width = 15.f * 6.f; auto* const mainPanel = new rack::SVGPanel; box.size = rack::Vec(15 * 6, 380); mainPanel->box.size = box.size; mainPanel->setBackground(rack::SVG::load(rack::assetPlugin(plugin, "res/recorder.svg"))); addChild(mainPanel); addChild(rack::createScrew({15, 0})); addChild(rack::createScrew({box.size.x - 30, 0})); addChild(rack::createScrew({15, box.size.y - 15})); addChild(rack::createScrew({box.size.x - 30, box.size.y - 15})); addChild(m_leftMeter); addChild(m_rightMeter); setModule(m_recorder); { static constexpr float const Left = (Width - (PortSize * 2.f + Spacing)) / 2.f; Helpers::addAudioInput(this, m_recorder, Recorder::INPUT_LEFT_IN, {Left, 315}, "L"); Helpers::addAudioInput(this, m_recorder, Recorder::INPUT_RIGHT_IN, {Left + Spacing + PortSize, 315}, "R"); } static constexpr float const Top = 90; auto* const selectFileButton = createParam>({10, Top - 30}, Recorder::PARAM_SELECT_FILE, 0.f, 1.f, 0.f); createParam({10, Top}, Recorder::PARAM_RECORD_ARM, 0.f, 1.f, 0.f); createParam({40, Top}, Recorder::PARAM_START_STOP, 0.f, 1.f, 0.f); createInput({37, Top + 25}, Recorder::INPUT_START_STOP); createOutput({37, Top + 55}, Recorder::OUTPUT_START_STOP); createLight>(rack::Vec{68, Top + 6}, Recorder::MAIN_LIGHT); createLight>(rack::Vec{16.5f, Top - 23.5f}, Recorder::FILE_LIGHT); m_label->text = Recorder::NoneLabel; m_label->box.pos.x = 22; m_label->box.pos.y = Top - 32; selectFileButton->setCallback(std::bind(&RecorderWidget::onSelectFileButtonClicked, this)); addChild(m_label); } void RecorderWidget::onSelectFileButtonClicked() { std::cout << "armed: " << m_recorder->isArmed() << std::endl; std::cout << "recording: " << m_recorder->isRecording() << std::endl; if (!m_recorder->isArmed() && !m_recorder->isRecording()) { selectOutputFile(); } } bool RecorderWidget::selectOutputFile() { std::unique_ptr> path{osdialog_file(OSDIALOG_SAVE, ".", "output.wav", nullptr)}; bool result = false; if (path) { std::string pathStr{path.get()}; if (Path::extractExtension(pathStr).empty()) { pathStr.append(".wav"); } setOutputFilePath(pathStr); result = true; } else { setOutputFilePath(Recorder::NoneLabel); } return result; } void RecorderWidget::setOutputFilePath(std::string const& outputFilePath) { m_recorder->setOutputFilePath(outputFilePath); m_label->text = Path::extractFileName(outputFilePath); } void RecorderWidget::step() { m_leftMeter->setValue(m_recorder->leftInputValue()); m_rightMeter->setValue(m_recorder->rightInputValue()); ExtendedModuleWidget::step(); }