31 , recordCommand(reactor.getCommandController())
34 , prevTime(EmuTime::infinity())
45 void AviRecorder::start(
bool recordAudio,
bool recordVideo,
bool recordMono,
58 }
else if (recordMono) {
65 warnedSampleRate =
false;
71 postProcessors.clear();
73 if (
auto* pp =
dynamic_cast<PostProcessor*
>(l)) {
74 postProcessors.push_back(pp);
77 if (postProcessors.empty()) {
78 throw CommandException(
79 "Current renderer doesn't support video recording.");
82 unsigned bpp = postProcessors.front()->getBpp();
85 prevTime = EmuTime::infinity();
88 aviWriter = std::make_unique<AviWriter>(
89 filename, frameWidth, frameHeight, bpp,
90 (recordAudio && stereo) ? 2 : 1, sampleRate);
91 }
catch (MSXException& e) {
92 throw CommandException(
"Can't start recording: ",
97 wavWriter = std::make_unique<Wav16Writer>(
98 filename, stereo ? 2 : 1, sampleRate);
101 for (
auto* pp : postProcessors) {
102 pp->setRecorder(
this);
109 for (
auto* pp : postProcessors) {
110 pp->setRecorder(
nullptr);
112 postProcessors.clear();
122 static int16_t float2int16(
float f)
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 "
137 VLA(int16_t, buf, 2 * num);
138 for (
auto i :
xrange(2 * num)) {
139 buf[i] = float2int16(fData[i]);
142 wavWriter->write(buf, 2, num);
145 audioBuf.insert(
end(audioBuf), buf, buf + 2 * num);
148 VLA(int16_t, buf, num);
150 for (; !warnedStereo && i < num; ++i) {
151 if (fData[2 * i + 0] != fData[2 * i + 1]) {
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(fData[2 * i]);
162 for (; i < num; ++i) {
163 buf[i] = float2int16((fData[2 * i + 0] + fData[2 * i + 1]) * 0.5f);
167 wavWriter->write(buf, 1, num);
170 audioBuf.insert(
end(audioBuf), buf, buf + num);
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(1.0 / duration.
toDouble());
195 aviWriter->addFrame(frame,
unsigned(audioBuf.size()), audioBuf.data());
201 assert (frameHeight != 0);
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),
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,
279 void AviRecorder::processToggle(Interpreter& interp,
span<const TclObject> tokens, TclObject& result)
281 if (aviWriter || wavWriter) {
283 processStop(tokens.
first<2>());
285 processStart(interp, tokens, result);
291 result.addDictKeyValue(
"status", (aviWriter || wavWriter) ?
"recording" :
"idle");
296 AviRecorder::Cmd::Cmd(CommandController& commandController_)
297 : Command(commandController_,
"record")
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); });
318 string AviRecorder::Cmd::help(
const vector<string>& )
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.";
334 void AviRecorder::Cmd::tabCompletion(vector<string>& tokens)
const
336 if (tokens.size() == 2) {
337 static constexpr
const char*
const cmds[] = {
338 "start",
"stop",
"toggle",
"status",
340 completeString(tokens, cmds);
341 }
else if ((tokens.size() >= 3) && (tokens[1] ==
"start")) {
342 static constexpr
const char*
const options[] = {
343 "-prefix",
"-videoonly",
"-audioonly",
"-doublesize",
"-triplesize",