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