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(array_ref<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(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 {
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(array_ref<TclObject> tokens)
285 {
286  if (tokens.size() != 2) {
287  throw SyntaxError();
288  }
289  stop();
290 }
291 
292 void AviRecorder::processToggle(array_ref<TclObject> tokens, TclObject& result)
293 {
294  if (aviWriter || wavWriter) {
295  // drop extra tokens
296  processStop(make_array_ref(tokens.data(), 2));
297  } else {
298  processStart(tokens, result);
299  }
300 }
301 
302 void AviRecorder::status(array_ref<TclObject> tokens, TclObject& result) const
303 {
304  if (tokens.size() != 2) {
305  throw SyntaxError();
306  }
307  result.addListElement("status");
308  if (aviWriter || wavWriter) {
309  result.addListElement("recording");
310  } else {
311  result.addListElement("idle");
312  }
313 
314 }
315 
316 // class RecordCommand
317 
319  AviRecorder& recorder_)
320  : Command(commandController, "record")
321  , recorder(recorder_)
322 {
323 }
324 
326 {
327  if (tokens.size() < 2) {
328  throw CommandException("Missing argument");
329  }
330  const string_ref subcommand = tokens[1].getString();
331  if (subcommand == "start") {
332  recorder.processStart(tokens, result);
333  } else if (subcommand == "stop") {
334  recorder.processStop(tokens);
335  } else if (subcommand == "toggle") {
336  recorder.processToggle(tokens, result);
337  } else if (subcommand == "status") {
338  recorder.status(tokens, result);
339  } else {
340  throw SyntaxError();
341  }
342 }
343 
344 string RecordCommand::help(const vector<string>& /*tokens*/) const
345 {
346  return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
347  "record start Record to file 'openmsxNNNN.avi'\n"
348  "record start <filename> Record to given file\n"
349  "record start -prefix foo Record to file 'fooNNNN.avi'\n"
350  "record stop Stop recording\n"
351  "record toggle Toggle recording (useful as keybinding)\n"
352  "record status Query recording state\n"
353  "\n"
354  "The start subcommand also accepts an optional -audioonly, -videoonly, "
355  " -mono, -stereo, -doublesize flag.\n"
356  "Videos are recorded in a 320x240 size by default and at 640x480 when the "
357  "-doublesize flag is used.";
358 }
359 
360 void RecordCommand::tabCompletion(vector<string>& tokens) const
361 {
362  if (tokens.size() == 2) {
363  static const char* const cmds[] = {
364  "start", "stop", "toggle", "status",
365  };
366  completeString(tokens, cmds);
367  } else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
368  static const char* const options[] = {
369  "-prefix", "-videoonly", "-audioonly", "-doublesize",
370  "-mono", "-stereo",
371  };
372  completeFileName(tokens, UserFileContext(), options);
373  }
374 }
375 
376 } // 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:135
size_type size() const
Definition: array_ref.hh:61
std::string str() const
Definition: string_ref.cc:10
unsigned getSampleRate() const
Definition: MSXMixer.cc:786
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:136
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
virtual void tabCompletion(vector< string > &tokens) const
Attempt tab completion for this command.
Definition: AviRecorder.cc:360
RecordCommand(CommandController &commandController, AviRecorder &recorder)
Definition: AviRecorder.cc:318
Interface for getting lines from a video frame.
Definition: FrameSource.hh:15
virtual void execute(array_ref< TclObject > tokens, TclObject &result)
Execute this command.
Definition: AviRecorder.cc:325
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:102
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
static const EmuDuration infinity
Definition: EmuDuration.hh:120
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:402
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
double toDouble() const
Definition: EmuDuration.hh:46
void addImage(FrameSource *frame, EmuTime::param time)
Definition: AviRecorder.cc:175
void setRecorder(AviRecorder *recorder)
Definition: MSXMixer.cc:778
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:55
uint8_t * data()
CliComm & getCliComm()
Definition: Reactor.cc:293
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:134
#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:344
std::unique_ptr< T > make_unique()
Definition: memory.hh:27
Display & getDisplay()
Definition: Reactor.cc:308
const Layers & getAllLayers() const
Definition: Display.hh:63
AviRecorder(Reactor &reactor)
Definition: AviRecorder.cc:37