openMSX
AviRecorder.cc
Go to the documentation of this file.
1#include "AviRecorder.hh"
2#include "AviWriter.hh"
3#include "Mixer.hh"
4#include "WavWriter.hh"
5#include "Reactor.hh"
6#include "MSXMotherBoard.hh"
7#include "FileContext.hh"
8#include "CommandException.hh"
9#include "Display.hh"
10#include "PostProcessor.hh"
11#include "Math.hh"
12#include "MSXMixer.hh"
13#include "Filename.hh"
14#include "CliComm.hh"
15#include "FileOperations.hh"
16#include "TclArgParser.hh"
17#include "TclObject.hh"
18#include "enumerate.hh"
19#include "narrow.hh"
20#include "outer.hh"
21#include "vla.hh"
22#include "xrange.hh"
23#include <array>
24#include <cassert>
25#include <memory>
26
27namespace openmsx {
28
30 : reactor(reactor_)
31 , recordCommand(reactor.getCommandController())
32{
33}
34
36{
37 assert(!aviWriter);
38 assert(!wavWriter);
39}
40
41void AviRecorder::start(bool recordAudio, bool recordVideo, bool recordMono,
42 bool recordStereo, const Filename& filename)
43{
44 stop();
45 MSXMotherBoard* motherBoard = reactor.getMotherBoard();
46 if (!motherBoard) {
47 throw CommandException("No active MSX machine.");
48 }
49 if (recordAudio) {
50 mixer = &motherBoard->getMSXMixer();
51 warnedStereo = false;
52 if (recordStereo) {
53 stereo = true;
54 } else if (recordMono) {
55 stereo = false;
56 warnedStereo = true; // no warning if data is actually stereo
57 } else {
58 stereo = mixer->needStereoRecording();
59 }
60 sampleRate = mixer->getSampleRate();
61 warnedSampleRate = false;
62 }
63 if (recordVideo) {
64 // Set V99x8, V9990, Laserdisc, ... in record mode (when
65 // present). Only the active one will actually send frames to
66 // the video. This also works for Video9000.
67 postProcessors.clear();
68 for (auto* l : reactor.getDisplay().getAllLayers()) {
69 if (auto* pp = dynamic_cast<PostProcessor*>(l)) {
70 postProcessors.push_back(pp);
71 }
72 }
73 if (postProcessors.empty()) {
74 throw CommandException(
75 "Current renderer doesn't support video recording.");
76 }
77 // any source is fine because they all have the same bpp
78 warnedFps = false;
79 duration = EmuDuration::infinity();
80 prevTime = EmuTime::infinity();
81
82 try {
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: ",
88 e.getMessage());
89 }
90 } else {
91 assert(recordAudio);
92 wavWriter = std::make_unique<Wav16Writer>(
93 filename, stereo ? 2 : 1, sampleRate);
94 }
95 // only set recorders when all errors are checked for
96 for (auto* pp : postProcessors) {
97 pp->setRecorder(this);
98 }
99 if (mixer) mixer->setRecorder(this);
100}
101
103{
104 for (auto* pp : postProcessors) {
105 pp->setRecorder(nullptr);
106 }
107 postProcessors.clear();
108 if (mixer) {
109 mixer->setRecorder(nullptr);
110 mixer = nullptr;
111 }
112 sampleRate = 0;
113 aviWriter.reset();
114 wavWriter.reset();
115}
116
117static int16_t float2int16(float f)
118{
119 return Math::clipToInt16(lrintf(32768.0f * f));
120}
121
122void AviRecorder::addWave(std::span<const StereoFloat> data)
123{
124 if (data.empty()) return;
125
126 assert(mixer);
127 if (!warnedSampleRate && (mixer->getSampleRate() != sampleRate)) {
128 warnedSampleRate = true;
129 reactor.getCliComm().printWarning(
130 "Detected audio sample frequency change during "
131 "avi recording. Audio/video might get out of sync "
132 "because of this.");
133 }
134 auto num = data.size();
135 if (stereo) {
136 if (wavWriter) {
137 wavWriter->write(data);
138 } else {
139 VLA(int16_t, buf, 2 * num);
140 for (auto [i, s] : enumerate(data)) {
141 buf[2 * i + 0] = float2int16(s.left);
142 buf[2 * i + 1] = float2int16(s.right);
143 }
144 assert(aviWriter);
145 append(audioBuf, buf);
146 }
147 } else {
148 VLA(int16_t, buf, num);
149 size_t i = 0;
150 for (; !warnedStereo && i < num; ++i) {
151 if (data[i].left != data[i].right) {
152 reactor.getCliComm().printWarning(
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.");
157 warnedStereo = true;
158 break;
159 }
160 buf[i] = float2int16(data[i].left);
161 }
162 for (; i < num; ++i) {
163 buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
164 }
165
166 if (wavWriter) {
167 wavWriter->write(buf);
168 } else {
169 assert(aviWriter);
170 append(audioBuf, buf);
171 }
172 }
173}
174
175void AviRecorder::addImage(FrameSource* frame, EmuTime::param time)
176{
177 assert(!wavWriter);
178 if (duration != EmuDuration::infinity()) {
179 if (!warnedFps && ((time - prevTime) != duration)) {
180 warnedFps = true;
181 reactor.getCliComm().printWarning(
182 "Detected frame rate change (PAL/NTSC or frameskip) "
183 "during avi recording. Audio/video might get out of "
184 "sync because of this.");
185 }
186 } else if (prevTime != EmuTime::infinity()) {
187 duration = time - prevTime;
188 aviWriter->setFps(narrow_cast<float>(1.0 / duration.toDouble()));
189 }
190 prevTime = time;
191
192 if (mixer) {
193 mixer->updateStream(time);
194 }
195 aviWriter->addFrame(frame, audioBuf);
196 audioBuf.clear();
197}
198
199// TODO: Can this be dropped?
201 assert (frameHeight != 0); // someone uses the getter too early?
202 return frameHeight;
203}
204
205void AviRecorder::processStart(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
206{
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;
214 std::array info = {
215 valueArg("-prefix", prefix),
216 flagArg("-audioonly", audioOnly),
217 flagArg("-videoonly", videoOnly),
218 flagArg("-mono", recordMono),
219 flagArg("-stereo", recordStereo),
220 flagArg("-doublesize", doubleSize),
221 flagArg("-triplesize", tripleSize),
222 };
223 auto arguments = parseTclArgs(interp, tokens.subspan(2), info);
224
225 if (audioOnly && videoOnly) {
226 throw CommandException("Can't have both -videoonly and -audioonly.");
227 }
228 if (recordStereo && recordMono) {
229 throw CommandException("Can't have both -mono and -stereo.");
230 }
231 if (doubleSize && tripleSize) {
232 throw CommandException("Can't have both -doublesize and -triplesize.");
233 }
234 if (videoOnly && (recordStereo || recordMono)) {
235 throw CommandException("Can't have both -videoonly and -stereo or -mono.");
236 }
237 std::string_view filenameArg;
238 switch (arguments.size()) {
239 case 0:
240 // nothing
241 break;
242 case 1:
243 filenameArg = arguments[0].getString();
244 break;
245 default:
246 throw SyntaxError();
247 }
248
249 frameWidth = 320;
250 frameHeight = 240;
251 if (doubleSize) {
252 frameWidth *= 2;
253 frameHeight *= 2;
254 } else if (tripleSize) {
255 frameWidth *= 3;
256 frameHeight *= 3;
257 }
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);
264
265 if (aviWriter || wavWriter) {
266 result = "Already recording.";
267 } else {
268 start(recordAudio, recordVideo, recordMono, recordStereo,
269 Filename(filename));
270 result = tmpStrCat("Recording to ", filename);
271 }
272}
273
274void AviRecorder::processStop(std::span<const TclObject> /*tokens*/)
275{
276 stop();
277}
278
279void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
280{
281 if (aviWriter || wavWriter) {
282 // drop extra tokens
283 processStop(tokens.first<2>());
284 } else {
285 processStart(interp, tokens, result);
286 }
287}
288
289void AviRecorder::status(std::span<const TclObject> /*tokens*/, TclObject& result) const
290{
291 result.addDictKeyValue("status", (aviWriter || wavWriter) ? "recording" : "idle");
292}
293
294// class AviRecorder::Cmd
295
296AviRecorder::Cmd::Cmd(CommandController& commandController_)
297 : Command(commandController_, "record")
298{
299}
300
301void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
302{
303 if (tokens.size() < 2) {
304 throw CommandException("Missing argument");
305 }
306 auto& recorder = OUTER(AviRecorder, recordCommand);
307 executeSubCommand(tokens[1].getString(),
308 "start", [&]{ recorder.processStart(getInterpreter(), tokens, result); },
309 "stop", [&]{
310 checkNumArgs(tokens, 2, Prefix{2}, nullptr);
311 recorder.processStop(tokens); },
312 "toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
313 "status", [&]{
314 checkNumArgs(tokens, 2, Prefix{2}, nullptr);
315 recorder.status(tokens, result); });
316}
317
318std::string AviRecorder::Cmd::help(std::span<const TclObject> /*tokens*/) const
319{
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"
327 "\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.";
332}
333
334void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens) const
335{
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,
340 };
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,
347 };
348 completeFileName(tokens, userFileContext(), options);
349 }
350}
351
352} // namespace openmsx
void addImage(FrameSource *frame, EmuTime::param time)
Definition: AviRecorder.cc:175
AviRecorder(Reactor &reactor)
Definition: AviRecorder.cc:29
void addWave(std::span< const StereoFloat > data)
Definition: AviRecorder.cc:122
unsigned getFrameHeight() const
Definition: AviRecorder.cc:200
void printWarning(std::string_view message)
Definition: CliComm.cc:10
const Layers & getAllLayers() const
Definition: Display.hh:61
static constexpr EmuDuration infinity()
Definition: EmuDuration.hh:112
constexpr double toDouble() const
Definition: EmuDuration.hh:52
This class represents a filename.
Definition: Filename.hh:18
Interface for getting lines from a video frame.
Definition: FrameSource.hh:18
void setRecorder(AviRecorder *recorder)
Definition: MSXMixer.cc:657
unsigned getSampleRate() const
Definition: MSXMixer.hh:134
bool needStereoRecording() const
Definition: MSXMixer.cc:601
void updateStream(EmuTime::param time)
Use this method to force an 'early' call to all updateBuffer() methods.
Definition: MSXMixer.cc:159
Contains the main loop of openMSX.
Definition: Reactor.hh:72
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:404
Display & getDisplay()
Definition: Reactor.hh:90
CliComm & getCliComm()
Definition: Reactor.cc:319
constexpr auto enumerate(Iterable &&iterable)
Heavily inspired by Nathan Reed's blog post: Python-Like enumerate() In C++17 http://reedbeta....
Definition: enumerate.hh:28
int16_t clipToInt16(T x)
Clip x to range [-32768,32767].
Definition: Math.hh:47
constexpr double e
Definition: Math.hh:21
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:
Definition: Autofire.cc:9
ArgsInfo valueArg(std::string_view name, T &value)
Definition: TclArgParser.hh:85
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:171
ArgsInfo flagArg(std::string_view name, bool &flag)
Definition: TclArgParser.hh:72
#define OUTER(type, member)
Definition: outer.hh:41
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:693
#define VLA(TYPE, NAME, LENGTH)
Definition: vla.hh:12