openMSX
AviRecorder.cc
Go to the documentation of this file.
1#include "AviRecorder.hh"
2#include "AviWriter.hh"
3#include "Mixer.hh"
4#include "WavWriter.hh"
5#include "Reactor.hh"
6#include "MSXMotherBoard.hh"
7#include "FileContext.hh"
8#include "CommandException.hh"
9#include "Display.hh"
10#include "PostProcessor.hh"
11#include "Math.hh"
12#include "MSXMixer.hh"
13#include "Filename.hh"
14#include "CliComm.hh"
15#include "FileOperations.hh"
16#include "TclArgParser.hh"
17#include "TclObject.hh"
18#include "enumerate.hh"
19#include "narrow.hh"
20#include "outer.hh"
21#include "vla.hh"
22#include "xrange.hh"
23#include <array>
24#include <cassert>
25#include <memory>
26
27namespace openmsx {
28
30 : reactor(reactor_)
31 , recordCommand(reactor.getCommandController())
32{
33}
34
36{
37 assert(!aviWriter);
38 assert(!wavWriter);
39}
40
41void 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 warnedFps = false;
79 duration = EmuDuration::infinity();
80 prevTime = EmuTime::infinity();
81
82 try {
83 aviWriter = std::make_unique<AviWriter>(
84 filename, frameWidth, frameHeight,
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 = std::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
117static int16_t float2int16(float f)
118{
119 return Math::clipToInt16(lrintf(32768.0f * f));
120}
121
122void AviRecorder::addWave(std::span<const StereoFloat> data)
123{
124 if (data.empty()) return;
125
126 assert(mixer);
127 if (!warnedSampleRate && (mixer->getSampleRate() != sampleRate)) {
128 warnedSampleRate = true;
129 reactor.getCliComm().printWarning(
130 "Detected audio sample frequency change during "
131 "avi recording. Audio/video might get out of sync "
132 "because of this.");
133 }
134 auto num = data.size();
135 if (stereo) {
136 if (wavWriter) {
137 wavWriter->write(data);
138 } else {
139 VLA(int16_t, buf, 2 * num);
140 for (auto [i, s] : enumerate(data)) {
141 buf[2 * i + 0] = float2int16(s.left);
142 buf[2 * i + 1] = float2int16(s.right);
143 }
144 assert(aviWriter);
145 append(audioBuf, buf);
146 }
147 } else {
148 VLA(int16_t, buf, num);
149 size_t i = 0;
150 for (; !warnedStereo && i < num; ++i) {
151 if (data[i].left != data[i].right) {
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 explicitly pass the "
156 "-mono or -stereo flag to the record command.");
157 warnedStereo = true;
158 break;
159 }
160 buf[i] = float2int16(data[i].left);
161 }
162 for (; i < num; ++i) {
163 buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
164 }
165
166 if (wavWriter) {
167 wavWriter->write(buf);
168 } else {
169 assert(aviWriter);
170 append(audioBuf, buf);
171 }
172 }
173}
174
175void AviRecorder::addImage(FrameSource* frame, EmuTime::param time)
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(narrow_cast<float>(1.0 / duration.toDouble()));
189 }
190 prevTime = time;
191
192 if (mixer) {
193 mixer->updateStream(time);
194 }
195 aviWriter->addFrame(frame, audioBuf);
196 audioBuf.clear();
197}
198
199// TODO: Can this be dropped?
201 assert (frameHeight != 0); // someone uses the getter too early?
202 return frameHeight;
203}
204
205void AviRecorder::processStart(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
206{
207 std::string_view prefix = "openmsx";
208 bool audioOnly = false;
209 bool videoOnly = false;
210 bool recordMono = false;
211 bool recordStereo = false;
212 bool doubleSize = false;
213 bool tripleSize = false;
214 std::array info = {
215 valueArg("-prefix", prefix),
216 flagArg("-audioonly", audioOnly),
217 flagArg("-videoonly", videoOnly),
218 flagArg("-mono", recordMono),
219 flagArg("-stereo", recordStereo),
220 flagArg("-doublesize", doubleSize),
221 flagArg("-triplesize", tripleSize),
222 };
223 auto arguments = parseTclArgs(interp, tokens.subspan(2), info);
224
225 if (audioOnly && videoOnly) {
226 throw CommandException("Can't have both -videoonly and -audioonly.");
227 }
228 if (recordStereo && recordMono) {
229 throw CommandException("Can't have both -mono and -stereo.");
230 }
231 if (doubleSize && tripleSize) {
232 throw CommandException("Can't have both -doublesize and -triplesize.");
233 }
234 if (videoOnly && (recordStereo || recordMono)) {
235 throw CommandException("Can't have both -videoonly and -stereo or -mono.");
236 }
237 std::string_view filenameArg;
238 switch (arguments.size()) {
239 case 0:
240 // nothing
241 break;
242 case 1:
243 filenameArg = arguments[0].getString();
244 break;
245 default:
246 throw SyntaxError();
247 }
248
249 frameWidth = 320;
250 frameHeight = 240;
251 if (doubleSize) {
252 frameWidth *= 2;
253 frameHeight *= 2;
254 } else if (tripleSize) {
255 frameWidth *= 3;
256 frameHeight *= 3;
257 }
258 bool recordAudio = !videoOnly;
259 bool recordVideo = !audioOnly;
260 std::string_view directory = recordVideo ? "videos" : "soundlogs";
261 std::string_view extension = recordVideo ? ".avi" : ".wav";
263 filenameArg, directory, prefix, extension);
264
265 if (aviWriter || wavWriter) {
266 result = "Already recording.";
267 } else {
268 start(recordAudio, recordVideo, recordMono, recordStereo,
269 Filename(filename));
270 result = tmpStrCat("Recording to ", filename);
271 }
272}
273
274void AviRecorder::processStop(std::span<const TclObject> /*tokens*/)
275{
276 stop();
277}
278
279void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
280{
281 if (aviWriter || wavWriter) {
282 // drop extra tokens
283 processStop(tokens.first<2>());
284 } else {
285 processStart(interp, tokens, result);
286 }
287}
288
290{
291 return aviWriter || wavWriter;
292}
293
294void AviRecorder::status(std::span<const TclObject> /*tokens*/, TclObject& result) const
295{
296 result.addDictKeyValue("status", isRecording() ? "recording" : "idle");
297}
298
299// class AviRecorder::Cmd
300
301AviRecorder::Cmd::Cmd(CommandController& commandController_)
302 : Command(commandController_, "record")
303{
304}
305
306void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
307{
308 if (tokens.size() < 2) {
309 throw CommandException("Missing argument");
310 }
311 auto& recorder = OUTER(AviRecorder, recordCommand);
312 executeSubCommand(tokens[1].getString(),
313 "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
314 "stop", [&]{
315 checkNumArgs(tokens, 2, Prefix{2}, nullptr);
316 recorder.processStop(tokens); },
317 "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
318 "status", [&]{
319 checkNumArgs(tokens, 2, Prefix{2}, nullptr);
320 recorder.status(tokens, result); });
321}
322
323std::string AviRecorder::Cmd::help(std::span<const TclObject> /*tokens*/) const
324{
325 return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
326 "record start Record to file 'openmsxNNNN.avi'\n"
327 "record start <filename> Record to given file\n"
328 "record start -prefix foo Record to file 'fooNNNN.avi'\n"
329 "record stop Stop recording\n"
330 "record toggle Toggle recording (useful as keybinding)\n"
331 "record status Query recording state\n"
332 "\n"
333 "The start subcommand also accepts an optional -audioonly, -videoonly, "
334 " -mono, -stereo, -doublesize, -triplesize flag.\n"
335 "Videos are recorded in a 320x240 size by default, at 640x480 when the "
336 "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
337}
338
339void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens) const
340{
341 using namespace std::literals;
342 if (tokens.size() == 2) {
343 static constexpr std::array cmds = {
344 "start"sv, "stop"sv, "toggle"sv, "status"sv,
345 };
346 completeString(tokens, cmds);
347 } else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
348 static constexpr std::array options = {
349 "-prefix"sv, "-videoonly"sv, "-audioonly"sv,
350 "-doublesize"sv, "-triplesize"sv,
351 "-mono"sv, "-stereo"sv,
352 };
353 completeFileName(tokens, userFileContext(), options);
354 }
355}
356
357} // namespace openmsx
void addImage(FrameSource *frame, EmuTime::param time)
AviRecorder(Reactor &reactor)
void addWave(std::span< const StereoFloat > data)
unsigned getFrameHeight() const
bool isRecording() const
void printWarning(std::string_view message)
Definition CliComm.cc:10
static constexpr EmuDuration infinity()
constexpr double toDouble() const
This class represents a filename.
Definition Filename.hh:18
Interface for getting lines from a video frame.
void setRecorder(AviRecorder *recorder)
Definition MSXMixer.cc:657
unsigned getSampleRate() const
Definition MSXMixer.hh:134
bool needStereoRecording() const
Definition MSXMixer.cc:601
void updateStream(EmuTime::param time)
Use this method to force an 'early' call to all updateBuffer() methods.
Definition MSXMixer.cc:159
Contains the main loop of openMSX.
Definition Reactor.hh:72
MSXMotherBoard * getMotherBoard() const
Definition Reactor.cc:409
CliComm & getCliComm()
Definition Reactor.cc:324
void addDictKeyValue(const Key &key, const Value &value)
Definition TclObject.hh:141
constexpr auto enumerate(Iterable &&iterable)
Heavily inspired by Nathan Reed's blog post: Python-Like enumerate() In C++17 http://reedbeta....
Definition enumerate.hh:28
int16_t clipToInt16(T x)
Clip x to range [-32768,32767].
Definition Math.hh:47
constexpr double e
Definition Math.hh:21
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
ArgsInfo valueArg(std::string_view name, T &value)
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
const FileContext & userFileContext()
ArgsInfo flagArg(std::string_view name, bool &flag)
#define OUTER(type, member)
Definition outer.hh:41
TemporaryString tmpStrCat(Ts &&... ts)
Definition strCat.hh:742
#define VLA(TYPE, NAME, LENGTH)
Definition vla.hh:12