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