working j2c as-02
[asdcplib.git] / src / as-02-wrap.cpp
1 /*
2 Copyright (c) 2011-2012, Robert Scheler, Heiko Sparenberg Fraunhofer IIS, John Hurst
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions
7 are met:
8 1. Redistributions of source code must retain the above copyright
9    notice, this list of conditions and the following disclaimer.
10 2. Redistributions in binary form must reproduce the above copyright
11    notice, this list of conditions and the following disclaimer in the
12    documentation and/or other materials provided with the distribution.
13 3. The name of the author may not be used to endorse or promote products
14    derived from this software without specific prior written permission.
15
16 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27 /*! \file    as-02-wrap.cpp
28     \version $Id$       
29     \brief   AS-02 file manipulation utility
30
31   This program wraps IMF essence (picture or sound) in to an AS-02 MXF file.
32
33   For more information about AS-02, please refer to the header file AS_02.h
34   For more information about asdcplib, please refer to the header file AS_DCP.h
35 */
36
37 #include <KM_fileio.h>
38 #include <KM_prng.h>
39 #include <AS_02.h>
40 #include <PCMParserList.h>
41 #include <Metadata.h>
42
43 using namespace ASDCP;
44
45 const ui32_t FRAME_BUFFER_SIZE = 4 * Kumu::Megabyte;
46
47
48 const char*
49 RationalToString(const ASDCP::Rational& r, char* buf, const ui32_t& len)
50 {
51   snprintf(buf, len, "%d/%d", r.Numerator, r.Denominator);
52   return buf;
53 }
54
55 //------------------------------------------------------------------------------------------
56 //
57 // command line option parser class
58
59 static const char* PROGRAM_NAME = "as-02-wrap";  // program name for messages
60
61 // local program identification info written to file headers
62 class MyInfo : public WriterInfo
63 {
64 public:
65   MyInfo()
66   {
67       static byte_t default_ProductUUID_Data[UUIDlen] =
68       { 0x7d, 0x83, 0x6e, 0x16, 0x37, 0xc7, 0x4c, 0x22,
69         0xb2, 0xe0, 0x46, 0xa7, 0x17, 0xe8, 0x4f, 0x42 };
70       
71       memcpy(ProductUUID, default_ProductUUID_Data, UUIDlen);
72       CompanyName = "WidgetCo";
73       ProductName = "as-02-wrap";
74       ProductVersion = ASDCP::Version();
75   }
76 } s_MyInfo;
77
78
79
80 // Increment the iterator, test for an additional non-option command line argument.
81 // Causes the caller to return if there are no remaining arguments or if the next
82 // argument begins with '-'.
83 #define TEST_EXTRA_ARG(i,c)                                             \
84   if ( ++i >= argc || argv[(i)][0] == '-' ) {                           \
85     fprintf(stderr, "Argument not found for option -%c.\n", (c));       \
86     return;                                                             \
87   }
88
89 //
90 void
91 banner(FILE* stream = stdout)
92 {
93   fprintf(stream, "\n\
94 %s (asdcplib %s)\n\n\
95 Copyright (c) 2011-2012, Robert Scheler, Heiko Sparenberg Fraunhofer IIS, John Hurst\n\n\
96 asdcplib may be copied only under the terms of the license found at\n\
97 the top of every file in the asdcplib distribution kit.\n\n\
98 Specify the -h (help) option for further information about %s\n\n",
99           PROGRAM_NAME, ASDCP::Version(), PROGRAM_NAME);
100 }
101
102 //
103 void
104 usage(FILE* stream = stdout)
105 {
106   fprintf(stream, "\
107 USAGE: %s [-h|-help] [-V]\n\
108 \n\
109        %s [-a <uuid>] [-b <buffer-size>] [-C <UL>] [-d <duration>]\n\
110           [-e|-E] [-f <start-frame>] [-j <key-id-string>] [-k <key-string>]\n\
111             [-M] [-p <n>/<d>] [-s <seconds>] [-v] [-W]\n\
112           [-z|-Z] <input-file>+ <output-file>\n\n",
113           PROGRAM_NAME, PROGRAM_NAME);
114
115   fprintf(stream, "\
116 Options:\n\
117   -C <UL>           - Set ChannelAssignment UL value\n\
118   -h | -help        - Show help\n\
119   -V                - Show version information\n\
120   -e                - Encrypt JP2K headers (default)\n\
121   -E                - Do not encrypt JP2K headers\n\
122   -j <key-id-str>   - Write key ID instead of creating a random value\n\
123   -k <key-string>   - Use key for ciphertext operations\n\
124   -M                - Do not create HMAC values when writing\n\
125   -a <UUID>         - Specify the Asset ID of the file\n\
126   -b <buffer-size>  - Specify size in bytes of picture frame buffer\n\
127                       Defaults to 4,194,304 (4MB)\n\
128   -d <duration>     - Number of frames to process, default all\n\
129   -f <start-frame>  - Starting frame number, default 0\n\
130   -p <n>/<d>        - Edit Rate of the output file.  24/1 is the default\n\
131   -s <seconds>      - Duration of a frame-wrapped partition (default 60)\n\
132   -v                - Verbose, prints informative messages to stderr\n\
133   -W                - Read input file only, do not write source file\n\
134   -z                - Fail if j2c inputs have unequal parameters (default)\n\
135   -Z                - Ignore unequal parameters in j2c inputs\n\
136 \n\
137   NOTES: o There is no option grouping, all options must be distinct arguments.\n\
138          o All option arguments must be separated from the option by whitespace.\n\n");
139 }
140
141 //
142 //
143 class CommandOptions
144 {
145   CommandOptions();
146
147 public:
148   bool   error_flag;     // true if the given options are in error or not complete
149   bool   key_flag;       // true if an encryption key was given
150   bool   asset_id_flag;  // true if an asset ID was given
151   bool   encrypt_header_flag; // true if j2c headers are to be encrypted
152   bool   write_hmac;     // true if HMAC values are to be generated and written
153   bool   verbose_flag;   // true if the verbose option was selected
154   ui32_t fb_dump_size;   // number of bytes of frame buffer to dump
155   bool   no_write_flag;  // true if no output files are to be written
156   bool   version_flag;   // true if the version display option was selected
157   bool   help_flag;      // true if the help display option was selected
158   ui32_t start_frame;    // frame number to begin processing
159   ui32_t duration;       // number of frames to be processed
160   bool   j2c_pedantic;   // passed to JP2K::SequenceParser::OpenRead
161   Rational edit_rate;    // edit rate of JP2K sequence
162   ui32_t fb_size;        // size of picture frame buffer
163   byte_t key_value[KeyLen];  // value of given encryption key (when key_flag is true)
164   bool   key_id_flag;    // true if a key ID was given
165   byte_t key_id_value[UUIDlen];// value of given key ID (when key_id_flag is true)
166   byte_t asset_id_value[UUIDlen];// value of asset ID (when asset_id_flag is true)
167   std::string out_file; //
168   bool show_ul_values;    /// if true, dump the UL table before going tp work.
169   Kumu::PathList_t filenames;  // list of filenames to be processed
170   UL channel_assignment;
171
172   //new attributes for AS-02 support 
173   AS_02::IndexStrategy_t index_strategy; //Shim parameter index_strategy_frame/clip
174   ui32_t partition_space; //Shim parameter partition_spacing
175
176   //
177   CommandOptions(int argc, const char** argv) :
178     error_flag(true), key_flag(false), key_id_flag(false), asset_id_flag(false),
179     encrypt_header_flag(true), write_hmac(true), verbose_flag(false), fb_dump_size(0),
180     no_write_flag(false), version_flag(false), help_flag(false), start_frame(0),
181     duration(0xffffffff), j2c_pedantic(true), edit_rate(30,1), fb_size(FRAME_BUFFER_SIZE),
182     show_ul_values(false), index_strategy(AS_02::IS_FOLLOW), partition_space(60)
183   {
184     memset(key_value, 0, KeyLen);
185     memset(key_id_value, 0, UUIDlen);
186
187     for ( int i = 1; i < argc; i++ )
188       {
189
190         if ( (strcmp( argv[i], "-help") == 0) )
191           {
192             help_flag = true;
193             continue;
194           }
195          
196         if ( argv[i][0] == '-'
197              && ( isalpha(argv[i][1]) || isdigit(argv[i][1]) )
198              && argv[i][2] == 0 )
199           {
200             switch ( argv[i][1] )
201               {
202               case 'a':
203                 asset_id_flag = true;
204                 TEST_EXTRA_ARG(i, 'a');
205                 {
206                   ui32_t length;
207                   Kumu::hex2bin(argv[i], asset_id_value, UUIDlen, &length);
208
209                   if ( length != UUIDlen )
210                     {
211                       fprintf(stderr, "Unexpected asset ID length: %u, expecting %u characters.\n", length, UUIDlen);
212                       return;
213                     }
214                 }
215                 break;
216
217               case 'b':
218                 TEST_EXTRA_ARG(i, 'b');
219                 fb_size = abs(atoi(argv[i]));
220
221                 if ( verbose_flag )
222                   fprintf(stderr, "Frame Buffer size: %u bytes.\n", fb_size);
223
224                 break;
225
226               case 'C':
227                 TEST_EXTRA_ARG(i, 'U');
228                 if ( ! channel_assignment.DecodeHex(argv[i]) )
229                   {
230                     fprintf(stderr, "Error decoding UL value: %s\n", argv[i]);
231                     return;
232                   }
233                 break;
234
235               case 'd':
236                 TEST_EXTRA_ARG(i, 'd');
237                 duration = abs(atoi(argv[i]));
238                 break;
239
240               case 'E': encrypt_header_flag = false; break;
241               case 'e': encrypt_header_flag = true; break;
242
243               case 'f':
244                 TEST_EXTRA_ARG(i, 'f');
245                 start_frame = abs(atoi(argv[i]));
246                 break;
247
248               case 'h': help_flag = true; break;
249
250               case 'j': key_id_flag = true;
251                 TEST_EXTRA_ARG(i, 'j');
252                 {
253                   ui32_t length;
254                   Kumu::hex2bin(argv[i], key_id_value, UUIDlen, &length);
255
256                   if ( length != UUIDlen )
257                     {
258                       fprintf(stderr, "Unexpected key ID length: %u, expecting %u characters.\n", length, UUIDlen);
259                       return;
260                     }
261                 }
262                 break;
263
264               case 'k': key_flag = true;
265                 TEST_EXTRA_ARG(i, 'k');
266                 {
267                   ui32_t length;
268                   Kumu::hex2bin(argv[i], key_value, KeyLen, &length);
269
270                   if ( length != KeyLen )
271                     {
272                       fprintf(stderr, "Unexpected key length: %u, expecting %u characters.\n", length, KeyLen);
273                       return;
274                     }
275                 }
276                 break;
277
278               case 'M': write_hmac = false; break;
279
280               case 'p':
281                 TEST_EXTRA_ARG(i, 'p');
282                 /// TODO: VERY BROKEN, WANT RATIONAL
283                 edit_rate.Numerator = abs(atoi(argv[i]));
284                 edit_rate.Denominator = 1;
285                 break;
286
287               case 's':
288                 TEST_EXTRA_ARG(i, 's');
289                 partition_space = abs(atoi(argv[i]));
290                 break;
291
292               case 'V': version_flag = true; break;
293               case 'v': verbose_flag = true; break;
294               case 'W': no_write_flag = true; break;
295               case 'Z': j2c_pedantic = false; break;
296               case 'z': j2c_pedantic = true; break;
297
298               default:
299                 fprintf(stderr, "Unrecognized option: %s\n", argv[i]);
300                 return;
301               }
302           }
303         else
304           {
305
306             if ( argv[i][0] != '-' )
307               {
308                 filenames.push_back(argv[i]);
309               }
310             else
311               {
312                 fprintf(stderr, "Unrecognized argument: %s\n", argv[i]);
313                 return;
314               }
315           }
316       }
317
318     if ( help_flag || version_flag )
319       return;
320     
321     if ( filenames.size() < 2 )
322       {
323         fputs("Option requires at least two filename arguments: <input-file> <output-file>\n", stderr);
324         return;
325       }
326
327     out_file = filenames.back();
328     filenames.pop_back();
329     error_flag = false;
330   }
331 };
332
333
334 //------------------------------------------------------------------------------------------
335 // JPEG 2000 essence
336
337 // Write one or more plaintext JPEG 2000 codestreams to a plaintext AS-02 file
338 // Write one or more plaintext JPEG 2000 codestreams to a ciphertext AS-02 file
339 //
340 Result_t
341 write_JP2K_file(CommandOptions& Options)
342 {
343   AESEncContext*          Context = 0;
344   HMACContext*            HMAC = 0;
345   AS_02::JP2K::MXFWriter  Writer;
346   JP2K::FrameBuffer       FrameBuffer(Options.fb_size);
347   JP2K::PictureDescriptor PDesc;
348   JP2K::SequenceParser    Parser;
349   byte_t                  IV_buf[CBC_BLOCK_SIZE];
350   Kumu::FortunaRNG        RNG;
351
352   // set up essence parser
353   Result_t result = Parser.OpenRead(Options.filenames.front().c_str(), Options.j2c_pedantic);
354
355   // set up MXF writer
356   if ( ASDCP_SUCCESS(result) )
357     {
358       Parser.FillPictureDescriptor(PDesc);
359       PDesc.EditRate = Options.edit_rate;
360
361       if ( Options.verbose_flag )
362         {
363           fprintf(stderr, "JPEG 2000 pictures\n");
364           fputs("PictureDescriptor:\n", stderr);
365           fprintf(stderr, "Frame Buffer size: %u\n", Options.fb_size);
366           JP2K::PictureDescriptorDump(PDesc);
367         }
368     }
369
370   if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag )
371     {
372       WriterInfo Info = s_MyInfo;  // fill in your favorite identifiers here
373       Info.LabelSetType = LS_MXF_SMPTE;
374
375       if ( Options.asset_id_flag )
376         memcpy(Info.AssetUUID, Options.asset_id_value, UUIDlen);
377       else
378         Kumu::GenRandomUUID(Info.AssetUUID);
379
380       // configure encryption
381       if( Options.key_flag )
382         {
383           Kumu::GenRandomUUID(Info.ContextID);
384           Info.EncryptedEssence = true;
385
386           if ( Options.key_id_flag )
387             memcpy(Info.CryptographicKeyID, Options.key_id_value, UUIDlen);
388           else
389             RNG.FillRandom(Info.CryptographicKeyID, UUIDlen);
390
391           Context = new AESEncContext;
392           result = Context->InitKey(Options.key_value);
393
394           if ( ASDCP_SUCCESS(result) )
395             result = Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE));
396
397           if ( ASDCP_SUCCESS(result) && Options.write_hmac )
398             {
399               Info.UsesHMAC = true;
400               HMAC = new HMACContext;
401               result = HMAC->InitKey(Options.key_value, Info.LabelSetType);
402             }
403         }
404
405       if ( ASDCP_SUCCESS(result) )
406         result = Writer.OpenWrite(Options.out_file.c_str(), Info, PDesc, Options.index_strategy, Options.partition_space);
407     }
408
409   if ( ASDCP_SUCCESS(result) )
410     {
411       ui32_t duration = 0;
412       result = Parser.Reset();
413
414       while ( ASDCP_SUCCESS(result) && duration++ < Options.duration )
415         {
416           result = Parser.ReadFrame(FrameBuffer);
417           
418           if ( ASDCP_SUCCESS(result) )
419             {
420               if ( Options.verbose_flag )
421                 FrameBuffer.Dump(stderr, Options.fb_dump_size);
422               
423               if ( Options.encrypt_header_flag )
424                 FrameBuffer.PlaintextOffset(0);
425             }
426
427           if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag )
428             {
429               result = Writer.WriteFrame(FrameBuffer, Context, HMAC);
430
431               // The Writer class will forward the last block of ciphertext
432               // to the encryption context for use as the IV for the next
433               // frame. If you want to use non-sequitur IV values, un-comment
434               // the following  line of code.
435               // if ( ASDCP_SUCCESS(result) && Options.key_flag )
436               //   Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE));
437             }
438         }
439
440       if ( result == RESULT_ENDOFFILE )
441         result = RESULT_OK;
442     }
443
444   if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag )
445     result = Writer.Finalize();
446
447   return result;
448 }
449
450 //------------------------------------------------------------------------------------------
451 // PCM essence
452
453
454 // Write one or more plaintext PCM audio streams to a plaintext ASDCP file
455 // Write one or more plaintext PCM audio streams to a ciphertext ASDCP file
456 //
457 Result_t
458 write_PCM_file(CommandOptions& Options)
459 {
460   AESEncContext*    Context = 0;
461   HMACContext*      HMAC = 0;
462   PCMParserList     Parser;
463   AS_02::PCM::MXFWriter    Writer;
464   PCM::FrameBuffer  FrameBuffer;
465   PCM::AudioDescriptor ADesc;
466   byte_t            IV_buf[CBC_BLOCK_SIZE];
467   Kumu::FortunaRNG  RNG;
468
469   // set up essence parser
470   Result_t result = Parser.OpenRead(Options.filenames, Rational(1, 1));
471
472   // set up MXF writer
473   if ( ASDCP_SUCCESS(result) )
474     {
475       Parser.FillAudioDescriptor(ADesc);
476
477       ADesc.EditRate = Options.edit_rate;
478       FrameBuffer.Capacity(PCM::CalcFrameBufferSize(ADesc));
479
480       if ( Options.verbose_flag )
481         {
482           char buf[64];
483           fprintf(stderr, "%.1fkHz PCM Audio, %s fps (%u spf)\n",
484                   ADesc.AudioSamplingRate.Quotient() / 1000.0,
485                   RationalToString(Options.edit_rate, buf, 64),
486                   PCM::CalcSamplesPerFrame(ADesc));
487           fputs("AudioDescriptor:\n", stderr);
488           PCM::AudioDescriptorDump(ADesc);
489         }
490     }
491
492   if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag )
493     {
494       WriterInfo Info = s_MyInfo;  // fill in your favorite identifiers here
495       Info.LabelSetType = LS_MXF_SMPTE;
496
497       if ( Options.asset_id_flag )
498         memcpy(Info.AssetUUID, Options.asset_id_value, UUIDlen);
499       else
500         Kumu::GenRandomUUID(Info.AssetUUID);
501
502       // configure encryption
503       if( Options.key_flag )
504         {
505           Kumu::GenRandomUUID(Info.ContextID);
506           Info.EncryptedEssence = true;
507
508           if ( Options.key_id_flag )
509             memcpy(Info.CryptographicKeyID, Options.key_id_value, UUIDlen);
510           else
511             RNG.FillRandom(Info.CryptographicKeyID, UUIDlen);
512
513           Context = new AESEncContext;
514           result = Context->InitKey(Options.key_value);
515
516           if ( ASDCP_SUCCESS(result) )
517             result = Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE));
518
519           if ( ASDCP_SUCCESS(result) && Options.write_hmac )
520             {
521               Info.UsesHMAC = true;
522               HMAC = new HMACContext;
523               result = HMAC->InitKey(Options.key_value, Info.LabelSetType);
524             }
525         }
526
527       if ( ASDCP_SUCCESS(result) )
528         result = Writer.OpenWrite(Options.out_file.c_str(), Info, ADesc);
529
530       if ( ASDCP_SUCCESS(result) && Options.channel_assignment.HasValue() )
531         {
532           MXF::WaveAudioDescriptor *descriptor = 0;
533           Writer.OP1aHeader().GetMDObjectByType(DefaultSMPTEDict().ul(MDD_WaveAudioDescriptor),
534                                                 reinterpret_cast<MXF::InterchangeObject**>(&descriptor));
535           descriptor->ChannelAssignment = Options.channel_assignment;
536         }
537     }
538
539   if ( ASDCP_SUCCESS(result) )
540     {
541       result = Parser.Reset();
542       ui32_t duration = 0;
543
544       while ( ASDCP_SUCCESS(result) && duration++ < Options.duration )
545         {
546           result = Parser.ReadFrame(FrameBuffer);
547
548           if ( ASDCP_SUCCESS(result) )
549             {
550               if ( FrameBuffer.Size() != FrameBuffer.Capacity() )
551                 {
552                   fprintf(stderr, "WARNING: Last frame read was short, PCM input is possibly not frame aligned.\n");
553                   fprintf(stderr, "Expecting %u bytes, got %u.\n", FrameBuffer.Capacity(), FrameBuffer.Size());
554                   result = RESULT_ENDOFFILE;
555                   continue;
556                 }
557
558               if ( Options.verbose_flag )
559                 FrameBuffer.Dump(stderr, Options.fb_dump_size);
560
561               if ( ! Options.no_write_flag )
562                 {
563                   result = Writer.WriteFrame(FrameBuffer, Context, HMAC);
564
565                   // The Writer class will forward the last block of ciphertext
566                   // to the encryption context for use as the IV for the next
567                   // frame. If you want to use non-sequitur IV values, un-comment
568                   // the following  line of code.
569                   // if ( ASDCP_SUCCESS(result) && Options.key_flag )
570                   //   Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE));
571                 }
572             }
573         }
574
575       if ( result == RESULT_ENDOFFILE )
576         result = RESULT_OK;
577     }
578
579   if ( ASDCP_SUCCESS(result) && ! Options.no_write_flag )
580     result = Writer.Finalize();
581
582   return result;
583 }
584
585
586 //
587 int
588 main(int argc, const char** argv)
589 {
590   Result_t result = RESULT_OK;
591   char     str_buf[64];
592   CommandOptions Options(argc, argv);
593
594   if ( Options.version_flag )
595     banner();
596
597   if ( Options.help_flag )
598     usage();
599
600   if ( Options.version_flag || Options.help_flag )
601     return 0;
602
603   if ( Options.error_flag )
604     {
605       fprintf(stderr, "There was a problem. Type %s -h for help.\n", PROGRAM_NAME);
606       return 3;
607     }
608
609   if ( Options.show_ul_values )
610     {
611       DefaultSMPTEDict().Dump(stdout);
612     }
613
614   EssenceType_t EssenceType;
615   result = ASDCP::RawEssenceType(Options.filenames.front().c_str(), EssenceType);
616
617   if ( ASDCP_SUCCESS(result) )
618     {
619       switch ( EssenceType )
620         {
621         case ESS_JPEG_2000:
622           result = write_JP2K_file(Options);
623           break;
624
625         case ESS_PCM_24b_48k:
626         case ESS_PCM_24b_96k:
627           result = write_PCM_file(Options);
628           break;
629
630         default:
631           fprintf(stderr, "%s: Unknown file type, not ASDCP-compatible essence.\n",
632                   Options.filenames.front().c_str());
633           return 5;
634         }
635     }
636
637   if ( ASDCP_FAILURE(result) )
638     {
639       fputs("Program stopped on error.\n", stderr);
640
641       if ( result != RESULT_FAIL )
642         {
643           fputs(result, stderr);
644           fputc('\n', stderr);
645         }
646
647       return 1;
648     }
649
650   return 0;
651 }
652
653
654 //
655 // end as-02-wrap.cpp
656 //