AviRecorder.cc 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #include "AviRecorder.hh"
  2. #include "AviWriter.hh"
  3. #include "WavWriter.hh"
  4. #include "Reactor.hh"
  5. #include "MSXMotherBoard.hh"
  6. #include "FileContext.hh"
  7. #include "CommandException.hh"
  8. #include "Display.hh"
  9. #include "PostProcessor.hh"
  10. #include "Math.hh"
  11. #include "MSXMixer.hh"
  12. #include "Filename.hh"
  13. #include "CliComm.hh"
  14. #include "FileOperations.hh"
  15. #include "TclArgParser.hh"
  16. #include "TclObject.hh"
  17. #include "outer.hh"
  18. #include "view.hh"
  19. #include "vla.hh"
  20. #include <cassert>
  21. #include <memory>
  22. using std::string;
  23. using std::vector;
  24. namespace openmsx {
  25. AviRecorder::AviRecorder(Reactor& reactor_)
  26. : reactor(reactor_)
  27. , recordCommand(reactor.getCommandController())
  28. , mixer(nullptr)
  29. , duration(EmuDuration::infinity())
  30. , prevTime(EmuTime::infinity())
  31. , frameHeight(0)
  32. {
  33. }
  34. AviRecorder::~AviRecorder()
  35. {
  36. assert(!aviWriter);
  37. assert(!wavWriter);
  38. }
  39. void AviRecorder::start(bool recordAudio, bool recordVideo, bool recordMono,
  40. bool recordStereo, const Filename& filename)
  41. {
  42. stop();
  43. MSXMotherBoard* motherBoard = reactor.getMotherBoard();
  44. if (!motherBoard) {
  45. throw CommandException("No active MSX machine.");
  46. }
  47. if (recordAudio) {
  48. mixer = &motherBoard->getMSXMixer();
  49. warnedStereo = false;
  50. if (recordStereo) {
  51. stereo = true;
  52. } else if (recordMono) {
  53. stereo = false;
  54. warnedStereo = true; // no warning if data is actually stereo
  55. } else {
  56. stereo = mixer->needStereoRecording();
  57. }
  58. sampleRate = mixer->getSampleRate();
  59. warnedSampleRate = false;
  60. }
  61. if (recordVideo) {
  62. // Set V99x8, V9990, Laserdisc, ... in record mode (when
  63. // present). Only the active one will actually send frames to
  64. // the video. This also works for Video9000.
  65. postProcessors.clear();
  66. for (auto* l : reactor.getDisplay().getAllLayers()) {
  67. if (auto* pp = dynamic_cast<PostProcessor*>(l)) {
  68. postProcessors.push_back(pp);
  69. }
  70. }
  71. if (postProcessors.empty()) {
  72. throw CommandException(
  73. "Current renderer doesn't support video recording.");
  74. }
  75. // any source is fine because they all have the same bpp
  76. unsigned bpp = postProcessors.front()->getBpp();
  77. warnedFps = false;
  78. duration = EmuDuration::infinity();
  79. prevTime = EmuTime::infinity();
  80. try {
  81. aviWriter = std::make_unique<AviWriter>(
  82. filename, frameWidth, frameHeight, bpp,
  83. (recordAudio && stereo) ? 2 : 1, sampleRate);
  84. } catch (MSXException& e) {
  85. throw CommandException("Can't start recording: ",
  86. e.getMessage());
  87. }
  88. } else {
  89. assert(recordAudio);
  90. wavWriter = std::make_unique<Wav16Writer>(
  91. filename, stereo ? 2 : 1, sampleRate);
  92. }
  93. // only set recorders when all errors are checked for
  94. for (auto* pp : postProcessors) {
  95. pp->setRecorder(this);
  96. }
  97. if (mixer) mixer->setRecorder(this);
  98. }
  99. void AviRecorder::stop()
  100. {
  101. for (auto* pp : postProcessors) {
  102. pp->setRecorder(nullptr);
  103. }
  104. postProcessors.clear();
  105. if (mixer) {
  106. mixer->setRecorder(nullptr);
  107. mixer = nullptr;
  108. }
  109. sampleRate = 0;
  110. aviWriter.reset();
  111. wavWriter.reset();
  112. }
  113. static int16_t float2int16(float f)
  114. {
  115. return Math::clipIntToShort(lrintf(32768.0f * f));
  116. }
  117. void AviRecorder::addWave(unsigned num, float* fdata)
  118. {
  119. if (!warnedSampleRate && (mixer->getSampleRate() != sampleRate)) {
  120. warnedSampleRate = true;
  121. reactor.getCliComm().printWarning(
  122. "Detected audio sample frequency change during "
  123. "avi recording. Audio/video might get out of sync "
  124. "because of this.");
  125. }
  126. if (stereo) {
  127. VLA(int16_t, buf, 2 * num);
  128. for (unsigned i = 0; i < 2 * num; ++i) {
  129. buf[i] = float2int16(fdata[i]);
  130. }
  131. if (wavWriter) {
  132. wavWriter->write(buf, 2, num);
  133. } else {
  134. assert(aviWriter);
  135. audioBuf.insert(end(audioBuf), buf, buf + 2 * num);
  136. }
  137. } else {
  138. VLA(int16_t, buf, num);
  139. unsigned i = 0;
  140. for (/**/; !warnedStereo && i < num; ++i) {
  141. if (fdata[2 * i + 0] != fdata[2 * i + 1]) {
  142. reactor.getCliComm().printWarning(
  143. "Detected stereo sound during mono recording. "
  144. "Channels will be mixed down to mono. To "
  145. "avoid this warning you can explicity pass the "
  146. "-mono or -stereo flag to the record command.");
  147. warnedStereo = true;
  148. break;
  149. }
  150. buf[i] = float2int16(fdata[2 * i]);
  151. }
  152. for (/**/; i < num; ++i) {
  153. buf[i] = float2int16((fdata[2 * i + 0] + fdata[2 * i + 1]) * 0.5f);
  154. }
  155. if (wavWriter) {
  156. wavWriter->write(buf, 1, num);
  157. } else {
  158. assert(aviWriter);
  159. audioBuf.insert(end(audioBuf), buf, buf + num);
  160. }
  161. }
  162. }
  163. void AviRecorder::addImage(FrameSource* frame, EmuTime::param time)
  164. {
  165. assert(!wavWriter);
  166. if (duration != EmuDuration::infinity()) {
  167. if (!warnedFps && ((time - prevTime) != duration)) {
  168. warnedFps = true;
  169. reactor.getCliComm().printWarning(
  170. "Detected frame rate change (PAL/NTSC or frameskip) "
  171. "during avi recording. Audio/video might get out of "
  172. "sync because of this.");
  173. }
  174. } else if (prevTime != EmuTime::infinity()) {
  175. duration = time - prevTime;
  176. aviWriter->setFps(1.0 / duration.toDouble());
  177. }
  178. prevTime = time;
  179. if (mixer) {
  180. mixer->updateStream(time);
  181. }
  182. aviWriter->addFrame(frame, unsigned(audioBuf.size()), audioBuf.data());
  183. audioBuf.clear();
  184. }
  185. // TODO: Can this be dropped?
  186. unsigned AviRecorder::getFrameHeight() const {
  187. assert (frameHeight != 0); // someone uses the getter too early?
  188. return frameHeight;
  189. }
  190. void AviRecorder::processStart(Interpreter& interp, span<const TclObject> tokens, TclObject& result)
  191. {
  192. std::string_view prefix = "openmsx";
  193. bool audioOnly = false;
  194. bool videoOnly = false;
  195. bool recordMono = false;
  196. bool recordStereo = false;
  197. bool doubleSize = false;
  198. bool tripleSize = false;
  199. ArgsInfo info[] = {
  200. valueArg("-prefix", prefix),
  201. flagArg("-audioonly", audioOnly),
  202. flagArg("-videoonly", videoOnly),
  203. flagArg("-mono", recordMono),
  204. flagArg("-stereo", recordStereo),
  205. flagArg("-doublesize", doubleSize),
  206. flagArg("-triplesize", tripleSize),
  207. };
  208. auto arguments = parseTclArgs(interp, tokens.subspan(2), info);
  209. if (audioOnly && videoOnly) {
  210. throw CommandException("Can't have both -videoonly and -audioonly.");
  211. }
  212. if (recordStereo && recordMono) {
  213. throw CommandException("Can't have both -mono and -stereo.");
  214. }
  215. if (doubleSize && tripleSize) {
  216. throw CommandException("Can't have both -doublesize and -triplesize.");
  217. }
  218. if (videoOnly && (recordStereo || recordMono)) {
  219. throw CommandException("Can't have both -videoonly and -stereo or -mono.");
  220. }
  221. std::string_view filenameArg;
  222. switch (arguments.size()) {
  223. case 0:
  224. // nothing
  225. break;
  226. case 1:
  227. filenameArg = arguments[0].getString();
  228. break;
  229. default:
  230. throw SyntaxError();
  231. }
  232. frameWidth = 320;
  233. frameHeight = 240;
  234. if (doubleSize) {
  235. frameWidth *= 2;
  236. frameHeight *= 2;
  237. } else if (tripleSize) {
  238. frameWidth *= 3;
  239. frameHeight *= 3;
  240. }
  241. bool recordAudio = !videoOnly;
  242. bool recordVideo = !audioOnly;
  243. string directory = recordVideo ? "videos" : "soundlogs";
  244. string extension = recordVideo ? ".avi" : ".wav";
  245. string filename = FileOperations::parseCommandFileArgument(
  246. filenameArg, directory, prefix, extension);
  247. if (aviWriter || wavWriter) {
  248. result = "Already recording.";
  249. } else {
  250. start(recordAudio, recordVideo, recordMono, recordStereo,
  251. Filename(filename));
  252. result = "Recording to " + filename;
  253. }
  254. }
  255. void AviRecorder::processStop(span<const TclObject> /*tokens*/)
  256. {
  257. stop();
  258. }
  259. void AviRecorder::processToggle(Interpreter& interp, span<const TclObject> tokens, TclObject& result)
  260. {
  261. if (aviWriter || wavWriter) {
  262. // drop extra tokens
  263. processStop(tokens.first<2>());
  264. } else {
  265. processStart(interp, tokens, result);
  266. }
  267. }
  268. void AviRecorder::status(span<const TclObject> /*tokens*/, TclObject& result) const
  269. {
  270. result.addDictKeyValue("status", (aviWriter || wavWriter) ? "recording" : "idle");
  271. }
  272. // class AviRecorder::Cmd
  273. AviRecorder::Cmd::Cmd(CommandController& commandController_)
  274. : Command(commandController_, "record")
  275. {
  276. }
  277. void AviRecorder::Cmd::execute(span<const TclObject> tokens, TclObject& result)
  278. {
  279. if (tokens.size() < 2) {
  280. throw CommandException("Missing argument");
  281. }
  282. auto& recorder = OUTER(AviRecorder, recordCommand);
  283. executeSubCommand(tokens[1].getString(),
  284. "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
  285. "stop", [&]{
  286. checkNumArgs(tokens, 2, Prefix{2}, nullptr);
  287. recorder.processStop(tokens); },
  288. "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
  289. "status", [&]{
  290. checkNumArgs(tokens, 2, Prefix{2}, nullptr);
  291. recorder.status(tokens, result); });
  292. }
  293. string AviRecorder::Cmd::help(const vector<string>& /*tokens*/) const
  294. {
  295. return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
  296. "record start Record to file 'openmsxNNNN.avi'\n"
  297. "record start <filename> Record to given file\n"
  298. "record start -prefix foo Record to file 'fooNNNN.avi'\n"
  299. "record stop Stop recording\n"
  300. "record toggle Toggle recording (useful as keybinding)\n"
  301. "record status Query recording state\n"
  302. "\n"
  303. "The start subcommand also accepts an optional -audioonly, -videoonly, "
  304. " -mono, -stereo, -doublesize, -triplesize flag.\n"
  305. "Videos are recorded in a 320x240 size by default, at 640x480 when the "
  306. "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
  307. }
  308. void AviRecorder::Cmd::tabCompletion(vector<string>& tokens) const
  309. {
  310. if (tokens.size() == 2) {
  311. static constexpr const char* const cmds[] = {
  312. "start", "stop", "toggle", "status",
  313. };
  314. completeString(tokens, cmds);
  315. } else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
  316. static constexpr const char* const options[] = {
  317. "-prefix", "-videoonly", "-audioonly", "-doublesize", "-triplesize",
  318. "-mono", "-stereo",
  319. };
  320. completeFileName(tokens, userFileContext(), options);
  321. }
  322. }
  323. } // namespace openmsx