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