You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

324 lines
8.4KB

  1. #include "rack.hpp"
  2. #include "Simple.hpp"
  3. #include <utils/WavWriter.hpp>
  4. #include <utils/SimpleHelpers.hpp>
  5. #include <utils/ExtendedButton.hpp>
  6. #include <utils/LightControl.hpp>
  7. #include <utils/StateMachine.hpp>
  8. #include <utils/Memory.hpp>
  9. #include <utils/Path.hpp>
  10. #include <utils/VuMeter.hpp>
  11. #include <dsp/digital.hpp>
  12. #include <../ext/osdialog/osdialog.h>
  13. #include <iostream> // DEBUG
  14. #include <cstdlib>
  15. #include <array>
  16. #include <cmath>
  17. class Recorder : public rack::Module
  18. {
  19. public:
  20. static char const* const NoneLabel;
  21. enum InputIds
  22. {
  23. INPUT_LEFT_IN = 0,
  24. INPUT_RIGHT_IN,
  25. INPUT_START_STOP,
  26. NUM_INPUTS
  27. };
  28. enum ParamIds
  29. {
  30. PARAM_RECORD_ARM = 0,
  31. PARAM_START_STOP,
  32. PARAM_INPUT_VOLUME,
  33. PARAM_SELECT_FILE,
  34. NUM_PARAMS
  35. };
  36. enum OutputIds
  37. {
  38. OUTPUT_START_STOP = 0,
  39. OUTPUT_RECORD_ARM,
  40. NUM_OUTPUTS
  41. };
  42. enum LightIds
  43. {
  44. MAIN_LIGHT = 0,
  45. FILE_LIGHT,
  46. NUM_LIGHTS
  47. };
  48. enum StateIds
  49. {
  50. INITIAL_STATE = 0u,
  51. ARMED_STATE,
  52. RECORD_STATE
  53. };
  54. Recorder() :
  55. rack::Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS)
  56. {
  57. m_stateMachine.addState(INITIAL_STATE, [this](StateMachine& machine)
  58. {
  59. auto const armValue = params[PARAM_RECORD_ARM].value;
  60. if (hasOutputFilePath() && m_armTrigger.process(armValue))
  61. {
  62. machine.change(ARMED_STATE);
  63. }
  64. });
  65. m_stateMachine.addStateBegin(INITIAL_STATE, [this]()
  66. {
  67. m_redLightControl.setState<LightControl::StateOff>();
  68. });
  69. m_stateMachine.addState(ARMED_STATE, [this](StateMachine& machine)
  70. {
  71. auto const& startStopInput = inputs[INPUT_START_STOP];
  72. auto const armValue = params[PARAM_RECORD_ARM].value;
  73. auto const startStopValue = params[PARAM_START_STOP].value + getInputValue(startStopInput);
  74. if (m_armTrigger.process(armValue))
  75. {
  76. machine.change(INITIAL_STATE);
  77. }
  78. if (m_startStopTrigger.process(startStopValue))
  79. {
  80. machine.change(RECORD_STATE);
  81. outputs[OUTPUT_START_STOP].value = 1.f;
  82. }
  83. });
  84. m_stateMachine.addStateBegin(ARMED_STATE, [this]()
  85. {
  86. m_redLightControl.setState<LightControl::StateBlink>(0.5f, false);
  87. });
  88. m_stateMachine.addState(RECORD_STATE, [this](StateMachine& machine)
  89. {
  90. auto const& startStopInput = inputs[INPUT_START_STOP];
  91. auto const startStopValue = params[PARAM_START_STOP].value + getInputValue(startStopInput);
  92. auto const& leftInput = inputs[INPUT_LEFT_IN];
  93. auto const& rightInput = inputs[INPUT_RIGHT_IN];
  94. if (m_startStopTrigger.process(startStopValue))
  95. {
  96. machine.change(INITIAL_STATE);
  97. outputs[OUTPUT_START_STOP].value = 1.f;
  98. }
  99. WavWriter::Frame frame;
  100. frame.samples[0u] = getInputValue(leftInput) / 10.f;
  101. frame.samples[1u] = getInputValue(rightInput) / 10.f;
  102. m_writer.push(frame);
  103. if (m_writer.haveError())
  104. {
  105. // TODO: error notification
  106. std::cerr << "Recorder error: " << WavWriter::getErrorText(m_writer.error()) << std::endl;
  107. m_writer.clearError();
  108. machine.change(INITIAL_STATE);
  109. }
  110. });
  111. m_stateMachine.addStateBegin(RECORD_STATE, [this]()
  112. {
  113. startRecording();
  114. m_redLightControl.setState<LightControl::StateOn>();
  115. });
  116. m_stateMachine.addStateEnd(RECORD_STATE, [this]()
  117. {
  118. stopRecording();
  119. });
  120. m_stateMachine.change(INITIAL_STATE);
  121. }
  122. float leftInputValue()const
  123. {
  124. return inputs.at(INPUT_LEFT_IN).active ? inputs.at(INPUT_LEFT_IN).value : 0.f;
  125. }
  126. float rightInputValue()const
  127. {
  128. return inputs.at(INPUT_RIGHT_IN).active ? inputs.at(INPUT_RIGHT_IN).value : 0.f;
  129. }
  130. void setOutputFilePath(std::string const& path)
  131. {
  132. m_outputFilePath = path;
  133. }
  134. bool hasOutputFilePath()const
  135. {
  136. return !m_outputFilePath.empty();
  137. }
  138. bool isArmed()const
  139. {
  140. return m_stateMachine.currentIndex() == ARMED_STATE;
  141. }
  142. bool isRecording()const
  143. {
  144. return m_writer.isRunning();
  145. }
  146. void startRecording()
  147. {
  148. m_writer.start(m_outputFilePath);
  149. }
  150. void stopRecording()
  151. {
  152. m_writer.stop();
  153. }
  154. void onSampleRateChange() override
  155. {
  156. m_writer.stop();
  157. // TODO: error notification
  158. std::cerr << "Recorder error: the sample rate has changed during the recording" << std::endl;
  159. }
  160. void step() override
  161. {
  162. outputs.at(OUTPUT_START_STOP).value = 0.f;
  163. m_stateMachine.step();
  164. m_redLightControl.step();
  165. lights.at(FILE_LIGHT).value = (m_outputFilePath.empty() || m_outputFilePath == Recorder::NoneLabel) ? 0.f : 1.f;
  166. lights.at(MAIN_LIGHT).value = m_redLightControl.lightValue();
  167. }
  168. private:
  169. WavWriter m_writer;
  170. StateMachine m_stateMachine;
  171. LightControl m_redLightControl;
  172. rack::SchmittTrigger m_startStopTrigger;
  173. rack::SchmittTrigger m_armTrigger;
  174. std::string m_outputFilePath;
  175. };
  176. char const* const Recorder::NoneLabel = "<none>";
  177. namespace Helpers
  178. {
  179. template <class InputPortClass>
  180. static rack::Port* addAudioInput(rack::ModuleWidget* const widget, rack::Module* const module,
  181. int const inputId, rack::Vec const& position,
  182. std::string const& label)
  183. {
  184. auto* const port = rack::createInput<InputPortClass>(position, module, inputId);
  185. auto* const labelWidget = new rack::Label;
  186. labelWidget->text = label;
  187. widget->addInput(port);
  188. widget->addChild(labelWidget);
  189. float const portSize = port->box.size.x;
  190. labelWidget->box.pos.x = position.x;
  191. labelWidget->box.pos.y = position.y + portSize;
  192. return port;
  193. }
  194. }
  195. RecorderWidget::RecorderWidget() :
  196. m_recorder(new Recorder),
  197. m_label(new rack::Label),
  198. m_leftMeter(new VuMeter({20.f, 180.f}, {15.f, 130.f})),
  199. m_rightMeter(new VuMeter({15.f * 6.f - 15.f - 20.f, 180.f}, {15.f, 130.f}))
  200. {
  201. static constexpr float const PortSize = 24.6146f;
  202. static constexpr float const Spacing = 10.f;
  203. static constexpr float const Width = 15.f * 6.f;
  204. auto* const mainPanel = new rack::SVGPanel;
  205. box.size = rack::Vec(15 * 6, 380);
  206. mainPanel->box.size = box.size;
  207. mainPanel->setBackground(rack::SVG::load(rack::assetPlugin(plugin, "res/recorder.svg")));
  208. addChild(mainPanel);
  209. addChild(rack::createScrew<rack::ScrewSilver>({15, 0}));
  210. addChild(rack::createScrew<rack::ScrewSilver>({box.size.x - 30, 0}));
  211. addChild(rack::createScrew<rack::ScrewSilver>({15, box.size.y - 15}));
  212. addChild(rack::createScrew<rack::ScrewSilver>({box.size.x - 30, box.size.y - 15}));
  213. addChild(m_leftMeter);
  214. addChild(m_rightMeter);
  215. setModule(m_recorder);
  216. {
  217. static constexpr float const Left = (Width - (PortSize * 2.f + Spacing)) / 2.f;
  218. Helpers::addAudioInput<rack::PJ301MPort>(this, m_recorder, Recorder::INPUT_LEFT_IN, {Left, 315}, "L");
  219. Helpers::addAudioInput<rack::PJ301MPort>(this, m_recorder, Recorder::INPUT_RIGHT_IN, {Left + Spacing + PortSize, 315}, "R");
  220. }
  221. static constexpr float const Top = 90;
  222. auto* const selectFileButton = createParam<ExtendedButton<rack::LEDButton>>({10, Top - 30}, Recorder::PARAM_SELECT_FILE, 0.f, 1.f, 0.f);
  223. createParam<rack::LEDButton>({10, Top}, Recorder::PARAM_RECORD_ARM, 0.f, 1.f, 0.f);
  224. createParam<rack::LEDButton>({40, Top}, Recorder::PARAM_START_STOP, 0.f, 1.f, 0.f);
  225. createInput<rack::PJ301MPort>({37, Top + 25}, Recorder::INPUT_START_STOP);
  226. createOutput<rack::PJ301MPort>({37, Top + 55}, Recorder::OUTPUT_START_STOP);
  227. createLight<rack::SmallLight<rack::RedLight>>(rack::Vec{68, Top + 6}, Recorder::MAIN_LIGHT);
  228. createLight<rack::TinyLight<rack::GreenLight>>(rack::Vec{16.5f, Top - 23.5f}, Recorder::FILE_LIGHT);
  229. m_label->text = Recorder::NoneLabel;
  230. m_label->box.pos.x = 22;
  231. m_label->box.pos.y = Top - 32;
  232. selectFileButton->setCallback(std::bind(&RecorderWidget::onSelectFileButtonClicked, this));
  233. addChild(m_label);
  234. }
  235. void RecorderWidget::onSelectFileButtonClicked()
  236. {
  237. std::cout << "armed: " << m_recorder->isArmed() << std::endl;
  238. std::cout << "recording: " << m_recorder->isRecording() << std::endl;
  239. if (!m_recorder->isArmed() && !m_recorder->isRecording())
  240. {
  241. selectOutputFile();
  242. }
  243. }
  244. bool RecorderWidget::selectOutputFile()
  245. {
  246. std::unique_ptr<char[], FreeDeleter<char>> path{osdialog_file(OSDIALOG_SAVE, ".", "output.wav", nullptr)};
  247. bool result = false;
  248. if (path)
  249. {
  250. std::string pathStr{path.get()};
  251. if (Path::extractExtension(pathStr).empty())
  252. {
  253. pathStr.append(".wav");
  254. }
  255. setOutputFilePath(pathStr);
  256. result = true;
  257. }
  258. else
  259. {
  260. setOutputFilePath(Recorder::NoneLabel);
  261. }
  262. return result;
  263. }
  264. void RecorderWidget::setOutputFilePath(std::string const& outputFilePath)
  265. {
  266. m_recorder->setOutputFilePath(outputFilePath);
  267. m_label->text = Path::extractFileName(outputFilePath);
  268. }
  269. void RecorderWidget::step()
  270. {
  271. m_leftMeter->setValue(m_recorder->leftInputValue());
  272. m_rightMeter->setValue(m_recorder->rightInputValue());
  273. ExtendedModuleWidget::step();
  274. }