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