OXYGENHUB
◆ main openmsx / src / video / AviRecorder.cc
9.8 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "AviRecorder.hh"
2
3
#include "AviWriter.hh"
4
#include "PostProcessor.hh"
5
6
#include "CliComm.hh"
7
#include "CommandException.hh"
8
#include "Display.hh"
9
#include "FileContext.hh"
10
#include "FileOperations.hh"
11
#include "MSXMixer.hh"
12
#include "MSXMotherBoard.hh"
13
#include "Mixer.hh"
14
#include "Reactor.hh"
15
#include "TclArgParser.hh"
16
#include "TclObject.hh"
17
#include "WavWriter.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
29
namespace openmsx {
30
31
using namespace std::literals;
32
33
AviRecorder::AviRecorder(Reactor& reactor_)
34
	: reactor(reactor_)
35
	, recordCommand(reactor.getCommandController())
36
{
37
}
38
39
AviRecorder::~AviRecorder()
40
{
41
	assert(!aviWriter);
42
	assert(!wavWriter);
43
}
44
45
void AviRecorder::start(bool recordAudio, bool recordVideo, bool recordMono,
46
                        bool recordStereo, const std::string& filename)
47
{
48
	stop();
49
	MSXMotherBoard* motherBoard = reactor.getMotherBoard();
50
	if (!motherBoard) {
51
		throw CommandException("No active MSX machine.");
52
	}
53
	if (recordAudio) {
54
		mixer = &motherBoard->getMSXMixer();
55
		warnedStereo = false;
56
		if (recordStereo) {
57
			stereo = true;
58
		} else if (recordMono) {
59
			stereo = false;
60
			warnedStereo = true; // no warning if data is actually stereo
61
		} else {
62
			stereo = mixer->needStereoRecording();
63
		}
64
		sampleRate = mixer->getSampleRate();
65
		warnedSampleRate = false;
66
	}
67
	if (recordVideo) {
68
		// Set V99x8, V9990, Laserdisc, ... in record mode (when
69
		// present). Only the active one will actually send frames to
70
		// the video. This also works for Video9000.
71
		postProcessors.clear();
72
		for (auto* l : reactor.getDisplay().getAllLayers()) {
73
			if (auto* pp = dynamic_cast<PostProcessor*>(l)) {
74
				postProcessors.push_back(pp);
75
			}
76
		}
77
		if (postProcessors.empty()) {
78
			throw CommandException(
79
				"Current renderer doesn't support video recording.");
80
		}
81
		// any source is fine because they all have the same bpp
82
		warnedFps = false;
83
		duration = EmuDuration::infinity();
84
		prevTime = EmuTime::infinity();
85
86
		try {
87
			aviWriter = std::make_unique<AviWriter>(
88
				filename, frameWidth, frameHeight,
89
				(recordAudio && stereo) ? 2 : 1, sampleRate);
90
		} catch (MSXException& e) {
91
			throw CommandException("Can't start recording: ",
92
			                       e.getMessage());
93
		}
94
	} else {
95
		assert(recordAudio);
96
		wavWriter = std::make_unique<Wav16Writer>(
97
			filename, stereo ? 2 : 1, sampleRate);
98
	}
99
	// only set recorders when all errors are checked for
100
	for (auto* pp : postProcessors) {
101
		pp->setRecorder(this);
102
	}
103
	if (mixer) mixer->setRecorder(this);
104
}
105
106
void AviRecorder::stop()
107
{
108
	for (auto* pp : postProcessors) {
109
		pp->setRecorder(nullptr);
110
	}
111
	postProcessors.clear();
112
	if (mixer) {
113
		mixer->setRecorder(nullptr);
114
		mixer = nullptr;
115
	}
116
	sampleRate = 0;
117
	aviWriter.reset();
118
	wavWriter.reset();
119
}
120
121
static int16_t float2int16(float f)
122
{
123
	return Math::clipToInt16(lrintf(32768.0f * f));
124
}
125
126
void AviRecorder::addWave(std::span<const StereoFloat> data)
127
{
128
	if (data.empty()) return;
129
130
	assert(mixer);
131
	if (!warnedSampleRate && (mixer->getSampleRate() != sampleRate)) {
132
		warnedSampleRate = true;
133
		reactor.getCliComm().printWarning(
134
			"Detected audio sample frequency change during "
135
			"avi recording. Audio/video might get out of sync "
136
			"because of this.");
137
	}
138
	auto num = data.size();
139
	if (stereo) {
140
		if (wavWriter) {
141
			wavWriter->write(data);
142
		} else {
143
			small_buffer<int16_t, 2 * 4096> buf(uninitialized_tag{}, 2 * size_t(num));
144
			for (auto [i, s] : enumerate(data)) {
145
				buf[2 * i + 0] = float2int16(s.left);
146
				buf[2 * i + 1] = float2int16(s.right);
147
			}
148
			assert(aviWriter);
149
			append(audioBuf, std::span{buf});
150
		}
151
	} else {
152
		small_buffer<int16_t, 4096> buf(uninitialized_tag{}, num);
153
		size_t i = 0;
154
		for (/**/; !warnedStereo && i < num; ++i) {
155
			if (data[i].left != data[i].right) {
156
				reactor.getCliComm().printWarning(
157
				    "Detected stereo sound during mono recording. "
158
				    "Channels will be mixed down to mono. To "
159
				    "avoid this warning you can explicitly pass the "
160
				    "-mono or -stereo flag to the record command.");
161
				warnedStereo = true;
162
				break;
163
			}
164
			buf[i] = float2int16(data[i].left);
165
		}
166
		for (/**/; i < num; ++i) {
167
			buf[i] = float2int16((data[i].left + data[i].right) * 0.5f);
168
		}
169
170
		if (wavWriter) {
171
			wavWriter->write(buf);
172
		} else {
173
			assert(aviWriter);
174
			append(audioBuf, std::span{buf});
175
		}
176
	}
177
}
178
179
void AviRecorder::addImage(const FrameSource* frame, EmuTime time)
180
{
181
	assert(!wavWriter);
182
	if (duration != EmuDuration::infinity()) {
183
		if (!warnedFps && ((time - prevTime) != duration)) {
184
			warnedFps = true;
185
			reactor.getCliComm().printWarning(
186
				"Detected frame rate change (PAL/NTSC or frameskip) "
187
				"during avi recording. Audio/video might get out of "
188
				"sync because of this.");
189
		}
190
	} else if (prevTime != EmuTime::infinity()) {
191
		duration = time - prevTime;
192
		aviWriter->setFps(narrow_cast<float>(1.0 / duration.toDouble()));
193
	}
194
	prevTime = time;
195
196
	if (mixer) {
197
		mixer->updateStream(time);
198
	}
199
	aviWriter->addFrame(frame, audioBuf);
200
	audioBuf.clear();
201
}
202
203
// TODO: Can this be dropped?
204
unsigned AviRecorder::getFrameHeight() const {
205
	assert (frameHeight != 0); // someone uses the getter too early?
206
	return frameHeight;
207
}
208
209
void AviRecorder::processStart(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
210
{
211
	std::string_view prefix = "openmsx";
212
	bool audioOnly    = false;
213
	bool videoOnly    = false;
214
	bool recordMono   = false;
215
	bool recordStereo = false;
216
	bool doubleSize   = false;
217
	bool tripleSize   = false;
218
	std::array info = {
219
		valueArg("-prefix", prefix),
220
		flagArg("-audioonly", audioOnly),
221
		flagArg("-videoonly", videoOnly),
222
		flagArg("-mono",      recordMono),
223
		flagArg("-stereo",    recordStereo),
224
		flagArg("-doublesize", doubleSize),
225
		flagArg("-triplesize", tripleSize),
226
	};
227
	auto arguments = parseTclArgs(interp, tokens.subspan(2), info);
228
229
	if (audioOnly && videoOnly) {
230
		throw CommandException("Can't have both -videoonly and -audioonly.");
231
	}
232
	if (recordStereo && recordMono) {
233
		throw CommandException("Can't have both -mono and -stereo.");
234
	}
235
	if (doubleSize && tripleSize) {
236
		throw CommandException("Can't have both -doublesize and -triplesize.");
237
	}
238
	if (videoOnly && (recordStereo || recordMono)) {
239
		throw CommandException("Can't have both -videoonly and -stereo or -mono.");
240
	}
241
	std::string_view filenameArg;
242
	switch (arguments.size()) {
243
	case 0:
244
		// nothing
245
		break;
246
	case 1:
247
		filenameArg = arguments[0].getString();
248
		break;
249
	default:
250
		throw SyntaxError();
251
	}
252
253
	frameWidth = 320;
254
	frameHeight = 240;
255
	if (doubleSize) {
256
		frameWidth  *= 2;
257
		frameHeight *= 2;
258
	} else if (tripleSize) {
259
		frameWidth  *= 3;
260
		frameHeight *= 3;
261
	}
262
	bool recordAudio = !videoOnly;
263
	bool recordVideo = !audioOnly;
264
	std::string_view directory = recordVideo ? VIDEO_DIR : AUDIO_DIR;
265
	std::string_view extension = recordVideo ? VIDEO_EXTENSION : AUDIO_EXTENSION;
266
	auto filename = FileOperations::parseCommandFileArgument(
267
		filenameArg, directory, prefix, extension);
268
269
	if (aviWriter || wavWriter) {
270
		result = "Already recording.";
271
	} else {
272
		start(recordAudio, recordVideo, recordMono, recordStereo, filename);
273
		result = tmpStrCat("Recording to ", filename);
274
	}
275
}
276
277
void AviRecorder::processStop(std::span<const TclObject> /*tokens*/)
278
{
279
	stop();
280
}
281
282
void AviRecorder::processToggle(Interpreter& interp, std::span<const TclObject> tokens, TclObject& result)
283
{
284
	if (aviWriter || wavWriter) {
285
		// drop extra tokens
286
		processStop(tokens.first<2>());
287
	} else {
288
		processStart(interp, tokens, result);
289
	}
290
}
291
292
bool AviRecorder::isRecording() const
293
{
294
	return aviWriter || wavWriter;
295
}
296
297
void AviRecorder::status(std::span<const TclObject> /*tokens*/, TclObject& result) const
298
{
299
	result.addDictKeyValue("status", isRecording() ? "recording"sv : "idle"sv);
300
}
301
302
// class AviRecorder::Cmd
303
304
AviRecorder::Cmd::Cmd(CommandController& commandController_)
305
	: Command(commandController_, "record")
306
{
307
}
308
309
void AviRecorder::Cmd::execute(std::span<const TclObject> tokens, TclObject& result)
310
{
311
	if (tokens.size() < 2) {
312
		throw CommandException("Missing argument");
313
	}
314
	auto& recorder = OUTER(AviRecorder, recordCommand);
315
	executeSubCommand(tokens[1].getString(),
316
		"start",  [&]{ recorder.processStart(getInterpreter(), tokens, result); },
317
		"stop",   [&]{
318
			checkNumArgs(tokens, 2, Prefix{2}, nullptr);
319
			recorder.processStop(tokens); },
320
		"toggle", [&]{ recorder.processToggle(getInterpreter(), tokens, result); },
321
		"status", [&]{
322
			checkNumArgs(tokens, 2, Prefix{2}, nullptr);
323
			recorder.status(tokens, result); });
324
}
325
326
std::string AviRecorder::Cmd::help(std::span<const TclObject> /*tokens*/) const
327
{
328
	return "Controls video recording: Write openMSX audio/video to a .avi file.\n"
329
	       "record start              Record to file 'openmsxNNNN.avi'\n"
330
	       "record start <filename>   Record to given file\n"
331
	       "record start -prefix foo  Record to file 'fooNNNN.avi'\n"
332
	       "record stop               Stop recording\n"
333
	       "record toggle             Toggle recording (useful as keybinding)\n"
334
	       "record status             Query recording state\n"
335
	       "\n"
336
	       "The start subcommand also accepts an optional -audioonly, -videoonly, "
337
	       " -mono, -stereo, -doublesize, -triplesize flag.\n"
338
	       "Videos are recorded in a 320x240 size by default, at 640x480 when the "
339
	       "-doublesize flag is used and at 960x720 when the -triplesize flag is used.";
340
}
341
342
void AviRecorder::Cmd::tabCompletion(std::vector<std::string>& tokens) const
343
{
344
	using namespace std::literals;
345
	if (tokens.size() == 2) {
346
		static constexpr std::array cmds = {
347
			"start"sv, "stop"sv, "toggle"sv, "status"sv,
348
		};
349
		completeString(tokens, cmds);
350
	} else if ((tokens.size() >= 3) && (tokens[1] == "start")) {
351
		static constexpr std::array options = {
352
			"-prefix"sv, "-videoonly"sv, "-audioonly"sv,
353
			"-doublesize"sv, "-triplesize"sv,
354
			"-mono"sv, "-stereo"sv,
355
		};
356
		completeFileName(tokens, userFileContext(), options);
357
	}
358
}
359
360
} // namespace openmsx