Also support dumping JMD and SDMP over TCP/IP
[lsnes.git] / src / video / jmd.cpp
blobd840f124cd58680a81544a3d00577b74ad921c8a
1 #include "core/advdumper.hpp"
2 #include "core/dispatch.hpp"
3 #include "core/settings.hpp"
4 #include "library/serialization.hpp"
5 #include "video/tcp.hpp"
7 #include <iomanip>
8 #include <cassert>
9 #include <cstring>
10 #include <sstream>
11 #include <fstream>
12 #include <deque>
13 #include <zlib.h>
14 #define INBUF_PIXELS 4096
15 #define OUTBUF_ADVANCE 4096
18 namespace
20 numeric_setting clevel("jmd-compression", 0, 9, 7);
22 void deleter_fn(void* f)
24 delete reinterpret_cast<std::ofstream*>(f);
27 class jmd_avsnoop : public information_dispatch
29 public:
30 jmd_avsnoop(const std::string& filename, bool tcp_flag) throw(std::bad_alloc)
31 : information_dispatch("dump-jmd")
33 enable_send_sound();
34 complevel = clevel;
35 if(tcp_flag) {
36 jmd = &(socket_address(filename).connect());
37 deleter = socket_address::deleter();
38 } else {
39 jmd = new std::ofstream(filename.c_str(), std::ios::out | std::ios::binary);
40 deleter = deleter_fn;
42 if(!*jmd)
43 throw std::runtime_error("Can't open output JMD file.");
44 last_written_ts = 0;
45 //Write the segment tables.
46 //Stream #0 is video.
47 //Stream #1 is PCM audio.
48 //Stream #2 is Gameinfo.
49 //Stream #3 is Dummy.
50 char header[] = {
51 /* Magic */
52 -1, -1, 0x4A, 0x50, 0x43, 0x52, 0x52, 0x4D, 0x55, 0x4C, 0x54, 0x49, 0x44, 0x55, 0x4D,
53 0x50,
54 /* Channel count. */
55 0x00, 0x04,
56 /* Video channel header. */
57 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 'v', 'i',
58 /* Audio channel header. */
59 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 'a', 'u',
60 /* Gameinfo channel header. */
61 0x00, 0x02, 0x00, 0x05, 0x00, 0x02, 'g', 'i',
62 /* Dummy channel header. */
63 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 'd', 'u'
65 jmd->write(header, sizeof(header));
66 if(!*jmd)
67 throw std::runtime_error("Can't write JMD header and segment table");
68 have_dumped_frame = false;
69 audio_w = 0;
70 audio_n = 0;
71 video_w = 0;
72 video_n = 0;
73 maxtc = 0;
74 soundrate = get_sound_rate();
75 try {
76 on_gameinfo(get_gameinfo());
77 } catch(std::exception& e) {
78 messages << "Can't write gameinfo: " << e.what() << std::endl;
82 ~jmd_avsnoop() throw()
84 try {
85 on_dump_end();
86 } catch(...) {
90 void on_frame(struct lcscreen& _frame, uint32_t fps_n, uint32_t fps_d)
92 render_video_hud(dscr, _frame, 1, 1, 0, 8, 16, 0, 0, 0, 0, NULL);
93 frame_buffer f;
94 f.ts = get_next_video_ts(fps_n, fps_d);
95 size_t fsize = 0;
96 //We'll compress the frame here.
97 f.data = compress_frame(dscr.memory, dscr.width, dscr.height);
98 frames.push_back(f);
99 flush_buffers(false);
100 have_dumped_frame = true;
103 void on_sample(short l, short r)
105 uint64_t ts = get_next_audio_ts();
106 if(have_dumped_frame) {
107 sample_buffer s;
108 s.ts = ts;
109 s.l = l;
110 s.r = r;
111 samples.push_back(s);
112 flush_buffers(false);
116 void on_dump_end()
118 if(!jmd)
119 return;
120 flush_buffers(true);
121 if(last_written_ts > maxtc) {
122 deleter(jmd);
123 jmd = NULL;
124 return;
126 char dummypacket[8] = {0x00, 0x03};
127 write32ube(dummypacket + 2, maxtc - last_written_ts);
128 last_written_ts = maxtc;
129 jmd->write(dummypacket, sizeof(dummypacket));
130 if(!*jmd)
131 throw std::runtime_error("Can't write JMD ending dummy packet");
132 deleter(jmd);
133 jmd = NULL;
136 void on_gameinfo(const struct gameinfo_struct& gi)
138 std::string authstr;
139 for(size_t i = 0; i < gi.get_author_count(); i++) {
140 if(i != 0)
141 authstr = authstr + ", ";
142 authstr = authstr + gi.get_author_short(i);
144 //TODO: Implement
147 bool get_dumper_flag() throw()
149 return true;
151 private:
152 uint64_t get_next_video_ts(uint32_t fps_n, uint32_t fps_d)
154 uint64_t ret = video_w;
155 video_w += (1000000000ULL * fps_d) / fps_n;
156 video_n += (1000000000ULL * fps_d) % fps_n;
157 if(video_n >= fps_n) {
158 video_n -= fps_n;
159 video_w++;
161 maxtc = (ret > maxtc) ? ret : maxtc;
162 return ret;
165 uint64_t get_next_audio_ts()
167 uint64_t ret = audio_w;
168 audio_w += (1000000000ULL * soundrate.second) / soundrate.first;
169 audio_n += (1000000000ULL * soundrate.second) % soundrate.first;
170 if(audio_n >= soundrate.first) {
171 audio_n -= soundrate.first;
172 audio_w++;
174 maxtc = (ret > maxtc) ? ret : maxtc;
175 return ret;
178 screen<false> dscr;
179 unsigned dcounter;
180 bool have_dumped_frame;
181 uint64_t audio_w;
182 uint64_t audio_n;
183 uint64_t video_w;
184 uint64_t video_n;
185 uint64_t maxtc;
186 std::pair<uint32_t, uint32_t> soundrate;
187 struct frame_buffer
189 uint64_t ts;
190 std::vector<char> data;
192 struct sample_buffer
194 uint64_t ts;
195 short l;
196 short r;
199 std::deque<frame_buffer> frames;
200 std::deque<sample_buffer> samples;
202 std::vector<char> compress_frame(uint32_t* memory, uint32_t width, uint32_t height)
204 std::vector<char> ret;
205 z_stream stream;
206 memset(&stream, 0, sizeof(stream));
207 if(deflateInit(&stream, complevel) != Z_OK)
208 throw std::runtime_error("Can't initialize zlib stream");
210 size_t usize = 4;
211 ret.resize(4);
212 write16ube(&ret[0], width);
213 write16ube(&ret[2], height);
214 uint8_t input_buffer[4 * INBUF_PIXELS];
215 size_t ptr = 0;
216 size_t pixels = static_cast<size_t>(width) * height;
217 bool input_clear = true;
218 bool flushed = false;
219 size_t bsize = 0;
220 while(1) {
221 if(input_clear) {
222 size_t pixel = ptr;
223 for(unsigned i = 0; i < INBUF_PIXELS && pixel < pixels; i++, pixel++)
224 write32ule(input_buffer + (4 * i), memory[pixel]);
225 bsize = pixel - ptr;
226 ptr = pixel;
227 input_clear = false;
228 //Now the input data to compress is in input_buffer, bsize elements.
229 stream.next_in = reinterpret_cast<uint8_t*>(input_buffer);
230 stream.avail_in = 4 * bsize;
232 if(!stream.avail_out) {
233 if(flushed)
234 usize += (OUTBUF_ADVANCE - stream.avail_out);
235 flushed = true;
236 ret.resize(usize + OUTBUF_ADVANCE);
237 stream.next_out = reinterpret_cast<uint8_t*>(&ret[usize]);
238 stream.avail_out = OUTBUF_ADVANCE;
240 int r = deflate(&stream, (ptr == pixels) ? Z_FINISH : 0);
241 if(r == Z_STREAM_END)
242 break;
243 if(r != Z_OK)
244 throw std::runtime_error("Can't deflate data");
245 if(!stream.avail_in)
246 input_clear = true;
248 usize += (OUTBUF_ADVANCE - stream.avail_out);
249 deflateEnd(&stream);
251 ret.resize(usize);
252 return ret;
255 void flush_buffers(bool force)
257 while(!frames.empty() || !samples.empty()) {
258 if(frames.empty() || samples.empty()) {
259 if(!force)
260 return;
261 else if(!frames.empty()) {
262 frame_buffer& f = frames.front();
263 flush_frame(f);
264 frames.pop_front();
265 } else if(!samples.empty()) {
266 sample_buffer& s = samples.front();
267 flush_sample(s);
268 samples.pop_front();
270 continue;
272 frame_buffer& f = frames.front();
273 sample_buffer& s = samples.front();
274 if(f.ts <= s.ts) {
275 flush_frame(f);
276 frames.pop_front();
277 } else {
278 flush_sample(s);
279 samples.pop_front();
284 void flush_frame(frame_buffer& f)
286 //Channel 0, minor 1.
287 char videopacketh[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
288 write32ube(videopacketh + 2, f.ts - last_written_ts);
289 last_written_ts = f.ts;
290 unsigned lneed = 0;
291 uint64_t datasize = f.data.size(); //Possibly upcast to avoid warnings.
292 for(unsigned shift = 63; shift > 0; shift -= 7)
293 if(datasize >= (1ULL << shift))
294 videopacketh[7 + lneed++] = 0x80 | ((datasize >> shift) & 0x7F);
295 videopacketh[7 + lneed++] = (datasize & 0x7F);
297 jmd->write(videopacketh, 7 + lneed);
298 if(!*jmd)
299 throw std::runtime_error("Can't write JMD video packet header");
300 if(datasize > 0)
301 jmd->write(&f.data[0], datasize);
302 if(!*jmd)
303 throw std::runtime_error("Can't write JMD video packet body");
306 void flush_sample(sample_buffer& s)
308 //Channel 1, minor 1, payload 4.
309 char soundpacket[12] = {0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04};
310 write32ube(soundpacket + 2, s.ts - last_written_ts);
311 last_written_ts = s.ts;
312 write16sbe(soundpacket + 8, s.l);
313 write16sbe(soundpacket + 10, s.r);
314 jmd->write(soundpacket, sizeof(soundpacket));
315 if(!*jmd)
316 throw std::runtime_error("Can't write JMD sound packet");
319 std::ostream* jmd;
320 void (*deleter)(void* f);
321 uint64_t last_written_ts;
322 unsigned complevel;
325 jmd_avsnoop* vid_dumper;
327 class adv_jmd_dumper : public adv_dumper
329 public:
330 adv_jmd_dumper() : adv_dumper("INTERNAL-JMD") {information_dispatch::do_dumper_update(); }
331 ~adv_jmd_dumper() throw();
332 std::set<std::string> list_submodes() throw(std::bad_alloc)
334 std::set<std::string> x;
335 x.insert("file");
336 x.insert("tcp");
337 return x;
340 unsigned mode_details(const std::string& mode) throw()
342 return (mode == "tcp") ? target_type_special : target_type_file;
345 std::string name() throw(std::bad_alloc)
347 return "JMD";
350 std::string modename(const std::string& mode) throw(std::bad_alloc)
352 return (mode == "tcp") ? "over TCP/IP" : "to file";
355 bool busy()
357 return (vid_dumper != NULL);
360 void start(const std::string& mode, const std::string& prefix) throw(std::bad_alloc,
361 std::runtime_error)
363 if(prefix == "")
364 throw std::runtime_error("Expected target");
365 if(vid_dumper)
366 throw std::runtime_error("JMD dumping already in progress");
367 try {
368 vid_dumper = new jmd_avsnoop(prefix, mode == "tcp");
369 } catch(std::bad_alloc& e) {
370 throw;
371 } catch(std::exception& e) {
372 std::ostringstream x;
373 x << "Error starting JMD dump: " << e.what();
374 throw std::runtime_error(x.str());
376 messages << "Dumping to " << prefix << " at level " << clevel << std::endl;
377 information_dispatch::do_dumper_update();
380 void end() throw()
382 if(!vid_dumper)
383 throw std::runtime_error("No JMD video dump in progress");
384 try {
385 vid_dumper->on_dump_end();
386 messages << "JMD Dump finished" << std::endl;
387 } catch(std::bad_alloc& e) {
388 throw;
389 } catch(std::exception& e) {
390 messages << "Error ending JMD dump: " << e.what() << std::endl;
392 delete vid_dumper;
393 vid_dumper = NULL;
394 information_dispatch::do_dumper_update();
396 } adv;
398 adv_jmd_dumper::~adv_jmd_dumper() throw()