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 final : public Command
26 {
27 public:
28  RecordCommand(CommandController& commandController, AviRecorder& recorder);
29  void execute(array_ref<TclObject> tokens, TclObject& result) override;
30  string help(const vector<string>& tokens) const override;
31  void tabCompletion(vector<string>& tokens) const override;
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(end(audioBuf), 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(end(audioBuf), 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(array_ref<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 = std::begin(tokens) + i + 1;
222  it != std::end(tokens); ++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 if (token == "-triplesize") {
244  frameWidth = 960;
245  frameHeight = 720;
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 || wavWriter) {
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(array_ref<TclObject> tokens)
288 {
289  if (tokens.size() != 2) {
290  throw SyntaxError();
291  }
292  stop();
293 }
294 
295 void AviRecorder::processToggle(array_ref<TclObject> tokens, TclObject& result)
296 {
297  if (aviWriter || wavWriter) {
298  // drop extra tokens
299  processStop(make_array_ref(tokens.data(), 2));
300  } else {
301  processStart(tokens, result);
302  }
303 }
304 
305 void AviRecorder::status(array_ref<TclObject> tokens, TclObject& result) const
306 {
307  if (tokens.size() != 2) {
308  throw SyntaxError();
309  }
310  result.addListElement("status");
311  if (aviWriter || wavWriter) {
312  result.addListElement("recording");
313  } else {
314  result.addListElement("idle");
315  }
316 
317 }
318 
319 // class RecordCommand
320 
322  AviRecorder& recorder_)
323  : Command(commandController, "record")
324  , recorder(recorder_)
325 {
326 }
327 
329 {
330  if (tokens.size() < 2) {
331  throw CommandException("Missing argument");
332  }
333  const string_ref subcommand = tokens[1].getString();
334  if (subcommand == "start") {
335  recorder.processStart(tokens, result);
336  } else if (subcommand == "stop") {
337  recorder.processStop(tokens);
338  } else if (subcommand == "toggle") {
339  recorder.processToggle(tokens, result);
340  } else if (subcommand == "status") {
341  recorder.status(tokens, result);
342  } else {
343  throw SyntaxError();
344  }
345 }
346 
347 string RecordCommand::help(const vector<string>& /*tokens*/) const
348 {
349  return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
350  "record start Record to file 'openmsxNNNN.avi'\n"
351  "record start <filename> Record to given file\n"
352  "record start -prefix foo Record to file 'fooNNNN.avi'\n"
353  "record stop Stop recording\n"
354  "record toggle Toggle recording (useful as keybinding)\n"
355  "record status Query recording state\n"
356  "\n"
357  "The start subcommand also accepts an optional -audioonly, -videoonly, "
358  " -mono, -stereo, -doublesize flag.\n"
359  "Videos are recorded in a 320x240 size by default, at 640x480 when the "
360  "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
361 }
362 
363 void RecordCommand::tabCompletion(vector<string>& tokens) const
364 {
365  if (tokens.size() == 2) {
366  static const char* const cmds[] = {
367  "start", "stop", "toggle", "status",
368  };
369  completeString(tokens, cmds);
370  } else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
371  static const char* const options[] = {
372  "-prefix", "-videoonly", "-audioonly", "-doublesize", "-triplesize",
373  "-mono", "-stereo",
374  };
375  completeFileName(tokens, UserFileContext(), options);
376  }
377 }
378 
379 } // namespace openmsx
unsigned getFrameHeight() const
Definition: AviRecorder.cc:200
Contains the main loop of openMSX.
Definition: Reactor.hh:62
void addWave(unsigned num, short *data)
Definition: AviRecorder.cc:131
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:150
size_type size() const
Definition: array_ref.hh:61
std::string str() const
Definition: string_ref.cc:12
void tabCompletion(vector< string > &tokens) const override
Attempt tab completion for this command.
Definition: AviRecorder.cc:363
unsigned getSampleRate() const
Definition: MSXMixer.hh:103
void printWarning(string_ref message)
Definition: CliComm.cc:28
bool needStereoRecording() const
Definition: MSXMixer.cc:716
bool starts_with(string_ref x) const
Definition: string_ref.cc:138
void updateStream(EmuTime::param time)
Use this method to force an 'early' call to all updateBuffer() methods.
Definition: MSXMixer.cc:227
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
RecordCommand(CommandController &commandController, AviRecorder &recorder)
Definition: AviRecorder.cc:321
Interface for getting lines from a video frame.
Definition: FrameSource.hh:15
void execute(array_ref< TclObject > tokens, TclObject &result) override
Execute this command.
Definition: AviRecorder.cc:328
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:120
const T * data() const
Definition: array_ref.hh:71
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:331
array_ref< T > make_array_ref(const T *array, size_t length)
Definition: array_ref.hh:104
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
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:7
void addImage(FrameSource *frame, EmuTime::param time)
Definition: AviRecorder.cc:175
void setRecorder(AviRecorder *recorder)
Definition: MSXMixer.cc:773
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition: Completer.hh:106
void setString(string_ref value)
Definition: TclObject.cc:65
CliComm & getCliComm()
Definition: Reactor.cc:268
Display & getDisplay()
Definition: Reactor.hh:82
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:149
#define VLA(TYPE, NAME, LENGTH)
Definition: vla.hh:10
std::unique_ptr< T > make_unique()
Definition: memory.hh:27
string help(const vector< string > &tokens) const override
Print help for this command.
Definition: AviRecorder.cc:347
const Layers & getAllLayers() const
Definition: Display.hh:62
AviRecorder(Reactor &reactor)
Definition: AviRecorder.cc:37