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