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