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 "vla.hh"
19 #include "xrange.hh"
20 #include <cassert>
21 #include <memory>
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 = std::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 = std::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 static int16_t float2int16(float f)
119 {
120  return Math::clipIntToShort(lrintf(32768.0f * f));
121 }
122 
123 void AviRecorder::addWave(unsigned num, float* fData)
124 {
125  if (!warnedSampleRate && (mixer->getSampleRate() != sampleRate)) {
126  warnedSampleRate = true;
127  reactor.getCliComm().printWarning(
128  "Detected audio sample frequency change during "
129  "avi recording. Audio/video might get out of sync "
130  "because of this.");
131  }
132  if (stereo) {
133  VLA(int16_t, buf, 2 * num);
134  for (auto i : xrange(2 * num)) {
135  buf[i] = float2int16(fData[i]);
136  }
137  if (wavWriter) {
138  wavWriter->write(buf, 2, num);
139  } else {
140  assert(aviWriter);
141  audioBuf.insert(end(audioBuf), buf, buf + 2 * num);
142  }
143  } else {
144  VLA(int16_t, buf, num);
145  unsigned i = 0;
146  for (; !warnedStereo && i < num; ++i) {
147  if (fData[2 * i + 0] != fData[2 * i + 1]) {
148  reactor.getCliComm().printWarning(
149  "Detected stereo sound during mono recording. "
150  "Channels will be mixed down to mono. To "
151  "avoid this warning you can explicitly pass the "
152  "-mono or -stereo flag to the record command.");
153  warnedStereo = true;
154  break;
155  }
156  buf[i] = float2int16(fData[2 * i]);
157  }
158  for (; i < num; ++i) {
159  buf[i] = float2int16((fData[2 * i + 0] + fData[2 * i + 1]) * 0.5f);
160  }
161 
162  if (wavWriter) {
163  wavWriter->write(buf, 1, num);
164  } else {
165  assert(aviWriter);
166  audioBuf.insert(end(audioBuf), buf, buf + num);
167  }
168  }
169 }
170 
171 void AviRecorder::addImage(FrameSource* frame, EmuTime::param time)
172 {
173  assert(!wavWriter);
174  if (duration != EmuDuration::infinity()) {
175  if (!warnedFps && ((time - prevTime) != duration)) {
176  warnedFps = true;
177  reactor.getCliComm().printWarning(
178  "Detected frame rate change (PAL/NTSC or frameskip) "
179  "during avi recording. Audio/video might get out of "
180  "sync because of this.");
181  }
182  } else if (prevTime != EmuTime::infinity()) {
183  duration = time - prevTime;
184  aviWriter->setFps(1.0 / duration.toDouble());
185  }
186  prevTime = time;
187 
188  if (mixer) {
189  mixer->updateStream(time);
190  }
191  aviWriter->addFrame(frame, unsigned(audioBuf.size()), audioBuf.data());
192  audioBuf.clear();
193 }
194 
195 // TODO: Can this be dropped?
196 unsigned AviRecorder::getFrameHeight() const {
197  assert (frameHeight != 0); // someone uses the getter too early?
198  return frameHeight;
199 }
200 
201 void AviRecorder::processStart(Interpreter& interp, span<const TclObject> tokens, TclObject& result)
202 {
203  std::string_view prefix = "openmsx";
204  bool audioOnly = false;
205  bool videoOnly = false;
206  bool recordMono = false;
207  bool recordStereo = false;
208  bool doubleSize = false;
209  bool tripleSize = false;
210  ArgsInfo info[] = {
211  valueArg("-prefix", prefix),
212  flagArg("-audioonly", audioOnly),
213  flagArg("-videoonly", videoOnly),
214  flagArg("-mono", recordMono),
215  flagArg("-stereo", recordStereo),
216  flagArg("-doublesize", doubleSize),
217  flagArg("-triplesize", tripleSize),
218  };
219  auto arguments = parseTclArgs(interp, tokens.subspan(2), info);
220 
221  if (audioOnly && videoOnly) {
222  throw CommandException("Can't have both -videoonly and -audioonly.");
223  }
224  if (recordStereo && recordMono) {
225  throw CommandException("Can't have both -mono and -stereo.");
226  }
227  if (doubleSize && tripleSize) {
228  throw CommandException("Can't have both -doublesize and -triplesize.");
229  }
230  if (videoOnly && (recordStereo || recordMono)) {
231  throw CommandException("Can't have both -videoonly and -stereo or -mono.");
232  }
233  std::string_view filenameArg;
234  switch (arguments.size()) {
235  case 0:
236  // nothing
237  break;
238  case 1:
239  filenameArg = arguments[0].getString();
240  break;
241  default:
242  throw SyntaxError();
243  }
244 
245  frameWidth = 320;
246  frameHeight = 240;
247  if (doubleSize) {
248  frameWidth *= 2;
249  frameHeight *= 2;
250  } else if (tripleSize) {
251  frameWidth *= 3;
252  frameHeight *= 3;
253  }
254  bool recordAudio = !videoOnly;
255  bool recordVideo = !audioOnly;
256  std::string_view directory = recordVideo ? "videos" : "soundlogs";
257  std::string_view extension = recordVideo ? ".avi" : ".wav";
259  filenameArg, directory, prefix, extension);
260 
261  if (aviWriter || wavWriter) {
262  result = "Already recording.";
263  } else {
264  start(recordAudio, recordVideo, recordMono, recordStereo,
265  Filename(filename));
266  result = tmpStrCat("Recording to ", filename);
267  }
268 }
269 
270 void AviRecorder::processStop(span<const TclObject> /*tokens*/)
271 {
272  stop();
273 }
274 
275 void AviRecorder::processToggle(Interpreter& interp, span<const TclObject> tokens, TclObject& result)
276 {
277  if (aviWriter || wavWriter) {
278  // drop extra tokens
279  processStop(tokens.first<2>());
280  } else {
281  processStart(interp, tokens, result);
282  }
283 }
284 
285 void AviRecorder::status(span<const TclObject> /*tokens*/, TclObject& result) const
286 {
287  result.addDictKeyValue("status", (aviWriter || wavWriter) ? "recording" : "idle");
288 }
289 
290 // class AviRecorder::Cmd
291 
292 AviRecorder::Cmd::Cmd(CommandController& commandController_)
293  : Command(commandController_, "record")
294 {
295 }
296 
297 void AviRecorder::Cmd::execute(span<const TclObject> tokens, TclObject& result)
298 {
299  if (tokens.size() < 2) {
300  throw CommandException("Missing argument");
301  }
302  auto& recorder = OUTER(AviRecorder, recordCommand);
303  executeSubCommand(tokens[1].getString(),
304  "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
305  "stop", [&]{
306  checkNumArgs(tokens, 2, Prefix{2}, nullptr);
307  recorder.processStop(tokens); },
308  "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
309  "status", [&]{
310  checkNumArgs(tokens, 2, Prefix{2}, nullptr);
311  recorder.status(tokens, result); });
312 }
313 
314 std::string AviRecorder::Cmd::help(span<const TclObject> /*tokens*/) const
315 {
316  return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
317  "record start Record to file 'openmsxNNNN.avi'\n"
318  "record start <filename> Record to given file\n"
319  "record start -prefix foo Record to file 'fooNNNN.avi'\n"
320  "record stop Stop recording\n"
321  "record toggle Toggle recording (useful as keybinding)\n"
322  "record status Query recording state\n"
323  "\n"
324  "The start subcommand also accepts an optional -audioonly, -videoonly, "
325  " -mono, -stereo, -doublesize, -triplesize flag.\n"
326  "Videos are recorded in a 320x240 size by default, at 640x480 when the "
327  "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
328 }
329 
330 void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens) const
331 {
332  using namespace std::literals;
333  if (tokens.size() == 2) {
334  static constexpr std::array cmds = {
335  "start"sv, "stop"sv, "toggle"sv, "status"sv,
336  };
337  completeString(tokens, cmds);
338  } else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
339  static constexpr std::array options = {
340  "-prefix"sv, "-videoonly"sv, "-audioonly"sv,
341  "-doublesize"sv, "-triplesize"sv,
342  "-mono"sv, "-stereo"sv,
343  };
344  completeFileName(tokens, userFileContext(), options);
345  }
346 }
347 
348 } // namespace openmsx
void addImage(FrameSource *frame, EmuTime::param time)
Definition: AviRecorder.cc:171
void addWave(unsigned num, float *data)
Definition: AviRecorder.cc:123
AviRecorder(Reactor &reactor)
Definition: AviRecorder.cc:25
unsigned getFrameHeight() const
Definition: AviRecorder.cc:196
void printWarning(std::string_view message)
Definition: CliComm.cc:10
const Layers & getAllLayers() const
Definition: Display.hh:63
static constexpr EmuDuration infinity()
Definition: EmuDuration.hh:120
constexpr double toDouble() const
Definition: EmuDuration.hh:49
This class represents a filename.
Definition: Filename.hh:18
Interface for getting lines from a video frame.
Definition: FrameSource.hh:16
void setRecorder(AviRecorder *recorder)
Definition: MSXMixer.cc:596
unsigned getSampleRate() const
Definition: MSXMixer.hh:117
bool needStereoRecording() const
Definition: MSXMixer.cc:540
void updateStream(EmuTime::param time)
Use this method to force an 'early' call to all updateBuffer() methods.
Definition: MSXMixer.cc:161
Contains the main loop of openMSX.
Definition: Reactor.hh:68
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:374
Display & getDisplay()
Definition: Reactor.hh:86
CliComm & getCliComm()
Definition: Reactor.cc:312
Definition: span.hh:126
constexpr span< element_type, Count > first() const
Definition: span.hh:246
constexpr subspan_return_t< Offset, Count > subspan() const
Definition: span.hh:266
constexpr index_type size() const noexcept
Definition: span.hh:296
int16_t clipIntToShort(int x)
Clip x to range [-32768,32767].
Definition: Math.hh:100
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
This file implemented 3 utility functions:
Definition: Autofire.cc:9
std::vector< TclObject > parseTclArgs(Interpreter &interp, span< const TclObject > inArgs, span< const ArgsInfo > table)
ArgsInfo valueArg(std::string_view name, T &value)
Definition: TclArgParser.hh:85
constexpr const char *const filename
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:173
ArgsInfo flagArg(std::string_view name, bool &flag)
Definition: TclArgParser.hh:72
#define OUTER(type, member)
Definition: outer.hh:41
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:659
#define VLA(TYPE, NAME, LENGTH)
Definition: vla.hh:10
constexpr auto xrange(T e)
Definition: xrange.hh:155
constexpr auto end(const zstring_view &x)
Definition: zstring_view.hh:84