OXYGENHUB
◆ main openmsx / src / video / AviWriter.cc
10.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
// Code based on DOSBox-0.65
2
3
#include "AviWriter.hh"
4
5
#include "FileOperations.hh"
6
#include "MSXException.hh"
7
#include "Version.hh"
8
9
#include "cstdiop.hh" // for snprintf
10
#include "endian.hh"
11
#include "narrow.hh"
12
#include "ranges.hh"
13
#include "small_buffer.hh"
14
#include "stl.hh"
15
#include "zstring_view.hh"
16
17
#include <array>
18
#include <cassert>
19
#include <cstring>
20
#include <ctime>
21
#include <limits>
22
23
namespace openmsx {
24
25
static constexpr unsigned AVI_HEADER_SIZE = 500;
26
27
AviWriter::AviWriter(const std::string& filename_, unsigned width_,
28
                     unsigned height_, unsigned channels_, unsigned freq_)
29
	: file(filename_, "wb")
30
	, filename(filename_)
31
	, codec(width_, height_)
32
	, width(width_)
33
	, height(height_)
34
	, channels(channels_)
35
	, audioRate(freq_)
36
{
37
	std::array<uint8_t, AVI_HEADER_SIZE> dummy = {};
38
	file.write(dummy);
39
40
	index.resize(2);
41
}
42
43
AviWriter::~AviWriter()
44
{
45
	if (written == 0) {
46
		// no data written yet (a recording less than one video frame)
47
		file.close(); // close file (needed for windows?)
48
		FileOperations::unlink(filename);
49
		return;
50
	}
51
	assert(fps != 0.0f); // a decent fps should have been set
52
53
	// Possible cleanup: use structs for the different headers, that
54
	// also allows to use the aligned versions of the Endian routines.
55
	std::array<uint8_t, AVI_HEADER_SIZE> avi_header = {};
56
	unsigned header_pos = 0;
57
58
	auto AVIOUT4 = [&](std::string_view s) {
59
		assert(s.size() == 4);
60
		copy_to_range(s, subspan(avi_header, header_pos));
61
		header_pos += 4;
62
	};
63
	auto AVIOUTw = [&](uint16_t w) {
64
		Endian::write_UA_L16(&avi_header[header_pos], w);
65
		header_pos += sizeof(w);
66
	};
67
	auto AVIOUTd = [&](uint32_t d) {
68
		Endian::write_UA_L32(&avi_header[header_pos], d);
69
		header_pos += sizeof(d);
70
	};
71
	auto AVIOUTs = [&](zstring_view s) {
72
		auto len1 = s.size() + 1; // +1 for zero-terminator
73
		copy_to_range(std::span{s.data(), len1}, subspan(avi_header, header_pos));
74
		header_pos += narrow<unsigned>((len1 + 1) & ~1); // round-up to even
75
	};
76
77
	bool hasAudio = audioRate != 0;
78
79
	// write avi header
80
	AVIOUT4("RIFF");                    // Riff header
81
	AVIOUTd(AVI_HEADER_SIZE + written - 8 + unsigned(index.size() * sizeof(Endian::L32)));
82
	AVIOUT4("AVI ");
83
	AVIOUT4("LIST");                    // List header
84
	auto main_list = header_pos;
85
	AVIOUTd(0);                         // size of list, filled in later
86
	AVIOUT4("hdrl");
87
88
	AVIOUT4("avih");
89
	AVIOUTd(56);                        // # of bytes to follow
90
	AVIOUTd(uint32_t(1000000 / fps));   // Microseconds per frame
91
	AVIOUTd(0);
92
	AVIOUTd(0);                         // PaddingGranularity (whatever that might be)
93
	AVIOUTd(0x110);                     // Flags,0x10 has index, 0x100 interleaved
94
	AVIOUTd(frames);                    // TotalFrames
95
	AVIOUTd(0);                         // InitialFrames
96
	AVIOUTd(hasAudio? 2 : 1);           // Stream count
97
	AVIOUTd(0);                         // SuggestedBufferSize
98
	AVIOUTd(width);                     // Width
99
	AVIOUTd(height);                    // Height
100
	AVIOUTd(0);                         // TimeScale:  Unit used to measure time
101
	AVIOUTd(0);                         // DataRate:   Data rate of playback
102
	AVIOUTd(0);                         // StartTime:  Starting time of AVI data
103
	AVIOUTd(0);                         // DataLength: Size of AVI data chunk
104
105
	// Video stream list
106
	AVIOUT4("LIST");
107
	AVIOUTd(4 + 8 + 56 + 8 + 40);       // Size of the list
108
	AVIOUT4("strl");
109
	// video stream header
110
	AVIOUT4("strh");
111
	AVIOUTd(56);                        // # of bytes to follow
112
	AVIOUT4("vids");                    // Type
113
	AVIOUT4(ZMBVEncoder::CODEC_4CC);    // Handler
114
	AVIOUTd(0);                         // Flags
115
	AVIOUTd(0);                         // Reserved, MS says: wPriority, wLanguage
116
	AVIOUTd(0);                         // InitialFrames
117
	AVIOUTd(1000000);                   // Scale
118
	AVIOUTd(uint32_t(1000000 * fps));   // Rate: Rate/Scale == samples/second
119
	AVIOUTd(0);                         // Start
120
	AVIOUTd(frames);                    // Length
121
	AVIOUTd(0);                         // SuggestedBufferSize
122
	AVIOUTd(uint32_t(~0));              // Quality
123
	AVIOUTd(0);                         // SampleSize
124
	AVIOUTd(0);                         // Frame
125
	AVIOUTd(0);                         // Frame
126
	// The video stream format
127
	AVIOUT4("strf");
128
	AVIOUTd(40);                        // # of bytes to follow
129
	AVIOUTd(40);                        // Size
130
	AVIOUTd(width);                     // Width
131
	AVIOUTd(height);                    // Height
132
	AVIOUTd(0);
133
	AVIOUT4(ZMBVEncoder::CODEC_4CC);    // Compression
134
	AVIOUTd(width * height * 4);        // SizeImage (in bytes?)
135
	AVIOUTd(0);                         // XPelsPerMeter
136
	AVIOUTd(0);                         // YPelsPerMeter
137
	AVIOUTd(0);                         // ClrUsed: Number of colors used
138
	AVIOUTd(0);                         // ClrImportant: Number of colors important
139
140
	if (hasAudio) {
141
		// 1 fragment is 1 (for mono) or 2 (for stereo) samples
142
		uint16_t bitsPerSample = 16;
143
		unsigned bytesPerSample = bitsPerSample / 8;
144
		unsigned bytesPerFragment = bytesPerSample * channels;
145
		unsigned bytesPerSecond = audioRate * bytesPerFragment;
146
		unsigned fragments = audioWritten / channels;
147
148
		// Audio stream list
149
		AVIOUT4("LIST");
150
		AVIOUTd(4 + 8 + 56 + 8 + 16);// Length of list in bytes
151
		AVIOUT4("strl");
152
		// The audio stream header
153
		AVIOUT4("strh");
154
		AVIOUTd(56);                // # of bytes to follow
155
		AVIOUT4("auds");
156
		AVIOUTd(0);                 // Format (Optionally)
157
		AVIOUTd(0);                 // Flags
158
		AVIOUTd(0);                 // Reserved, MS says: wPriority, wLanguage
159
		AVIOUTd(0);                 // InitialFrames
160
		// Rate/Scale should be number of samples per second!
161
		AVIOUTd(bytesPerFragment);  // Scale
162
		AVIOUTd(bytesPerSecond);    // Rate, actual rate is scale/rate
163
		AVIOUTd(0);                 // Start
164
		AVIOUTd(fragments);         // Length
165
		AVIOUTd(0);                 // SuggestedBufferSize
166
		AVIOUTd(unsigned(~0));      // Quality
167
		AVIOUTd(bytesPerFragment);  // SampleSize (should be the same as BlockAlign)
168
		AVIOUTd(0);                 // Frame
169
		AVIOUTd(0);                 // Frame
170
		// The audio stream format
171
		AVIOUT4("strf");
172
		AVIOUTd(16);                // # of bytes to follow
173
		AVIOUTw(1);                 // Format, WAVE_ZMBV_FORMAT_PCM
174
		AVIOUTw(narrow<uint16_t>(channels)); // Number of channels
175
		AVIOUTd(audioRate);         // SamplesPerSec
176
		AVIOUTd(bytesPerSecond);    // AvgBytesPerSec
177
		AVIOUTw(narrow<uint16_t>(bytesPerFragment)); // BlockAlign: for PCM: nChannels * BitsPerSample / 8
178
		AVIOUTw(bitsPerSample);     // BitsPerSample
179
	}
180
181
	auto versionStr = Version::full();
182
183
	// The standard snprintf() function does always zero-terminate the
184
	// output it writes. Though windows doesn't have a snprintf() function,
185
	// instead we #define snprintf to _snprintf and the latter does NOT
186
	// properly zero-terminate. See also
187
	//   http://stackoverflow.com/questions/7706936/is-snprintf-always-null-terminating
188
	//
189
	// A buffer size of 11 characters is large enough till the year 9999.
190
	// But the compiler doesn't understand calendars and warns that the
191
	// snprintf output could be truncated (e.g. because the year is
192
	// -2147483647). To silence this warning (and also to work around the
193
	// windows _snprintf stuff) we add some extra buffer space.
194
	constexpr size_t size = (4 + 1 + 2 + 1 + 2 + 1) + 22;
195
	std::array<char, size> dateStr;
196
	time_t t = time(nullptr);
197
	const struct tm* tm = localtime(&t);
198
	size_t dateLen = snprintf(dateStr.data(), sizeof(dateStr), "%04d-%02d-%02d", 1900 + tm->tm_year,
199
		                  tm->tm_mon + 1, tm->tm_mday);
200
	assert(dateLen < size);
201
202
	AVIOUT4("LIST");
203
	AVIOUTd(narrow<uint32_t>(
204
			4 // list type
205
			+ (4 + 4 + ((versionStr.size() + 1 + 1) & ~1)) // 1st chunk
206
			+ (4 + 4 + ((dateLen + 1 + 1) & ~1)) // 2nd chunk
207
		)); // size of the list
208
	AVIOUT4("INFO");
209
	AVIOUT4("ISFT");
210
	AVIOUTd(unsigned(versionStr.size()) + 1); // # of bytes to follow
211
	AVIOUTs(versionStr);
212
	AVIOUT4("ICRD");
213
	AVIOUTd(unsigned(dateLen) + 1); // # of bytes to follow
214
	AVIOUTs(zstring_view{dateStr.data(), dateLen});
215
	// TODO: add artist (IART), comments (ICMT), name (INAM), etc.
216
	// use a loop over chunks (type + string) to create the above bytes in
217
	// a much nicer way
218
219
	// Finish stream list, i.e. put number of bytes in the list to proper pos
220
	auto nMain = header_pos - main_list - 4;
221
	auto nJunk = AVI_HEADER_SIZE - 8 - 12 - header_pos;
222
	assert(nJunk > 0); // increase AVI_HEADER_SIZE if this occurs
223
	AVIOUT4("JUNK");
224
	AVIOUTd(nJunk);
225
	// Fix the size of the main list
226
	header_pos = main_list;
227
	AVIOUTd(nMain);
228
	header_pos = AVI_HEADER_SIZE - 12;
229
230
	AVIOUT4("LIST");
231
	AVIOUTd(written + 4);               // Length of list in bytes
232
	AVIOUT4("movi");
233
234
	try {
235
		// First add the index table to the end
236
		unsigned idxSize = unsigned(index.size()) * sizeof(Endian::L32);
237
		index[0] = ('i' << 0) | ('d' << 8) | ('x' << 16) | ('1' << 24);
238
		index[1] = idxSize - 8;
239
		file.write(std::span{index});
240
		file.seek(0);
241
		file.write(avi_header);
242
	} catch (MSXException&) {
243
		// can't throw from destructor
244
	}
245
}
246
247
void AviWriter::addAviChunk(std::span<const char, 4> tag, std::span<const uint8_t> data, unsigned flags)
248
{
249
	struct {
250
		std::array<char, 4> t;
251
		Endian::L32 s;
252
	} chunk;
253
254
	auto size32 = narrow<uint32_t>(data.size());
255
256
	copy_to_range(tag, chunk.t);
257
	chunk.s = size32;
258
	file.write(std::span{&chunk, 1});
259
260
	file.write(data);
261
	unsigned pos = written + 4;
262
	written += size32 + 8;
263
	if (size32 & 1) {
264
		std::array<uint8_t, 1> padding = {0};
265
		file.write(padding);
266
		++written;
267
	}
268
269
	size_t idxSize = index.size();
270
	index.resize(idxSize + 4);
271
	memcpy(&index[idxSize], tag.data(), tag.size());
272
	index[idxSize + 1] = flags;
273
	index[idxSize + 2] = pos;
274
	index[idxSize + 3] = size32;
275
}
276
277
void AviWriter::addFrame(const FrameSource* video, std::span<const int16_t> audio)
278
{
279
	bool keyFrame = (frames++ % 300 == 0);
280
	auto buffer = codec.compressFrame(keyFrame, video);
281
	addAviChunk(subspan<4>("00dc"), buffer, keyFrame ? 0x10 : 0x0);
282
283
	if (!audio.empty()) {
284
		assert((audio.size() % channels) == 0);
285
		assert(audioRate != 0);
286
		if constexpr (Endian::BIG) {
287
			small_buffer<Endian::L16, 4096> buf(audio);
288
			addAviChunk(subspan<4>("01wb"), as_byte_span(std::span{buf}), 0);
289
		} else {
290
			addAviChunk(subspan<4>("01wb"), as_byte_span(audio), 0);
291
		}
292
		audioWritten += narrow<uint32_t>(audio.size());
293
	}
294
}
295
296
} // namespace openmsx