33 virtual string help(
const vector<string>& tokens)
const;
43 reactor.getCommandController(), *this))
53 assert(!aviWriter.get());
54 assert(!wavWriter.get());
57 void AviRecorder::start(
bool recordAudio,
bool recordVideo,
bool recordMono,
58 bool recordStereo,
const Filename& filename)
70 }
else if (recordMono) {
77 warnedSampleRate =
false;
83 postProcessors.clear();
85 if (
auto* pp = dynamic_cast<PostProcessor*>(l)) {
86 postProcessors.push_back(pp);
89 if (postProcessors.empty()) {
90 throw CommandException(
91 "Current renderer doesn't support video recording.");
94 unsigned bpp = postProcessors.front()->getBpp();
100 aviWriter = make_unique<AviWriter>(
101 filename, frameWidth, frameHeight, bpp,
102 (recordAudio && stereo) ? 2 : 1, sampleRate);
103 }
catch (MSXException& e) {
104 throw CommandException(
"Can't start recording: " +
109 wavWriter = make_unique<Wav16Writer>(
110 filename, stereo ? 2 : 1, sampleRate);
113 for (
auto* pp : postProcessors) {
114 pp->setRecorder(
this);
121 for (
auto* pp : postProcessors) {
122 pp->setRecorder(
nullptr);
124 postProcessors.clear();
136 if (!warnedSampleRate && (mixer->
getSampleRate() != sampleRate)) {
137 warnedSampleRate =
true;
139 "Detected audio sample frequency change during "
140 "avi recording. Audio/video might get out of sync "
144 if (wavWriter.get()) {
145 wavWriter->write(data, 2, num);
147 assert(aviWriter.get());
148 audioBuf.insert(audioBuf.end(), data, data + 2 * num);
151 VLA(
short, buf, num);
153 for (; !warnedStereo && i < num; ++i) {
154 if (data[2 * i + 0] != data[2 * i + 1]) {
156 "Detected stereo sound during mono recording. "
157 "Channels will be mixed down to mono. To "
158 "avoid this warning you can explicity pass the "
159 "-mono or -stereo flag to the record command.");
163 buf[i] = data[2 * i];
165 for (; i < num; ++i) {
166 buf[i] = (int(data[2 * i + 0]) + int(data[2 * i + 1])) / 2;
169 if (wavWriter.get()) {
170 wavWriter->write(buf, 1, num);
172 assert(aviWriter.get());
173 audioBuf.insert(audioBuf.end(), buf, buf + num);
180 assert(!wavWriter.get());
182 if (!warnedFps && ((time - prevTime) != duration)) {
185 "Detected frame rate change (PAL/NTSC or frameskip) "
186 "during avi recording. Audio/video might get out of "
187 "sync because of this.");
190 duration = time - prevTime;
191 aviWriter->setFps(1.0 / duration.
toDouble());
198 aviWriter->addFrame(frame,
unsigned(audioBuf.size()), audioBuf.data());
204 assert (frameHeight != 0);
208 void AviRecorder::processStart(
const vector<TclObject>& tokens,
TclObject& result)
211 string prefix =
"openmsx";
212 bool recordAudio =
true;
213 bool recordVideo =
true;
214 bool recordMono =
false;
215 bool recordStereo =
false;
219 vector<string> arguments;
220 for (
unsigned i = 2; i < tokens.size(); ++i) {
224 for (
auto it = tokens.begin() + i + 1;
225 it != tokens.end(); ++it) {
226 arguments.push_back(it->getString().str());
230 if (token ==
"-prefix") {
231 if (++i == tokens.size()) {
232 throw CommandException(
"Missing argument");
234 prefix = tokens[i].getString().str();
235 }
else if (token ==
"-audioonly") {
237 }
else if (token ==
"-mono") {
239 }
else if (token ==
"-stereo") {
241 }
else if (token ==
"-videoonly") {
243 }
else if (token ==
"-doublesize") {
247 throw CommandException(
"Invalid option: " + token);
250 arguments.push_back(token.
str());
253 if (!recordAudio && !recordVideo) {
254 throw CommandException(
"Can't have both -videoonly and -audioonly.");
256 if (recordStereo && recordMono) {
257 throw CommandException(
"Can't have both -mono and -stereo.");
259 if (!recordAudio && (recordStereo || recordMono)) {
260 throw CommandException(
"Can't have both -videoonly and -stereo or -mono.");
262 switch (arguments.size()) {
267 filename = arguments[0];
273 string directory = recordVideo ?
"videos" :
"soundlogs";
274 string extension = recordVideo ?
".avi" :
".wav";
276 filename, directory, prefix, extension);
278 if (aviWriter.get() || wavWriter.get()) {
281 start(recordAudio, recordVideo, recordMono, recordStereo,
283 result.
setString(
"Recording to " + filename);
287 void AviRecorder::processStop(
const vector<TclObject>& tokens)
289 if (tokens.size() != 2) {
295 void AviRecorder::processToggle(
const vector<TclObject>& tokens, TclObject& result)
297 if (aviWriter.get() || wavWriter.get()) {
299 vector<TclObject> tmp(tokens.begin(), tokens.begin() + 2);
302 processStart(tokens, result);
306 void AviRecorder::status(
const vector<TclObject>& tokens, TclObject& result)
const
308 if (tokens.size() != 2) {
311 result.addListElement(
"status");
312 if (aviWriter.get() || wavWriter.get()) {
313 result.addListElement(
"recording");
315 result.addListElement(
"idle");
324 :
Command(commandController,
"record")
325 , recorder(recorder_)
331 if (tokens.size() < 2) {
334 const string_ref subcommand = tokens[1].getString();
335 if (subcommand ==
"start") {
336 recorder.processStart(tokens, result);
337 }
else if (subcommand ==
"stop") {
338 recorder.processStop(tokens);
339 }
else if (subcommand ==
"toggle") {
340 recorder.processToggle(tokens, result);
341 }
else if (subcommand ==
"status") {
342 recorder.status(tokens, result);
350 return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
351 "record start Record to file 'openmsxNNNN.avi'\n"
352 "record start <filename> Record to given file\n"
353 "record start -prefix foo Record to file 'fooNNNN.avi'\n"
354 "record stop Stop recording\n"
355 "record toggle Toggle recording (useful as keybinding)\n"
356 "record status Query recording state\n"
358 "The start subcommand also accepts an optional -audioonly, -videoonly, "
359 " -mono, -stereo, -doublesize flag.\n"
360 "Videos are recorded in a 320x240 size by default and at 640x480 when the "
361 "-doublesize flag is used.";
366 if (tokens.size() == 2) {
367 static const char*
const cmds[] = {
368 "start",
"stop",
"toggle",
"status",
371 }
else if ((tokens.size() >= 3) && (tokens[1] ==
"start")) {
372 static const char*
const options[] = {
373 "-prefix",
"-videoonly",
"-audioonly",
"-doublesize",