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