31 , recordCommand(reactor.getCommandController())
41void AviRecorder::start(
bool recordAudio,
bool recordVideo,
bool recordMono,
42 bool recordStereo,
const Filename& filename)
54 }
else if (recordMono) {
61 warnedSampleRate =
false;
67 postProcessors.clear();
69 if (
auto* pp =
dynamic_cast<PostProcessor*
>(l)) {
70 postProcessors.push_back(pp);
73 if (postProcessors.empty()) {
74 throw CommandException(
75 "Current renderer doesn't support video recording.");
78 unsigned bpp = postProcessors.front()->getBpp();
81 prevTime = EmuTime::infinity();
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: ",
93 wavWriter = std::make_unique<Wav16Writer>(
94 filename, stereo ? 2 : 1, sampleRate);
97 for (
auto* pp : postProcessors) {
98 pp->setRecorder(
this);
105 for (
auto* pp : postProcessors) {
106 pp->setRecorder(
nullptr);
108 postProcessors.clear();
118static int16_t float2int16(
float f)
125 if (data.empty())
return;
128 if (!warnedSampleRate && (mixer->
getSampleRate() != sampleRate)) {
129 warnedSampleRate =
true;
131 "Detected audio sample frequency change during "
132 "avi recording. Audio/video might get out of sync "
135 auto num = data.size();
138 wavWriter->write(data);
140 VLA(int16_t, buf, 2 * num);
142 buf[2 * i + 0] = float2int16(s.left);
143 buf[2 * i + 1] = float2int16(s.right);
146 append(audioBuf, buf);
149 VLA(int16_t, buf, num);
151 for (; !warnedStereo && i < num; ++i) {
152 if (data[i].left != data[i].right) {
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.");
161 buf[i] = float2int16(data[i].left);
163 for (; i < num; ++i) {
164 buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
168 wavWriter->write(buf);
171 append(audioBuf, buf);
180 if (!warnedFps && ((time - prevTime) != duration)) {
183 "Detected frame rate change (PAL/NTSC or frameskip) "
184 "during avi recording. Audio/video might get out of "
185 "sync because of this.");
187 }
else if (prevTime != EmuTime::infinity()) {
188 duration = time - prevTime;
189 aviWriter->setFps(narrow_cast<float>(1.0 / duration.
toDouble()));
196 aviWriter->addFrame(frame, audioBuf);
202 assert (frameHeight != 0);
206void AviRecorder::processStart(
Interpreter& interp, std::span<const TclObject> tokens,
TclObject& result)
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;
217 flagArg(
"-audioonly", audioOnly),
218 flagArg(
"-videoonly", videoOnly),
220 flagArg(
"-stereo", recordStereo),
221 flagArg(
"-doublesize", doubleSize),
222 flagArg(
"-triplesize", tripleSize),
224 auto arguments =
parseTclArgs(interp, tokens.subspan(2), info);
226 if (audioOnly && videoOnly) {
227 throw CommandException(
"Can't have both -videoonly and -audioonly.");
229 if (recordStereo && recordMono) {
230 throw CommandException(
"Can't have both -mono and -stereo.");
232 if (doubleSize && tripleSize) {
233 throw CommandException(
"Can't have both -doublesize and -triplesize.");
235 if (videoOnly && (recordStereo || recordMono)) {
236 throw CommandException(
"Can't have both -videoonly and -stereo or -mono.");
238 std::string_view filenameArg;
239 switch (arguments.size()) {
244 filenameArg = arguments[0].getString();
255 }
else if (tripleSize) {
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);
266 if (aviWriter || wavWriter) {
267 result =
"Already recording.";
269 start(recordAudio, recordVideo, recordMono, recordStereo,
271 result =
tmpStrCat(
"Recording to ", filename);
275void AviRecorder::processStop(std::span<const TclObject> )
280void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
282 if (aviWriter || wavWriter) {
284 processStop(tokens.first<2>());
286 processStart(interp, tokens, result);
290void AviRecorder::status(std::span<const TclObject> , TclObject& result)
const
292 result.addDictKeyValue(
"status", (aviWriter || wavWriter) ?
"recording" :
"idle");
297AviRecorder::Cmd::Cmd(CommandController& commandController_)
298 : Command(commandController_,
"record")
302void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
304 if (tokens.size() < 2) {
305 throw CommandException(
"Missing argument");
307 auto& recorder =
OUTER(AviRecorder, recordCommand);
308 executeSubCommand(tokens[1].getString(),
309 "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
311 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
312 recorder.processStop(tokens); },
313 "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
315 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
316 recorder.status(tokens, result); });
319std::string AviRecorder::Cmd::help(std::span<const TclObject> )
const
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"
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.";
335void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens)
const
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,
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,
void addImage(FrameSource *frame, EmuTime::param time)
AviRecorder(Reactor &reactor)
void addWave(std::span< const StereoFloat > data)
unsigned getFrameHeight() const
void printWarning(std::string_view message)
const Layers & getAllLayers() const
static constexpr EmuDuration infinity()
constexpr double toDouble() const
This class represents a filename.
Interface for getting lines from a video frame.
void setRecorder(AviRecorder *recorder)
unsigned getSampleRate() const
bool needStereoRecording() const
void updateStream(EmuTime::param time)
Use this method to force an 'early' call to all updateBuffer() methods.
Contains the main loop of openMSX.
MSXMotherBoard * getMotherBoard() const
constexpr auto enumerate(Iterable &&iterable)
Heavily inspired by Nathan Reed's blog post: Python-Like enumerate() In C++17 http://reedbeta....
int16_t clipToInt16(T x)
Clip x to range [-32768,32767].
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:
ArgsInfo valueArg(std::string_view name, T &value)
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
FileContext userFileContext(string_view savePath)
ArgsInfo flagArg(std::string_view name, bool &flag)
#define OUTER(type, member)
TemporaryString tmpStrCat(Ts &&... ts)
#define VLA(TYPE, NAME, LENGTH)