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