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