version/0.1
[cdimgtools.git] / nrgtool
blob0053d8e27588e14cd9f9d392f5334e1466011358
1 #!/usr/bin/perl -w
2 # Copyright © 2012 Géraud Meyer <graud@gmx.com>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License version 2 as
5 # published by the Free Software Foundation.
7 =encoding utf-8
9 =head1 NAME
11 nrgtool - reads and converts/splits Nero CD images (.nrg)
13 =head1 SYNOPSIS
15 B<nrgtool> B<-h>
17 B<nrgtool> S<[ B<-v> ]> S<[ B<-n> ]> S<[ B<-f> ]> S<[ B<-1> ]> I<nrg> S<[ I<cmd> I<args> ]>
19 =cut
21 use strict;
22 use integer;
24 use Getopt::Long;
25 Getopt::Long::Configure('bundling', 'no_auto_abbrev', 'auto_version', 'auto_help');
26 use Pod::Usage;
27 use String::Escape qw(printable);
28 use Data::Hexdumper qw(hexdump);
29 use Fcntl qw(:seek);
31 $main::VERSION = "0.1";
33 # Command line
34 my ($verbose, $no_act, $force) = (0, undef, undef);
35 my ($nrgv, $ext, $iff_ext, $trk_ext) = (2, '', '.iff', '.s%02d-t%02d');
36 die Getopt::Long::HelpMessage(128)
37 unless GetOptions(
38 'h|help' => sub { Pod::Usage::pod2usage(-exitval => 0, -verbose => 2) },
39 'v|verbose+' => \$verbose,
40 'q|quiet' => sub { $verbose = 0 },
41 'n|no-act!' => \$no_act,
42 'f|force!' => \$force,
43 '2|nrgv2' => sub { $nrgv = 2 },
44 '1|nrgv1' => sub { $nrgv = 1 },
45 'e|extension=s' => \$ext,
46 't|track-ext=s' => \$trk_ext,
47 'i|iff-ext=s' => \$iff_ext,
48 ) and my $nrg_name = shift;
49 $ARGV[0] = "info" unless (@ARGV);
51 # Data pertaining to the NRG file format IFF structure
52 die "ERROR For now only NRG version 2 files are supported" if ($nrgv != 2);
53 my $chunk_types;
54 my $Q = 'Q>'; # unpack template for 64-bit values
55 $chunk_types->{nero} = {
56 code => "NER5",
57 fields => [ [ 'iff_offset', 8, $Q ] ],
59 $chunk_types->{sinf} = {
60 code => "SINF",
61 fields => [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
63 $chunk_types->{etnf} = {
64 code => "ETN2",
65 fields => [ [ 'size', 4, 'N' ] ],
66 lfields => [
67 [ 'start', 8, $Q ],
68 [ 'tsize', 8, $Q ],
69 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
70 [ 'lba', 4, 'N' ],
71 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
72 [ 'length', 4, 'N' ] ],
74 $chunk_types->{daoi} = {
75 code => "DAOX",
76 fields => [
77 [ 'size', 4, 'N' ], [ 'le_size', 4, 'N' ],
78 [ 'upc', 13, 'a13' ], [ 'null', 1, 'C', '0x%02X' ],
79 [ 'toc_type', 2, 'n', [ '0x%04x', '%d' ] ],
80 [ 'first_track', 1, 'C' ], [ 'last_track', 1, 'C' ] ],
81 lfields => [
82 [ 'isrc', 12, 'a12' ],
83 [ 'sector_size', 2, 'n' ], [ 'mode', 2, 'n', '0x%04x' ],
84 [ 'unknown', 2, 'n', '0x%04X (always 1)' ],
85 [ 'index0', 8, $Q ], [ 'index1', 8, $Q ],
86 [ 'next_index', 8, $Q ] ],
88 $chunk_types->{cues} = {
89 code => "CUEX",
90 fields => [ [ 'size', 4, 'N' ] ],
91 lfields => [
92 [ 'mode', 1, 'C', [ '0x%02X', '%d' ] ],
93 [ 'track', 1, 'C', '%02X' ], [ 'index', 1, 'C', '%02X' ],
94 [ 'null', 1, 'C', '0x%02X' ], [ 'lba', 4, 'l>' ] ],
96 $chunk_types->{relo} = {
97 code => "RELO",
98 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
100 $chunk_types->{toct} = {
101 code => "TOCT",
102 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
104 $chunk_types->{dinf} = {
105 code => "DINF",
106 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
108 $chunk_types->{cdtx} = {
109 code => "CDTX",
110 fields => [ [ 'size', 4, 'N' ] ],
111 lfields => [
112 [ 'pack_type', 1, 'C', '%02X' ], [ 'track', 1, 'C' ],
113 [ 'pack_num', 1, 'C' ], [ 'block_char', 1, 'C', [ '%08B', '0x%02x' ] ],
114 [ 'text', 12, 'a12', '"%s"' ], [ 'crc', 2, 'n', '0x%04x' ] ],
116 $chunk_types->{mtyp} = {
117 code => "MTYP",
118 fields => [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
120 $chunk_types->{end} = {
121 code => "END!",
122 fields => [ [ 'size', 4, 'N' ] ],
124 $chunk_types->{unknown} = {
125 fields => [ [ 'size', 4, 'N' ] ],
127 # { "NERO", "NER5" },
128 # { "CUES", "CUEX" },
129 # { "ETNF", "ETN2" },
130 # { "DAOI", "DAOX" },
131 # { "SINF", "SINF" },
132 # { "END!", "END!" },
134 # File parsing & processing
135 sub process_file {
136 my ($process_iff, $nrg_name) = map shift, (0..1);
137 my ($nrg, $iff) = (undef, { });
138 unless (open $nrg, "<", $nrg_name . $ext) {
139 warn "ERROR Cannot open file '$nrg_name$ext' for reading: $!\n";
140 return;
142 binmode $nrg;
143 print "Locating the header at the end of the file\n" if ($verbose > 1);
144 seek $nrg, -4-$chunk_types->{nero}->{fields}->[0]->[1], SEEK_END;
145 read_chunk($nrg, $iff);
146 my $offset = $iff->{nero}->[0]->{fields}->[0];
147 unless (defined $offset) {
148 warn "ERROR The offset of the IFF chunk list in '$nrg_name' could not be determined\n";
149 return;
151 seek $nrg, $offset, SEEK_SET;
152 while (read_chunk($nrg, $iff)) {};
153 &$process_iff($iff, $nrg, $nrg_name);
155 # Read an IFF chunk at the current location in $nrg and set the fields' values in $iff
156 # $iff->{$type}->[$index]->{fields}->[$values]
157 # $iff->{$type}->[$index]->{lfields}->[$index]->[$values]
158 sub read_chunk {
159 my ($nrg, $iff) = map shift, (0..1);
160 my $code = read_fixed($nrg, 4);
161 my $type = typeofcode($code);
162 unless ($type) {
163 warn "ERROR Unknown chunk code '" . printable($code) . "'\n";
164 return;
166 my ($rc, $pos) = (1, tell($nrg));
167 print "Reading chunk '$code' at offset $pos\n" if ($verbose > 1);
168 $iff->{$type} = [ ] unless (defined($iff->{$type}));
169 # read the fields
170 my $fields = $chunk_types->{$type}->{fields};
171 my $chunk = {
172 offset => $pos-4,
173 fields => [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$fields ],
175 push(@{$iff->{$type}}, $chunk);
176 if ($fields->[0]->[0] eq 'size') {
177 # find the chunk end
178 my $size = $chunk->{fields}->[0];
179 $pos += $fields->[0]->[1]+$size;
180 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
181 # read the lfields
182 if (defined($chunk_types->{$type}->{lfields})) {
183 $chunk->{lfields} = [ ] unless (defined($chunk->{lfields}));
184 my $lfields = $chunk_types->{$type}->{lfields};
185 until (tell($nrg) >= $pos) {
186 my $vals = [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$lfields ];
187 push(@{$chunk->{lfields}}, $vals)
190 # check that all the chunk has been processed
191 my $left = $pos-tell($nrg);
192 warn "ERROR Data ($left bytes) read after the end of the chunk $code\n", $rc = 0
193 if ($left < 0);
194 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
195 if ($left > 0);
196 $pos++ if ($pos % 2);
197 seek $nrg, $pos, SEEK_SET;
199 return if ($type eq 'end');
200 return $rc;
202 sub typeofcode {
203 my $code = shift;
204 for (keys %{$chunk_types}) {
205 defined($chunk_types->{$_}->{code}) or next;
206 return $_ if ($code eq $chunk_types->{$_}->{code});
208 return undef;
210 sub extract_iff {
211 my $iff = shift;
212 my ($src, $src_name) = map shift, (0..1);
213 my $offset = $iff->{nero}->[0]->{fields}->[0];
214 unless (defined($offset)) {
215 warn "ERROR Cannot find offset of the IFF\n";
216 return;
218 print "IFF offset: $offset\n" if ($verbose > 1);
219 # Dest file
220 my $dst_name = $src_name . $iff_ext;
221 unless ($force or !-e $dst_name) {
222 warn "Not extracting the IFF because '$dst_name' already exists\n";
223 return;
225 my $dst;
226 unless ($no_act or open $dst, ">", $dst_name) {
227 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
228 return;
230 binmode $dst unless ($no_act);
231 # Copy
232 my $rc = 1;
233 seek $src, $offset, SEEK_SET;
234 my $size = (stat($src))[7]-$offset;
235 my $buf = read_fixed($src, $size);
236 print $dst $buf or $rc = 0;
237 print "$size bytes written in '$dst_name'\n" if ($verbose);
238 close $dst unless ($no_act);
239 return $rc;
242 # Command processing
243 sub process_cmd {
244 my $args = shift; my $cmd = shift @$args;
245 my $chunk_filter = (@$args) ? sub { grep { $_[0] eq $_ } @$args } : undef;
246 local *print_iff = walk_chunks(\&print_chunk, $chunk_filter);
247 local *hexdump_iff = walk_chunks(
248 sub { print "\n$chunk_types->{$_[0]}->{code}\n"; hexdump_chunk(@_) },
249 $chunk_filter
251 #*printhexdump_iff = walk_chunks(sub { print_chunk(@_); hexdump_chunk(@_) });
252 my $track_filter = sub {
253 for (@$args[0,1]) { defined($_) or next; ($_ == shift) or return };
254 return 1;
256 local *list_tracks = walk_tracks(\&list_track, $track_filter);
257 local *extract_tracks = walk_tracks(\&extract_track, $track_filter);
258 if ($cmd eq "help") {
259 Pod::Usage::pod2usage(
260 -verbose => 99,
261 -sections => "SYNOPSIS|COMMANDS",
262 -exitval => 0
264 } elsif ($cmd eq "types") {
265 for (keys %$chunk_types) { print "$_\n" unless ($_ eq 'unknown') };
267 } elsif ($cmd eq "nop") {
268 process_file(sub { return 1 }, @_);
269 } elsif ($cmd eq "info") {
270 process_file(\&print_iff, @_);
271 } elsif ($cmd eq "hexdump") {
272 process_file(\&hexdump_iff, @_);
273 } elsif ($cmd eq "list") {
274 process_file(\&list_tracks, @_);
275 } elsif ($cmd eq "track") {
276 process_file(\&extract_tracks, @_);
277 } elsif ($cmd eq "iff") {
278 process_file(\&extract_iff, @_);
279 } else {
280 warn "ERROR Unknown command '$cmd'\n";
284 # Main
285 unless (process_cmd(\@ARGV, $nrg_name)) {
286 print "There was an error.\n" if ($verbose);
287 exit 1;
288 } else {
289 exit;
292 # Chunks & Fields
293 sub walk_chunks {
294 my ($process, $filter) = @_;
295 sub {
296 my $iff = shift;
297 my $rc = 1;
298 for my $type (keys %$iff) {
299 CHUNK: for my $chunk (@{$iff->{$type}}) {
300 next CHUNK if (defined($filter) and not &$filter($type, $chunk));
301 &$process($type, $chunk, @_) or $rc = 0;
304 return $rc;
307 sub print_chunk {
308 my ($type, $chunk) = map shift, (0..1);
309 map shift, (0..1);
310 print "$chunk_types->{$type}->{code} (at offset $chunk->{offset})\n";
311 print_fields($chunk_types->{$type}->{fields}, $chunk->{fields}) if ($verbose);
312 if (defined($chunk->{lfields})) {
313 for (my $i = 0; $i < @{$chunk->{lfields}}; $i++) {
314 printf "%s[%d]\n", $chunk_types->{$type}->{code}, $i+1;
315 print_fields($chunk_types->{$type}->{lfields}, $chunk->{lfields}->[$i]) if ($verbose);
318 print "\n" if ($verbose);
319 return 1;
321 sub print_fields {
322 my ($fields, $vals) = map shift, (0..1);
323 for (my $i = 0; $i < @{$vals}; $i++) {
324 my $field = $fields->[$i];
325 my @val = ( $vals->[$i] );
326 my $str = $val[0];
327 if (defined($field->[3])) {
328 my $fmts = (ref($field->[3]) eq 'ARRAY') ? $field->[3] : [ $field->[3] ];
329 $str = join(" ", map { sprintf($_, @val) } @$fmts);
331 print "($field->[1])$field->[0]: $str\n";
333 return 1;
335 sub hexdump_chunk {
336 my ($type, $chunk) = map shift, (0..1);
337 my ($src) = map shift, (0..1);
338 $type = $chunk_types->{$type};
339 seek $src, $chunk->{offset}, SEEK_SET;
340 my $first_f = $type->{fields}->[0];
341 my $size = 4+$first_f->[1];
342 $size += $chunk->{fields}->[0] if ($first_f->[0] eq 'size');
343 print hexdump( read_fixed($src, $size) );
346 # Sessions & Tracks
347 sub walk_tracks {
348 my ($process, $filter) = @_;
349 sub {
350 my $iff = shift;
351 my $rc = 1;
352 for (my $sess = 1; $sess <= @{$iff->{sinf}}; $sess++) {
353 my $numoftrcks = $iff->{sinf}->[$sess-1]->{fields}->[1];
354 TRACK: for (my $trck = 0; $trck <= $numoftrcks; $trck++) {
355 # $trck is the ordinal index of the track in the current session, not the track number
356 # the track 0 is for the lead-in
357 my @params = ($sess, $trck, track_chunk($iff, $sess, $trck));
358 next TRACK if ($trck == 0 and defined($params[2]->{etnf}));
359 next TRACK if (defined($filter) and not &$filter(@params));
360 &$process(@params, @_) or $rc = 0;
363 return $rc;
366 sub list_track {
367 my ($sess, $trck, $chunk) = map shift, (0..2);
368 map shift, (0..1);
369 printf "SESSION %d TRACK %02d [%02d]\n", $sess, $chunk->{track}, $trck;
370 printf " mode $chunk_types->{daoi}->{lfields}->[2]->[3] %db/s %db\n",
371 $chunk->{mode}, $chunk->{sect_size}, $chunk->{end}-$chunk->{start}
372 if ($verbose);
374 # Extract the track at position $trck in session $sess from the image
375 sub extract_track {
376 list_track(@_) if ($verbose);
377 my ($sess, $trck, $chunk) = map shift, (0..2);
378 my ($src, $src_name) = map shift, (0..1);
379 my $track_num = $chunk->{track};
380 my $start = $chunk->{start};
381 my $end = $chunk->{end};
382 my $sect_size = $chunk->{sect_size};
383 unless (defined($start) and defined($end)) {
384 warn "ERROR Cannot find start/end of track $track_num\n";
385 return;
387 unless (defined($sect_size)) {
388 warn "ERROR Cannot find sector size of track $track_num\n";
389 return;
391 printf " start: $start\tend: $end\n" if ($verbose > 1);
392 # Dest file
393 my $dst_name = $src_name . sprintf($trk_ext, $sess, $track_num, $sess);
394 unless ($force or !-e $dst_name) {
395 warn "Skipping track $track_num because '$dst_name' already exists\n";
396 return;
398 my $dst;
399 unless ($no_act or open $dst, ">", $dst_name) {
400 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
401 return;
403 binmode $dst unless ($no_act);
404 # Copy
405 seek $src, $start, SEEK_SET;
406 my ($rc, $count) = (1, 0);
407 SECTOR: while (tell($src) + $sect_size <= $end) {
408 my $buf = read_fixed($src, $sect_size);
409 print $dst $buf unless ($no_act);
410 $count++;
412 print "$count sectors of size $sect_size from track $track_num written in '$dst_name'\n" if ($verbose);
413 if (my $left = $end-tell($src)) {
414 warn "ERROR Partial sector of size $left/$sect_size found in track $track_num\n";
415 $rc = 0;
416 my $buf = read_fixed($src, $left);
417 print $dst $buf unless ($no_act);
419 close $dst unless ($no_act);
420 return $rc;
422 sub session_chunk {
423 my ($iff, $sess) = map shift, @_;
424 my ($daoi_n, $etnf_n) = (0, 0);
425 my $next_session_chunk = sub {
426 my $chunk = { tracks => $iff->{sinf}->[$sess-1]->{fields}->[1] };
427 my $is_daoi = defined($iff->{daoi}->[$daoi_n]);
428 my $is_etnf = defined($iff->{etnf}->[$etnf_n]);
429 if ( $is_daoi and (not $is_etnf
430 or ($iff->{daoi}->[$daoi_n]->{offset} < $iff->{etnf}->[$etnf_n]->{offset})) ) {
431 $chunk->{daoi} = $iff->{daoi}->[$daoi_n];
432 $chunk->{first} = $iff->{daoi}->[$daoi_n]->{fields}->[5];
433 $daoi_n++;
434 } elsif ($is_etnf) {
435 $chunk->{etnf} = $iff->{etnf}->[$etnf_n];
436 $chunk->{etnf_first} = 1; # TODO
437 $chunk->{first} = 1; # TODO
438 $etnf_n++;
440 $chunk;
442 ( map &$next_session_chunk, (1..$sess) )[-1];
444 # (using index1 of the next track as the end because Nero records the end as the index0 of the next track)
445 sub track_chunk () {
446 my ($iff, $sess, $trck) = map shift, @_;
447 my $chunk = session_chunk($iff, $sess);
448 $chunk->{track} = $chunk->{first} + $trck - 1;
449 if (defined $chunk->{daoi}) {
450 $chunk->{sect_size} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
451 $chunk->{mode} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
452 $chunk->{start} = ($trck)
453 ? $chunk->{daoi}->{lfields}->[$trck-1]->[5]
454 : $chunk->{daoi}->{lfields}->[$trck]->[4];
455 # track 0 starts at index 0
456 $chunk->{end} = (defined($chunk->{daoi}->{lfields}->[$trck]))
457 ? $chunk->{daoi}->{lfields}->[$trck]->[5]
458 : $chunk->{daoi}->{lfields}->[$trck-1]->[6];
459 # last track ends at next_index
460 } elsif (defined($chunk->{etnf})) {
461 warn "WARNING Only DAO discs are fully supported for now\n";
462 $chunk->{track_size} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
463 $chunk->{track_length} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[5];
464 $chunk->{sect_size} = $chunk->{track_size} / $chunk->{track_length};
465 $chunk->{mode} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
466 $chunk->{start} = ($trck)
467 ? $chunk->{etnf}->{lfields}->[$trck-1]->[0]
468 : 0;
469 $chunk->{end} = ($trck)
470 ? $chunk->{start} + $chunk->{etnf}->{lfields}->[$trck-1]->[1]
471 : 0;
473 $chunk;
476 # Misc
477 sub read_fixed {
478 my ($file, $size) = @_;
479 my $buf;
480 my $n = read $file, $buf, $size;
481 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
482 return $buf;
485 __END__
487 =head1 DESCRIPTION
489 C<nrgtool> processes one NRG CD image file. It finds and parses the IFF
490 footer, which contains the metadata; then it prints information or extracts
491 some data.
493 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
494 are split at index 1 points.
496 =head1 EXAMPLES
498 To view the result of the parsing:
500 nrgtool -v image.nrg | less
502 To show the debugging output of the parsing:
504 nrgtool -vv image.nrg nop
506 To view a hexdump of the CD-text chunk:
508 nrgtool image.nrg hexdump cdtx | less
510 To list the tracks:
512 nrgtool image.nrg list
514 To extract all the audio tracks of F<image.nrg> to F<image-XX.cdda>:
516 nrgtool -v -e .nrg -t '-%n%02d.cdda' image track 1
518 To extract a data disc to an iso file F<image.nrg.iso>:
520 nrgtool -v -t .iso image.nrg track 1 1
522 If a track contains sub-channel data, you may extract the audio/data with one
523 of the following commands:
525 raw96cdconv -v image.nrg.s01-t16
526 raw96cdconv -v --iso image.nrg.s01-t01
527 raw96cdconv -v --sector-size 2336 image.nrg.s01-t01
529 An audio track may be converted to WAV by sox(1):
531 sox -t .cdda -L image.nrg.s01-t16 image-16.wav
533 =head1 COMMANDS
535 The default command is "info".
537 =over 8
539 =item B<help>
541 =item B<info> S<[ I<chunk_types> ]>
543 Print the chunks' structure resulting of the parsing of the IFF. Enbale
544 verbose mode to view the fields' values.
546 =item B<hexdump> S<[ I<chunk_types> ]>
548 =item B<nop>
550 Only parse the IFF.
552 =item B<list> S<[ I<session> S<[ I<track_pos> ]> ]>
554 List the tracks. In verbose mode, give some track attributes.
556 =item B<track> S<[ I<session> S<[ I<track_pos> ]> ]>
558 Extract the track (raw) data to file(s).
560 =item B<iff>
562 Extract the IFF header to a file.
564 =item B<types>
566 List the known chunk types.
568 =back
570 =head1 OPTIONS
572 Options may be negated by prefixing them with "--no-" instead of "--".
574 =over 8
576 =item B<-h>, B<--help>
578 =item B<-v>, B<--verbose>
580 Verbose: print some or more information, depending on the command.
582 If it is given twice, also print debugging information, notably during the
583 parsing.
585 =item B<-n>, B<--no-act>
587 No Action: test the reading of files but do not write any files.
589 =item B<-f>, B<--force>
591 Force: overwrite existing files.
593 =item B<-1>, B<--nrgv1>
595 NRG Version: Treat the file as a first version (prior to Nero 5.5) NRG; the
596 default is to treat the file as a version 2 (Nero 5.5 and later).
597 Unimplemented.
599 =item B<-e>, B<--extension> I<ext>
601 NRG extension: appended to the given file name to obtain the NRG file name;
602 this way the generated files' names can be controlled somewhat.
604 =item B<-t>, B<--track-ext> I<ext_fmt>
606 Track Extension: printf format template for the file name extensions of the
607 tracks; the first argument to printf is the session number, the second the
608 track number, the third the session number; the default is ".s%02d-t%02d".
610 =item B<-i>, B<--iff-ext> I<ext>
612 IFF Extension: file name extension of the IFF.
614 =back
616 =head1 ENVIRONMENT
618 No environment variables are used.
620 =head1 BUGS
622 C<nrgtool> has been tested with images created by Nerolinux 4.0 only. Any
623 resulting incompatibilties with other versions of Nero may be reported to the
624 author.
626 The NRG version 1 format is not supported because of the lack of
627 sample image files.
629 If you can reconstruct the same NRG from the extracted files, most probably
630 C<nrgtool> can handle the particular image:
632 nrgtool image.nrg iff
633 nrgtool image.nrg track
634 cat image.nrg.s??-t?? image.nrg.iff >image.nrg.nrgtool
635 cmp image.nrg image.nrg.nrgtool && echo "nrgtool can extract all the data"
637 =head1 AUTHOR
639 G.raud <graud@gmx.com>
641 =head1 SEE ALSO
643 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>
645 =cut