1
by Reinhard Tartler
Import upstream version 0.cvs20060210 |
1 |
/*****************************************************************************
|
2 |
* matroska.c:
|
|
3 |
*****************************************************************************
|
|
4 |
* Copyright (C) 2005 x264 project
|
|
5 |
* $Id: $
|
|
6 |
*
|
|
7 |
* Authors: Mike Matsnev
|
|
8 |
*
|
|
9 |
* This program is free software; you can redistribute it and/or modify
|
|
10 |
* it under the terms of the GNU General Public License as published by
|
|
11 |
* the Free Software Foundation; either version 2 of the License, or
|
|
12 |
* (at your option) any later version.
|
|
13 |
*
|
|
14 |
* This program is distributed in the hope that it will be useful,
|
|
15 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17 |
* GNU General Public License for more details.
|
|
18 |
*
|
|
19 |
* You should have received a copy of the GNU General Public License
|
|
20 |
* along with this program; if not, write to the Free Software
|
|
21 |
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA.
|
|
22 |
*****************************************************************************/
|
|
23 |
||
24 |
#define _LARGEFILE_SOURCE
|
|
25 |
#define _FILE_OFFSET_BITS 64
|
|
26 |
||
27 |
#include <stdio.h> |
|
28 |
#include <stdlib.h> |
|
29 |
#include <string.h> |
|
30 |
||
31 |
#ifdef HAVE_STDINT_H
|
|
32 |
#include <stdint.h> |
|
33 |
#else
|
|
34 |
#include <inttypes.h> |
|
35 |
#endif
|
|
36 |
||
37 |
#include "matroska.h" |
|
38 |
||
39 |
#define CLSIZE 1048576
|
|
40 |
#define CHECK(x) do { if ((x) < 0) return -1; } while (0)
|
|
41 |
||
42 |
struct mk_Context { |
|
43 |
struct mk_Context *next, **prev, *parent; |
|
44 |
struct mk_Writer *owner; |
|
45 |
unsigned id; |
|
46 |
||
47 |
void *data; |
|
48 |
unsigned d_cur, d_max; |
|
49 |
};
|
|
50 |
||
51 |
typedef struct mk_Context mk_Context; |
|
52 |
||
53 |
struct mk_Writer { |
|
54 |
FILE *fp; |
|
55 |
||
56 |
unsigned duration_ptr; |
|
57 |
||
58 |
mk_Context *root, *cluster, *frame; |
|
59 |
mk_Context *freelist; |
|
60 |
mk_Context *actlist; |
|
61 |
||
62 |
int64_t def_duration; |
|
63 |
int64_t timescale; |
|
64 |
int64_t cluster_tc_scaled; |
|
65 |
int64_t frame_tc, prev_frame_tc_scaled, max_frame_tc; |
|
66 |
||
67 |
char wrote_header, in_frame, keyframe; |
|
68 |
};
|
|
69 |
||
70 |
static mk_Context *mk_createContext(mk_Writer *w, mk_Context *parent, unsigned id) { |
|
71 |
mk_Context *c; |
|
72 |
||
73 |
if (w->freelist) { |
|
74 |
c = w->freelist; |
|
75 |
w->freelist = w->freelist->next; |
|
76 |
} else { |
|
77 |
c = malloc(sizeof(*c)); |
|
78 |
memset(c, 0, sizeof(*c)); |
|
79 |
}
|
|
80 |
||
81 |
if (c == NULL) |
|
82 |
return NULL; |
|
83 |
||
84 |
c->parent = parent; |
|
85 |
c->owner = w; |
|
86 |
c->id = id; |
|
87 |
||
88 |
if (c->owner->actlist) |
|
89 |
c->owner->actlist->prev = &c->next; |
|
90 |
c->next = c->owner->actlist; |
|
91 |
c->prev = &c->owner->actlist; |
|
92 |
||
93 |
return c; |
|
94 |
}
|
|
95 |
||
96 |
static int mk_appendContextData(mk_Context *c, const void *data, unsigned size) { |
|
97 |
unsigned ns = c->d_cur + size; |
|
98 |
||
99 |
if (ns > c->d_max) { |
|
100 |
void *dp; |
|
101 |
unsigned dn = c->d_max ? c->d_max << 1 : 16; |
|
102 |
while (ns > dn) |
|
103 |
dn <<= 1; |
|
104 |
||
105 |
dp = realloc(c->data, dn); |
|
106 |
if (dp == NULL) |
|
107 |
return -1; |
|
108 |
||
109 |
c->data = dp; |
|
110 |
c->d_max = dn; |
|
111 |
}
|
|
112 |
||
113 |
memcpy((char*)c->data + c->d_cur, data, size); |
|
114 |
||
115 |
c->d_cur = ns; |
|
116 |
||
117 |
return 0; |
|
118 |
}
|
|
119 |
||
120 |
static int mk_writeID(mk_Context *c, unsigned id) { |
|
121 |
unsigned char c_id[4] = { id >> 24, id >> 16, id >> 8, id }; |
|
122 |
||
123 |
if (c_id[0]) |
|
124 |
return mk_appendContextData(c, c_id, 4); |
|
125 |
if (c_id[1]) |
|
126 |
return mk_appendContextData(c, c_id+1, 3); |
|
127 |
if (c_id[2]) |
|
128 |
return mk_appendContextData(c, c_id+2, 2); |
|
129 |
return mk_appendContextData(c, c_id+3, 1); |
|
130 |
}
|
|
131 |
||
132 |
static int mk_writeSize(mk_Context *c, unsigned size) { |
|
133 |
unsigned char c_size[5] = { 0x08, size >> 24, size >> 16, size >> 8, size }; |
|
134 |
||
135 |
if (size < 0x7f) { |
|
136 |
c_size[4] |= 0x80; |
|
137 |
return mk_appendContextData(c, c_size+4, 1); |
|
138 |
}
|
|
139 |
if (size < 0x3fff) { |
|
140 |
c_size[3] |= 0x40; |
|
141 |
return mk_appendContextData(c, c_size+3, 2); |
|
142 |
}
|
|
143 |
if (size < 0x1fffff) { |
|
144 |
c_size[2] |= 0x20; |
|
145 |
return mk_appendContextData(c, c_size+2, 3); |
|
146 |
}
|
|
147 |
if (size < 0x0fffffff) { |
|
148 |
c_size[1] |= 0x10; |
|
149 |
return mk_appendContextData(c, c_size+1, 4); |
|
150 |
}
|
|
151 |
return mk_appendContextData(c, c_size, 5); |
|
152 |
}
|
|
153 |
||
154 |
static int mk_flushContextID(mk_Context *c) { |
|
155 |
unsigned char ff = 0xff; |
|
156 |
||
157 |
if (c->id == 0) |
|
158 |
return 0; |
|
159 |
||
160 |
CHECK(mk_writeID(c->parent, c->id)); |
|
161 |
CHECK(mk_appendContextData(c->parent, &ff, 1)); |
|
162 |
||
163 |
c->id = 0; |
|
164 |
||
165 |
return 0; |
|
166 |
}
|
|
167 |
||
168 |
static int mk_flushContextData(mk_Context *c) { |
|
169 |
if (c->d_cur == 0) |
|
170 |
return 0; |
|
171 |
||
172 |
if (c->parent) |
|
173 |
CHECK(mk_appendContextData(c->parent, c->data, c->d_cur)); |
|
174 |
else
|
|
175 |
if (fwrite(c->data, c->d_cur, 1, c->owner->fp) != 1) |
|
176 |
return -1; |
|
177 |
||
178 |
c->d_cur = 0; |
|
179 |
||
180 |
return 0; |
|
181 |
}
|
|
182 |
||
183 |
static int mk_closeContext(mk_Context *c, unsigned *off) { |
|
184 |
if (c->id) { |
|
185 |
CHECK(mk_writeID(c->parent, c->id)); |
|
186 |
CHECK(mk_writeSize(c->parent, c->d_cur)); |
|
187 |
}
|
|
188 |
||
189 |
if (c->parent && off != NULL) |
|
190 |
*off += c->parent->d_cur; |
|
191 |
||
192 |
CHECK(mk_flushContextData(c)); |
|
193 |
||
194 |
if (c->next) |
|
195 |
c->next->prev = c->prev; |
|
196 |
*(c->prev) = c->next; |
|
197 |
c->next = c->owner->freelist; |
|
198 |
c->owner->freelist = c; |
|
199 |
||
200 |
return 0; |
|
201 |
}
|
|
202 |
||
203 |
static void mk_destroyContexts(mk_Writer *w) { |
|
204 |
mk_Context *cur, *next; |
|
205 |
||
206 |
for (cur = w->freelist; cur; cur = next) { |
|
207 |
next = cur->next; |
|
208 |
free(cur->data); |
|
209 |
free(cur); |
|
210 |
}
|
|
211 |
||
212 |
for (cur = w->actlist; cur; cur = next) { |
|
213 |
next = cur->next; |
|
214 |
free(cur->data); |
|
215 |
free(cur); |
|
216 |
}
|
|
217 |
||
218 |
w->freelist = w->actlist = w->root = NULL; |
|
219 |
}
|
|
220 |
||
221 |
static int mk_writeStr(mk_Context *c, unsigned id, const char *str) { |
|
222 |
size_t len = strlen(str); |
|
223 |
||
224 |
CHECK(mk_writeID(c, id)); |
|
225 |
CHECK(mk_writeSize(c, len)); |
|
226 |
CHECK(mk_appendContextData(c, str, len)); |
|
227 |
return 0; |
|
228 |
}
|
|
229 |
||
230 |
static int mk_writeBin(mk_Context *c, unsigned id, const void *data, unsigned size) { |
|
231 |
CHECK(mk_writeID(c, id)); |
|
232 |
CHECK(mk_writeSize(c, size)); |
|
233 |
CHECK(mk_appendContextData(c, data, size)); |
|
234 |
return 0; |
|
235 |
}
|
|
236 |
||
237 |
static int mk_writeUInt(mk_Context *c, unsigned id, int64_t ui) { |
|
238 |
unsigned char c_ui[8] = { ui >> 56, ui >> 48, ui >> 40, ui >> 32, ui >> 24, ui >> 16, ui >> 8, ui }; |
|
239 |
unsigned i = 0; |
|
240 |
||
241 |
CHECK(mk_writeID(c, id)); |
|
242 |
while (i < 7 && c_ui[i] == 0) |
|
243 |
++i; |
|
244 |
CHECK(mk_writeSize(c, 8 - i)); |
|
245 |
CHECK(mk_appendContextData(c, c_ui+i, 8 - i)); |
|
246 |
return 0; |
|
247 |
}
|
|
248 |
||
249 |
static int mk_writeSInt(mk_Context *c, unsigned id, int64_t si) { |
|
250 |
unsigned char c_si[8] = { si >> 56, si >> 48, si >> 40, si >> 32, si >> 24, si >> 16, si >> 8, si }; |
|
251 |
unsigned i = 0; |
|
252 |
||
253 |
CHECK(mk_writeID(c, id)); |
|
254 |
if (si < 0) |
|
255 |
while (i < 7 && c_si[i] == 0xff && c_si[i+1] & 0x80) |
|
256 |
++i; |
|
257 |
else
|
|
258 |
while (i < 7 && c_si[i] == 0 && !(c_si[i+1] & 0x80)) |
|
259 |
++i; |
|
260 |
CHECK(mk_writeSize(c, 8 - i)); |
|
261 |
CHECK(mk_appendContextData(c, c_si+i, 8 - i)); |
|
262 |
return 0; |
|
263 |
}
|
|
264 |
||
265 |
static int mk_writeFloatRaw(mk_Context *c, float f) { |
|
266 |
union { |
|
267 |
float f; |
|
268 |
unsigned u; |
|
269 |
} u; |
|
270 |
unsigned char c_f[4]; |
|
271 |
||
272 |
u.f = f; |
|
273 |
c_f[0] = u.u >> 24; |
|
274 |
c_f[1] = u.u >> 16; |
|
275 |
c_f[2] = u.u >> 8; |
|
276 |
c_f[3] = u.u; |
|
277 |
||
278 |
return mk_appendContextData(c, c_f, 4); |
|
279 |
}
|
|
280 |
||
281 |
static int mk_writeFloat(mk_Context *c, unsigned id, float f) { |
|
282 |
CHECK(mk_writeID(c, id)); |
|
283 |
CHECK(mk_writeSize(c, 4)); |
|
284 |
CHECK(mk_writeFloatRaw(c, f)); |
|
285 |
return 0; |
|
286 |
}
|
|
287 |
||
288 |
static unsigned mk_ebmlSizeSize(unsigned s) { |
|
289 |
if (s < 0x7f) |
|
290 |
return 1; |
|
291 |
if (s < 0x3fff) |
|
292 |
return 2; |
|
293 |
if (s < 0x1fffff) |
|
294 |
return 3; |
|
295 |
if (s < 0x0fffffff) |
|
296 |
return 4; |
|
297 |
return 5; |
|
298 |
}
|
|
299 |
||
300 |
static unsigned mk_ebmlSIntSize(int64_t si) { |
|
301 |
unsigned char c_si[8] = { si >> 56, si >> 48, si >> 40, si >> 32, si >> 24, si >> 16, si >> 8, si }; |
|
302 |
unsigned i = 0; |
|
303 |
||
304 |
if (si < 0) |
|
305 |
while (i < 7 && c_si[i] == 0xff && c_si[i+1] & 0x80) |
|
306 |
++i; |
|
307 |
else
|
|
308 |
while (i < 7 && c_si[i] == 0 && !(c_si[i+1] & 0x80)) |
|
309 |
++i; |
|
310 |
||
311 |
return 8 - i; |
|
312 |
}
|
|
313 |
||
314 |
mk_Writer *mk_createWriter(const char *filename) { |
|
315 |
mk_Writer *w = malloc(sizeof(*w)); |
|
316 |
if (w == NULL) |
|
317 |
return NULL; |
|
318 |
||
319 |
memset(w, 0, sizeof(*w)); |
|
320 |
||
321 |
w->root = mk_createContext(w, NULL, 0); |
|
322 |
if (w->root == NULL) { |
|
323 |
free(w); |
|
324 |
return NULL; |
|
325 |
}
|
|
326 |
||
327 |
w->fp = fopen(filename, "wb"); |
|
328 |
if (w->fp == NULL) { |
|
329 |
mk_destroyContexts(w); |
|
330 |
free(w); |
|
331 |
return NULL; |
|
332 |
}
|
|
333 |
||
334 |
w->timescale = 1000000; |
|
335 |
||
336 |
return w; |
|
337 |
}
|
|
338 |
||
339 |
int mk_writeHeader(mk_Writer *w, const char *writingApp, |
|
340 |
const char *codecID, |
|
341 |
const void *codecPrivate, unsigned codecPrivateSize, |
|
342 |
int64_t default_frame_duration, |
|
343 |
int64_t timescale, |
|
344 |
unsigned width, unsigned height, |
|
345 |
unsigned d_width, unsigned d_height) |
|
346 |
{
|
|
347 |
mk_Context *c, *ti, *v; |
|
348 |
||
349 |
if (w->wrote_header) |
|
350 |
return -1; |
|
351 |
||
352 |
w->timescale = timescale; |
|
353 |
w->def_duration = default_frame_duration; |
|
354 |
||
355 |
if ((c = mk_createContext(w, w->root, 0x1a45dfa3)) == NULL) // EBML |
|
356 |
return -1; |
|
357 |
CHECK(mk_writeUInt(c, 0x4286, 1)); // EBMLVersion |
|
358 |
CHECK(mk_writeUInt(c, 0x42f7, 1)); // EBMLReadVersion |
|
359 |
CHECK(mk_writeUInt(c, 0x42f2, 4)); // EBMLMaxIDLength |
|
360 |
CHECK(mk_writeUInt(c, 0x42f3, 8)); // EBMLMaxSizeLength |
|
361 |
CHECK(mk_writeStr(c, 0x4282, "matroska")); // DocType |
|
362 |
CHECK(mk_writeUInt(c, 0x4287, 1)); // DocTypeVersion |
|
363 |
CHECK(mk_writeUInt(c, 0x4285, 1)); // DocTypeReadversion |
|
364 |
CHECK(mk_closeContext(c, 0)); |
|
365 |
||
366 |
if ((c = mk_createContext(w, w->root, 0x18538067)) == NULL) // Segment |
|
367 |
return -1; |
|
368 |
CHECK(mk_flushContextID(c)); |
|
369 |
CHECK(mk_closeContext(c, 0)); |
|
370 |
||
371 |
if ((c = mk_createContext(w, w->root, 0x1549a966)) == NULL) // SegmentInfo |
|
372 |
return -1; |
|
373 |
CHECK(mk_writeStr(c, 0x4d80, "Haali Matroska Writer b0")); |
|
374 |
CHECK(mk_writeStr(c, 0x5741, writingApp)); |
|
375 |
CHECK(mk_writeUInt(c, 0x2ad7b1, w->timescale)); |
|
376 |
CHECK(mk_writeFloat(c, 0x4489, 0)); |
|
377 |
w->duration_ptr = c->d_cur - 4; |
|
378 |
CHECK(mk_closeContext(c, &w->duration_ptr)); |
|
379 |
||
380 |
if ((c = mk_createContext(w, w->root, 0x1654ae6b)) == NULL) // tracks |
|
381 |
return -1; |
|
382 |
if ((ti = mk_createContext(w, c, 0xae)) == NULL) // TrackEntry |
|
383 |
return -1; |
|
384 |
CHECK(mk_writeUInt(ti, 0xd7, 1)); // TrackNumber |
|
385 |
CHECK(mk_writeUInt(ti, 0x73c5, 1)); // TrackUID |
|
386 |
CHECK(mk_writeUInt(ti, 0x83, 1)); // TrackType |
|
387 |
CHECK(mk_writeUInt(ti, 0x9c, 0)); // FlagLacing |
|
388 |
CHECK(mk_writeStr(ti, 0x86, codecID)); // CodecID |
|
389 |
if (codecPrivateSize) |
|
390 |
CHECK(mk_writeBin(ti, 0x63a2, codecPrivate, codecPrivateSize)); // CodecPrivate |
|
391 |
if (default_frame_duration) |
|
392 |
CHECK(mk_writeUInt(ti, 0x23e383, default_frame_duration)); // DefaultDuration |
|
393 |
||
394 |
if ((v = mk_createContext(w, ti, 0xe0)) == NULL) // Video |
|
395 |
return -1; |
|
396 |
CHECK(mk_writeUInt(v, 0xb0, width)); |
|
397 |
CHECK(mk_writeUInt(v, 0xba, height)); |
|
398 |
CHECK(mk_writeUInt(v, 0x54b0, d_width)); |
|
399 |
CHECK(mk_writeUInt(v, 0x54ba, d_height)); |
|
400 |
CHECK(mk_closeContext(v, 0)); |
|
401 |
||
402 |
CHECK(mk_closeContext(ti, 0)); |
|
403 |
||
404 |
CHECK(mk_closeContext(c, 0)); |
|
405 |
||
406 |
CHECK(mk_flushContextData(w->root)); |
|
407 |
||
408 |
w->wrote_header = 1; |
|
409 |
||
410 |
return 0; |
|
411 |
}
|
|
412 |
||
413 |
static int mk_closeCluster(mk_Writer *w) { |
|
414 |
if (w->cluster == NULL) |
|
415 |
return 0; |
|
416 |
CHECK(mk_closeContext(w->cluster, 0)); |
|
417 |
w->cluster = NULL; |
|
418 |
CHECK(mk_flushContextData(w->root)); |
|
419 |
return 0; |
|
420 |
}
|
|
421 |
||
422 |
int mk_flushFrame(mk_Writer *w) { |
|
423 |
int64_t delta, ref = 0; |
|
424 |
unsigned fsize, bgsize; |
|
425 |
unsigned char c_delta_flags[3]; |
|
426 |
||
427 |
if (!w->in_frame) |
|
428 |
return 0; |
|
429 |
||
430 |
delta = w->frame_tc/w->timescale - w->cluster_tc_scaled; |
|
431 |
if (delta > 32767ll || delta < -32768ll) |
|
432 |
CHECK(mk_closeCluster(w)); |
|
433 |
||
434 |
if (w->cluster == NULL) { |
|
435 |
w->cluster_tc_scaled = w->frame_tc / w->timescale; |
|
436 |
w->cluster = mk_createContext(w, w->root, 0x1f43b675); // Cluster |
|
437 |
if (w->cluster == NULL) |
|
438 |
return -1; |
|
439 |
||
440 |
CHECK(mk_writeUInt(w->cluster, 0xe7, w->cluster_tc_scaled)); // Timecode |
|
441 |
||
442 |
delta = 0; |
|
443 |
}
|
|
444 |
||
445 |
fsize = w->frame ? w->frame->d_cur : 0; |
|
446 |
bgsize = fsize + 4 + mk_ebmlSizeSize(fsize + 4) + 1; |
|
447 |
if (!w->keyframe) { |
|
448 |
ref = w->prev_frame_tc_scaled - w->cluster_tc_scaled - delta; |
|
449 |
bgsize += 1 + 1 + mk_ebmlSIntSize(ref); |
|
450 |
}
|
|
451 |
||
452 |
CHECK(mk_writeID(w->cluster, 0xa0)); // BlockGroup |
|
453 |
CHECK(mk_writeSize(w->cluster, bgsize)); |
|
454 |
CHECK(mk_writeID(w->cluster, 0xa1)); // Block |
|
455 |
CHECK(mk_writeSize(w->cluster, fsize + 4)); |
|
456 |
CHECK(mk_writeSize(w->cluster, 1)); // track number |
|
457 |
||
458 |
c_delta_flags[0] = delta >> 8; |
|
459 |
c_delta_flags[1] = delta; |
|
460 |
c_delta_flags[2] = 0; |
|
461 |
CHECK(mk_appendContextData(w->cluster, c_delta_flags, 3)); |
|
462 |
if (w->frame) { |
|
463 |
CHECK(mk_appendContextData(w->cluster, w->frame->data, w->frame->d_cur)); |
|
464 |
w->frame->d_cur = 0; |
|
465 |
}
|
|
466 |
if (!w->keyframe) |
|
467 |
CHECK(mk_writeSInt(w->cluster, 0xfb, ref)); // ReferenceBlock |
|
468 |
||
469 |
w->in_frame = 0; |
|
470 |
w->prev_frame_tc_scaled = w->cluster_tc_scaled + delta; |
|
471 |
||
472 |
if (w->cluster->d_cur > CLSIZE) |
|
473 |
CHECK(mk_closeCluster(w)); |
|
474 |
||
475 |
return 0; |
|
476 |
}
|
|
477 |
||
478 |
int mk_startFrame(mk_Writer *w) { |
|
479 |
if (mk_flushFrame(w) < 0) |
|
480 |
return -1; |
|
481 |
||
482 |
w->in_frame = 1; |
|
483 |
w->keyframe = 0; |
|
484 |
||
485 |
return 0; |
|
486 |
}
|
|
487 |
||
488 |
int mk_setFrameFlags(mk_Writer *w,int64_t timestamp, int keyframe) { |
|
489 |
if (!w->in_frame) |
|
490 |
return -1; |
|
491 |
||
492 |
w->frame_tc = timestamp; |
|
493 |
w->keyframe = keyframe != 0; |
|
494 |
||
495 |
if (w->max_frame_tc < timestamp) |
|
496 |
w->max_frame_tc = timestamp; |
|
497 |
||
498 |
return 0; |
|
499 |
}
|
|
500 |
||
501 |
int mk_addFrameData(mk_Writer *w, const void *data, unsigned size) { |
|
502 |
if (!w->in_frame) |
|
503 |
return -1; |
|
504 |
||
505 |
if (w->frame == NULL) |
|
506 |
if ((w->frame = mk_createContext(w, NULL, 0)) == NULL) |
|
507 |
return -1; |
|
508 |
||
509 |
return mk_appendContextData(w->frame, data, size); |
|
510 |
}
|
|
511 |
||
512 |
int mk_close(mk_Writer *w) { |
|
513 |
int ret = 0; |
|
514 |
if (mk_flushFrame(w) < 0 || mk_closeCluster(w) < 0) |
|
515 |
ret = -1; |
|
516 |
if (w->wrote_header) { |
|
517 |
fseek(w->fp, w->duration_ptr, SEEK_SET); |
|
518 |
if (mk_writeFloatRaw(w->root, (float)((double)(w->max_frame_tc+w->def_duration) / w->timescale)) < 0 || |
|
519 |
mk_flushContextData(w->root) < 0) |
|
520 |
ret = -1; |
|
521 |
}
|
|
522 |
mk_destroyContexts(w); |
|
523 |
fclose(w->fp); |
|
524 |
free(w); |
|
525 |
return ret; |
|
526 |
}
|
|
527 |