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.");
80 prevTime = EmuTime::infinity();
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: ",
92 wavWriter = std::make_unique<Wav16Writer>(
93 filename, stereo ? 2 : 1, sampleRate);
96 for (
auto* pp : postProcessors) {
97 pp->setRecorder(
this);
104 for (
auto* pp : postProcessors) {
105 pp->setRecorder(
nullptr);
107 postProcessors.clear();
117static int16_t float2int16(
float f)
124 if (data.empty())
return;
127 if (!warnedSampleRate && (mixer->
getSampleRate() != sampleRate)) {
128 warnedSampleRate =
true;
130 "Detected audio sample frequency change during "
131 "avi recording. Audio/video might get out of sync "
134 auto num = data.size();
137 wavWriter->write(data);
139 VLA(int16_t, buf, 2 * num);
141 buf[2 * i + 0] = float2int16(s.left);
142 buf[2 * i + 1] = float2int16(s.right);
145 append(audioBuf, buf);
148 VLA(int16_t, buf, num);
150 for (; !warnedStereo && i < num; ++i) {
151 if (data[i].left != data[i].right) {
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.");
160 buf[i] = float2int16(data[i].left);
162 for (; i < num; ++i) {
163 buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
167 wavWriter->write(buf);
170 append(audioBuf, buf);
179 if (!warnedFps && ((time - prevTime) != duration)) {
182 "Detected frame rate change (PAL/NTSC or frameskip) "
183 "during avi recording. Audio/video might get out of "
184 "sync because of this.");
186 }
else if (prevTime != EmuTime::infinity()) {
187 duration = time - prevTime;
188 aviWriter->setFps(narrow_cast<float>(1.0 / duration.
toDouble()));
195 aviWriter->addFrame(frame, audioBuf);
201 assert (frameHeight != 0);
205void AviRecorder::processStart(
Interpreter& interp, std::span<const TclObject> tokens,
TclObject& result)
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;
216 flagArg(
"-audioonly", audioOnly),
217 flagArg(
"-videoonly", videoOnly),
219 flagArg(
"-stereo", recordStereo),
220 flagArg(
"-doublesize", doubleSize),
221 flagArg(
"-triplesize", tripleSize),
223 auto arguments =
parseTclArgs(interp, tokens.subspan(2), info);
225 if (audioOnly && videoOnly) {
226 throw CommandException(
"Can't have both -videoonly and -audioonly.");
228 if (recordStereo && recordMono) {
229 throw CommandException(
"Can't have both -mono and -stereo.");
231 if (doubleSize && tripleSize) {
232 throw CommandException(
"Can't have both -doublesize and -triplesize.");
234 if (videoOnly && (recordStereo || recordMono)) {
235 throw CommandException(
"Can't have both -videoonly and -stereo or -mono.");
237 std::string_view filenameArg;
238 switch (arguments.size()) {
243 filenameArg = arguments[0].getString();
254 }
else if (tripleSize) {
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);
265 if (aviWriter || wavWriter) {
266 result =
"Already recording.";
268 start(recordAudio, recordVideo, recordMono, recordStereo,
270 result =
tmpStrCat(
"Recording to ", filename);
274void AviRecorder::processStop(std::span<const TclObject> )
279void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
281 if (aviWriter || wavWriter) {
283 processStop(tokens.first<2>());
285 processStart(interp, tokens, result);
289void AviRecorder::status(std::span<const TclObject> , TclObject& result)
const
291 result.addDictKeyValue(
"status", (aviWriter || wavWriter) ?
"recording" :
"idle");
296AviRecorder::Cmd::Cmd(CommandController& commandController_)
297 : Command(commandController_,
"record")
301void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
303 if (tokens.size() < 2) {
304 throw CommandException(
"Missing argument");
306 auto& recorder =
OUTER(AviRecorder, recordCommand);
307 executeSubCommand(tokens[1].getString(),
308 "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
310 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
311 recorder.processStop(tokens); },
312 "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
314 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
315 recorder.status(tokens, result); });
318std::string AviRecorder::Cmd::help(std::span<const TclObject> )
const
320 return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
321 "record start Record to file 'openmsxNNNN.avi'\n"
322 "record start <filename> Record to given file\n"
323 "record start -prefix foo Record to file 'fooNNNN.avi'\n"
324 "record stop Stop recording\n"
325 "record toggle Toggle recording (useful as keybinding)\n"
326 "record status Query recording state\n"
328 "The start subcommand also accepts an optional -audioonly, -videoonly, "
329 " -mono, -stereo, -doublesize, -triplesize flag.\n"
330 "Videos are recorded in a 320x240 size by default, at 640x480 when the "
331 "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
334void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens)
const
336 using namespace std::literals;
337 if (tokens.size() == 2) {
338 static constexpr std::array cmds = {
339 "start"sv,
"stop"sv,
"toggle"sv,
"status"sv,
341 completeString(tokens, cmds);
342 }
else if ((tokens.size() >= 3) && (tokens[1] ==
"start")) {
343 static constexpr std::array options = {
344 "-prefix"sv,
"-videoonly"sv,
"-audioonly"sv,
345 "-doublesize"sv,
"-triplesize"sv,
346 "-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)