33 , recordCommand(reactor.getCommandController())
43void AviRecorder::start(
bool recordAudio,
bool recordVideo,
bool recordMono,
44 bool recordStereo,
const Filename& filename)
56 }
else if (recordMono) {
63 warnedSampleRate =
false;
69 postProcessors.clear();
70 for (
auto* l : reactor.getDisplay().getAllLayers()) {
71 if (
auto* pp =
dynamic_cast<PostProcessor*
>(l)) {
72 postProcessors.push_back(pp);
75 if (postProcessors.empty()) {
76 throw CommandException(
77 "Current renderer doesn't support video recording.");
82 prevTime = EmuTime::infinity();
85 aviWriter = std::make_unique<AviWriter>(
86 filename, frameWidth, frameHeight,
87 (recordAudio && stereo) ? 2 : 1, sampleRate);
88 }
catch (MSXException& e) {
89 throw CommandException(
"Can't start recording: ",
94 wavWriter = std::make_unique<Wav16Writer>(
95 filename, stereo ? 2 : 1, sampleRate);
98 for (
auto* pp : postProcessors) {
99 pp->setRecorder(
this);
106 for (
auto* pp : postProcessors) {
107 pp->setRecorder(
nullptr);
109 postProcessors.clear();
119static int16_t float2int16(
float f)
126 if (data.empty())
return;
129 if (!warnedSampleRate && (mixer->
getSampleRate() != sampleRate)) {
130 warnedSampleRate =
true;
132 "Detected audio sample frequency change during "
133 "avi recording. Audio/video might get out of sync "
136 auto num = data.size();
139 wavWriter->write(data);
143 buf[2 * i + 0] = float2int16(s.left);
144 buf[2 * i + 1] = float2int16(s.right);
147 append(audioBuf, std::span{buf});
152 for (; !warnedStereo && i < num; ++i) {
153 if (data[i].left != data[i].right) {
155 "Detected stereo sound during mono recording. "
156 "Channels will be mixed down to mono. To "
157 "avoid this warning you can explicitly pass the "
158 "-mono or -stereo flag to the record command.");
162 buf[i] = float2int16(data[i].left);
164 for (; i < num; ++i) {
165 buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
169 wavWriter->write(buf);
172 append(audioBuf, std::span{buf});
181 if (!warnedFps && ((time - prevTime) != duration)) {
184 "Detected frame rate change (PAL/NTSC or frameskip) "
185 "during avi recording. Audio/video might get out of "
186 "sync because of this.");
188 }
else if (prevTime != EmuTime::infinity()) {
189 duration = time - prevTime;
190 aviWriter->setFps(narrow_cast<float>(1.0 / duration.
toDouble()));
197 aviWriter->addFrame(frame, audioBuf);
203 assert (frameHeight != 0);
207void AviRecorder::processStart(
Interpreter& interp, std::span<const TclObject> tokens,
TclObject& result)
209 std::string_view prefix =
"openmsx";
210 bool audioOnly =
false;
211 bool videoOnly =
false;
212 bool recordMono =
false;
213 bool recordStereo =
false;
214 bool doubleSize =
false;
215 bool tripleSize =
false;
218 flagArg(
"-audioonly", audioOnly),
219 flagArg(
"-videoonly", videoOnly),
221 flagArg(
"-stereo", recordStereo),
222 flagArg(
"-doublesize", doubleSize),
223 flagArg(
"-triplesize", tripleSize),
225 auto arguments =
parseTclArgs(interp, tokens.subspan(2), info);
227 if (audioOnly && videoOnly) {
228 throw CommandException(
"Can't have both -videoonly and -audioonly.");
230 if (recordStereo && recordMono) {
231 throw CommandException(
"Can't have both -mono and -stereo.");
233 if (doubleSize && tripleSize) {
234 throw CommandException(
"Can't have both -doublesize and -triplesize.");
236 if (videoOnly && (recordStereo || recordMono)) {
237 throw CommandException(
"Can't have both -videoonly and -stereo or -mono.");
239 std::string_view filenameArg;
240 switch (arguments.size()) {
245 filenameArg = arguments[0].getString();
256 }
else if (tripleSize) {
260 bool recordAudio = !videoOnly;
261 bool recordVideo = !audioOnly;
265 filenameArg, directory, prefix, extension);
267 if (aviWriter || wavWriter) {
268 result =
"Already recording.";
270 start(recordAudio, recordVideo, recordMono, recordStereo,
272 result =
tmpStrCat(
"Recording to ", filename);
276void AviRecorder::processStop(std::span<const TclObject> )
281void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
283 if (aviWriter || wavWriter) {
285 processStop(tokens.first<2>());
287 processStart(interp, tokens, result);
293 return aviWriter || wavWriter;
296void AviRecorder::status(std::span<const TclObject> ,
TclObject& result)
const
303AviRecorder::Cmd::Cmd(CommandController& commandController_)
304 : Command(commandController_,
"record")
308void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
310 if (tokens.size() < 2) {
311 throw CommandException(
"Missing argument");
313 auto& recorder =
OUTER(AviRecorder, recordCommand);
314 executeSubCommand(tokens[1].getString(),
315 "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
317 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
318 recorder.processStop(tokens); },
319 "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
321 checkNumArgs(tokens, 2, Prefix{2},
nullptr);
322 recorder.status(tokens, result); });
325std::string AviRecorder::Cmd::help(std::span<const TclObject> )
const
327 return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
328 "record start Record to file 'openmsxNNNN.avi'\n"
329 "record start <filename> Record to given file\n"
330 "record start -prefix foo Record to file 'fooNNNN.avi'\n"
331 "record stop Stop recording\n"
332 "record toggle Toggle recording (useful as keybinding)\n"
333 "record status Query recording state\n"
335 "The start subcommand also accepts an optional -audioonly, -videoonly, "
336 " -mono, -stereo, -doublesize, -triplesize flag.\n"
337 "Videos are recorded in a 320x240 size by default, at 640x480 when the "
338 "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
341void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens)
const
343 using namespace std::literals;
344 if (tokens.size() == 2) {
345 static constexpr std::array cmds = {
346 "start"sv,
"stop"sv,
"toggle"sv,
"status"sv,
348 completeString(tokens, cmds);
349 }
else if ((tokens.size() >= 3) && (tokens[1] ==
"start")) {
350 static constexpr std::array options = {
351 "-prefix"sv,
"-videoonly"sv,
"-audioonly"sv,
352 "-doublesize"sv,
"-triplesize"sv,
353 "-mono"sv,
"-stereo"sv,
void addImage(const FrameSource *frame, EmuTime::param time)
AviRecorder(Reactor &reactor)
void addWave(std::span< const StereoFloat > data)
static constexpr std::string_view AUDIO_DIR
unsigned getFrameHeight() const
static constexpr std::string_view VIDEO_DIR
static constexpr std::string_view AUDIO_EXTENSION
static constexpr std::string_view VIDEO_EXTENSION
void printWarning(std::string_view message)
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
void addDictKeyValue(const Key &key, const Value &value)
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)
const FileContext & userFileContext()
ArgsInfo flagArg(std::string_view name, bool &flag)
#define OUTER(type, member)
TemporaryString tmpStrCat(Ts &&... ts)