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