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.
11 nrgtool - reads and converts/splits Nero CD images (.nrg)
17 B<nrgtool> S<[ B<-v> ]> S<[ B<-n> ]> S<[ B<-f> ]> S<[ B<-1> ]> S<[ B<-I> ]> I<nrg> S<[ I<cmd> I<args> ]>
25 Getopt
::Long
::Configure
('bundling', 'no_auto_abbrev', 'auto_version', 'auto_help');
27 use String
::Escape
qw(printable);
28 use Data
::Hexdumper
qw(hexdump);
31 $main::VERSION
= "0.1";
34 my ($verbose, $no_act, $force) = (0, undef, undef);
35 my ($nrgv, $ext, $iff_ext, $trk_ext, $iff_only) = (2, '', '.iff', '.s%02d-t%02d', 0);
36 die Getopt
::Long
::HelpMessage
(128)
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 'I|iff' => \
$iff_only,
49 ) and my $nrg_name = shift;
50 $ARGV[0] = "info" unless (@ARGV);
52 # Data pertaining to the NRG file format IFF structure
53 die "ERROR For now only NRG version 2 files are supported" if ($nrgv != 2);
55 my $Q = 'Q>'; # unpack template for 64-bit values
56 $chunk_types->{nero
} = {
58 fields
=> [ [ 'iff_offset', 8, $Q ] ],
60 $chunk_types->{sinf
} = {
62 fields
=> [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
64 $chunk_types->{etnf
} = {
66 fields
=> [ [ 'size', 4, 'N' ] ],
70 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
72 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
73 [ 'length', 4, 'N' ] ],
75 $chunk_types->{daoi
} = {
78 [ 'size', 4, 'N' ], [ 'le_size', 4, 'N' ],
79 [ 'upc', 13, 'a13' ], [ 'null', 1, 'C', '0x%02X' ],
80 [ 'toc_type', 2, 'n', [ '0x%04x', '%d' ] ],
81 [ 'first_track', 1, 'C' ], [ 'last_track', 1, 'C' ] ],
83 [ 'isrc', 12, 'a12' ],
84 [ 'sector_size', 2, 'n' ], [ 'mode', 2, 'n', '0x%04x' ],
85 [ 'unknown', 2, 'n', '0x%04X (always 1)' ],
86 [ 'index0', 8, $Q ], [ 'index1', 8, $Q ],
87 [ 'next_index', 8, $Q ] ],
89 $chunk_types->{cues
} = {
91 fields
=> [ [ 'size', 4, 'N' ] ],
93 [ 'mode', 1, 'C', [ '0x%02X', '%d' ] ],
94 [ 'track', 1, 'C', '%02X' ], [ 'index', 1, 'C', '%02X' ],
95 [ 'null', 1, 'C', '0x%02X' ], [ 'lba', 4, 'l>' ] ],
97 $chunk_types->{relo
} = {
99 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
101 $chunk_types->{toct
} = {
103 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
105 $chunk_types->{dinf
} = {
107 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
109 $chunk_types->{cdtx
} = {
111 fields
=> [ [ 'size', 4, 'N' ] ],
113 [ 'pack_type', 1, 'C', '%02X' ], [ 'track', 1, 'C' ],
114 [ 'pack_num', 1, 'C' ], [ 'block_char', 1, 'C', [ '%08B', '0x%02x' ] ],
115 [ 'text', 12, 'a12', '"%s"' ], [ 'crc', 2, 'n', '0x%04x' ] ],
117 $chunk_types->{mtyp
} = {
119 fields
=> [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
121 $chunk_types->{end
} = {
123 fields
=> [ [ 'size', 4, 'N' ] ],
125 $chunk_types->{unknown
} = {
126 fields
=> [ [ 'size', 4, 'N' ] ],
128 # { "NERO", "NER5" },
129 # { "CUES", "CUEX" },
130 # { "ETNF", "ETN2" },
131 # { "DAOI", "DAOX" },
132 # { "SINF", "SINF" },
133 # { "END!", "END!" },
135 # File parsing & processing
137 my ($process_iff, $nrg_name) = map shift, (0..1);
138 my ($nrg, $iff, $rc) = (undef, { }, 1);
139 unless (open $nrg, "<", $nrg_name . $ext) {
140 warn "ERROR Cannot open file '$nrg_name$ext' for reading: $!\n";
144 print "Locating the header at the end of the file\n" if ($verbose > 1);
145 seek $nrg, -4-$chunk_types->{nero
}->{fields
}->[0]->[1], SEEK_END
;
146 read_chunk
($nrg, $iff);
147 my $offset = $iff->{nero
}->[0]->{fields
}->[0];
148 unless (defined $offset) {
149 warn "ERROR The offset of the IFF chunk list in '$nrg_name' could not be determined\n";
152 seek $nrg, 0, SEEK_SET
if ($iff_only);
153 seek $nrg, $offset, SEEK_SET
unless ($iff_only);
154 while (my $_ = read_chunk
($nrg, $iff)) {
156 warn "ERROR while reading an IFF chunk; aborting\n";
159 $rc = 0 if ($_ == -1); # report non fatal error
161 &$process_iff($iff, $nrg, $nrg_name) and ($rc);
163 # Read an IFF chunk at the current location in $nrg and set the fields' values in $iff
164 # $iff->{$type}->[$index]->{fields}->[$values]
165 # $iff->{$type}->[$index]->{lfields}->[$index]->[$values]
167 my ($nrg, $iff) = map shift, (0..1);
168 my $code = read_fixed
($nrg, 4);
169 my $type = typeofcode
($code);
171 warn "ERROR Unknown chunk code '" . printable
($code) . "'\n";
174 my ($rc, $pos) = (1, tell($nrg));
175 print "Reading chunk '$code' at offset $pos\n" if ($verbose > 1);
176 $iff->{$type} = [ ] unless (defined($iff->{$type}));
178 my $fields = $chunk_types->{$type}->{fields
};
181 fields
=> [ map { unpack($_->[2], read_fixed
($nrg, $_->[1])) } @
$fields ],
183 push(@
{$iff->{$type}}, $chunk);
184 if ($fields->[0]->[0] eq 'size') {
186 my $size = $chunk->{fields
}->[0];
187 $pos += $fields->[0]->[1]+$size;
188 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
190 if (defined($chunk_types->{$type}->{lfields
})) {
191 $chunk->{lfields
} = [ ] unless (defined($chunk->{lfields
}));
192 my $lfields = $chunk_types->{$type}->{lfields
};
193 until (tell($nrg) >= $pos) {
194 my $vals = [ map { unpack($_->[2], read_fixed
($nrg, $_->[1])) } @
$lfields ];
195 push(@
{$chunk->{lfields
}}, $vals)
198 # check that all the chunk has been processed
199 my $left = $pos-tell($nrg);
200 warn "ERROR Data ($left bytes) read after the end of the chunk $code\n", $rc = -1
202 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
204 $pos++ if ($pos % 2);
205 seek $nrg, $pos, SEEK_SET
;
207 return 0 if ($type eq 'end');
212 for (keys %{$chunk_types}) {
213 defined($chunk_types->{$_}->{code
}) or next;
214 return $_ if ($code eq $chunk_types->{$_}->{code
});
220 my ($src, $src_name) = map shift, (0..1);
221 my $offset = $iff->{nero
}->[0]->{fields
}->[0];
222 unless (defined($offset)) {
223 warn "ERROR Cannot find offset of the IFF\n";
226 print "IFF offset: $offset\n" if ($verbose > 1);
228 my $dst_name = $src_name . $iff_ext;
229 unless ($force or !-e
$dst_name) {
230 warn "Not extracting the IFF because '$dst_name' already exists\n";
234 unless ($no_act or open $dst, ">", $dst_name) {
235 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
238 binmode $dst unless ($no_act);
241 seek $src, $offset, SEEK_SET
;
242 my $size = (stat($src))[7]-$offset;
243 my $buf = read_fixed
($src, $size);
244 print $dst $buf or $rc = 0;
245 print "$size bytes written in '$dst_name'\n" if ($verbose);
246 close $dst unless ($no_act);
252 my $args = shift; my $cmd = shift @
$args;
253 my $chunk_filter = (@
$args) ?
sub { grep { $_[0] eq $_ } @
$args } : undef;
254 local *print_iff
= walk_chunks
(\
&print_chunk
, $chunk_filter);
255 local *hexdump_iff
= walk_chunks
(
256 sub { print "\n$chunk_types->{$_[0]}->{code}\n"; hexdump_chunk
(@_) },
259 #*printhexdump_iff = walk_chunks(sub { print_chunk(@_); hexdump_chunk(@_) });
260 my $track_filter = sub {
261 for (@
$args[0,1]) { defined($_) or next; ($_ == shift) or return };
264 my $iff_only_warn = sub {
265 return $iff_only unless ($iff_only);
266 print "WARNING Command $cmd on a bare IFF file ignored\n";
267 process_file
(sub { return 1 }, @_);
270 local *list_tracks
= walk_tracks
(\
&list_track
, $track_filter);
271 local *extract_tracks
= walk_tracks
(\
&extract_track
, $track_filter);
272 if ($cmd eq "help") {
273 Pod
::Usage
::pod2usage
(
275 -sections
=> "SYNOPSIS|COMMANDS",
278 } elsif ($cmd eq "types") {
279 for (keys %$chunk_types) { print "$_\n" unless ($_ eq 'unknown') };
281 } elsif ($cmd eq "nop") {
282 process_file
(sub { return 1 }, @_);
283 } elsif ($cmd eq "info") {
284 process_file
(\
&print_iff
, @_);
285 } elsif ($cmd eq "hexdump") {
286 process_file
(\
&hexdump_iff
, @_);
287 } elsif ($cmd eq "list") {
288 process_file
(\
&list_tracks
, @_);
289 } elsif ($cmd eq "track") {
290 process_file
(\
&extract_tracks
, @_) unless (&$iff_only_warn(@_));
291 } elsif ($cmd eq "iff") {
292 process_file
(\
&extract_iff
, @_) unless (&$iff_only_warn(@_));
294 warn "ERROR Unknown command '$cmd'\n";
299 unless (process_cmd
(\
@ARGV, $nrg_name)) {
300 print "There was an error.\n" if ($verbose);
308 my ($process, $filter) = @_;
312 for my $type (keys %$iff) {
313 CHUNK
: for my $chunk (@
{$iff->{$type}}) {
314 next CHUNK
if (defined($filter) and not &$filter($type, $chunk));
315 &$process($type, $chunk, @_) or $rc = 0;
322 my ($type, $chunk) = map shift, (0..1);
324 print "$chunk_types->{$type}->{code} (at offset $chunk->{offset})\n";
325 print_fields
($chunk_types->{$type}->{fields
}, $chunk->{fields
}) if ($verbose);
326 if (defined($chunk->{lfields
})) {
327 for (my $i = 0; $i < @
{$chunk->{lfields
}}; $i++) {
328 printf "%s[%d]\n", $chunk_types->{$type}->{code
}, $i+1;
329 print_fields
($chunk_types->{$type}->{lfields
}, $chunk->{lfields
}->[$i]) if ($verbose);
332 print "\n" if ($verbose);
336 my ($fields, $vals) = map shift, (0..1);
337 for (my $i = 0; $i < @
{$vals}; $i++) {
338 my $field = $fields->[$i];
339 my @val = ( $vals->[$i] );
341 if (defined($field->[3])) {
342 my $fmts = (ref($field->[3]) eq 'ARRAY') ?
$field->[3] : [ $field->[3] ];
343 $str = join(" ", map { sprintf($_, @val) } @
$fmts);
345 print "($field->[1])$field->[0]: $str\n";
350 my ($type, $chunk) = map shift, (0..1);
351 my ($src) = map shift, (0..1);
352 $type = $chunk_types->{$type};
353 seek $src, $chunk->{offset
}, SEEK_SET
;
354 my $first_f = $type->{fields
}->[0];
355 my $size = 4+$first_f->[1];
356 $size += $chunk->{fields
}->[0] if ($first_f->[0] eq 'size');
357 print hexdump
( read_fixed
($src, $size) );
362 my ($process, $filter) = @_;
366 for (my $sess = 1; $sess <= @
{$iff->{sinf
}}; $sess++) {
367 my $numoftrcks = $iff->{sinf
}->[$sess-1]->{fields
}->[1];
368 TRACK
: for (my $trck = 0; $trck <= $numoftrcks; $trck++) {
369 # $trck is the ordinal index of the track in the current session, not the track number
370 # the track 0 is for the lead-in
371 my @params = ($sess, $trck, track_chunk
($iff, $sess, $trck));
372 next TRACK
if ($trck == 0 and defined($params[2]->{etnf
}));
373 next TRACK
if (defined($filter) and not &$filter(@params));
374 &$process(@params, @_) or $rc = 0;
381 my ($sess, $trck, $chunk) = map shift, (0..2);
383 printf "SESSION %d TRACK %02d [%02d]\n", $sess, $chunk->{track
}, $trck;
384 printf " mode $chunk_types->{daoi}->{lfields}->[2]->[3] %db/s %db\n",
385 $chunk->{mode
}, $chunk->{sect_size
}, $chunk->{end
}-$chunk->{start
}
388 # Extract the track at position $trck in session $sess from the image
390 list_track
(@_) if ($verbose);
391 my ($sess, $trck, $chunk) = map shift, (0..2);
392 my ($src, $src_name) = map shift, (0..1);
393 my $track_num = $chunk->{track
};
394 my $start = $chunk->{start
};
395 my $end = $chunk->{end
};
396 my $sect_size = $chunk->{sect_size
};
397 unless (defined($start) and defined($end)) {
398 warn "ERROR Cannot find start/end of track $track_num\n";
401 unless (defined($sect_size)) {
402 warn "ERROR Cannot find sector size of track $track_num\n";
405 printf " start: $start\tend: $end\n" if ($verbose > 1);
407 my $dst_name = $src_name . sprintf($trk_ext, $sess, $track_num, $sess);
408 unless ($force or !-e
$dst_name) {
409 warn "Skipping track $track_num because '$dst_name' already exists\n";
413 unless ($no_act or open $dst, ">", $dst_name) {
414 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
417 binmode $dst unless ($no_act);
419 seek $src, $start, SEEK_SET
;
420 my ($rc, $count) = (1, 0);
421 SECTOR
: while (tell($src) + $sect_size <= $end) {
422 my $buf = read_fixed
($src, $sect_size);
423 print $dst $buf unless ($no_act);
426 print "$count sectors of size $sect_size from track $track_num written in '$dst_name'\n" if ($verbose);
427 if (my $left = $end-tell($src)) {
428 warn "ERROR Partial sector of size $left/$sect_size found in track $track_num\n";
430 my $buf = read_fixed
($src, $left);
431 print $dst $buf unless ($no_act);
433 close $dst unless ($no_act);
437 my ($iff, $sess) = map shift, @_;
438 my ($daoi_n, $etnf_n) = (0, 0);
439 my $next_session_chunk = sub {
440 my $chunk = { tracks
=> $iff->{sinf
}->[$sess-1]->{fields
}->[1] };
441 my $is_daoi = defined($iff->{daoi
}->[$daoi_n]);
442 my $is_etnf = defined($iff->{etnf
}->[$etnf_n]);
443 if ( $is_daoi and (not $is_etnf
444 or ($iff->{daoi
}->[$daoi_n]->{offset
} < $iff->{etnf
}->[$etnf_n]->{offset
})) ) {
445 $chunk->{daoi
} = $iff->{daoi
}->[$daoi_n];
446 $chunk->{first
} = $iff->{daoi
}->[$daoi_n]->{fields
}->[5];
449 $chunk->{etnf
} = $iff->{etnf
}->[$etnf_n];
450 $chunk->{etnf_first
} = 1; # TODO
451 $chunk->{first
} = 1; # TODO
456 ( map &$next_session_chunk, (1..$sess) )[-1];
458 # (using index1 of the next track as the end because Nero records the end as the index0 of the next track)
460 my ($iff, $sess, $trck) = map shift, @_;
461 my $chunk = session_chunk
($iff, $sess);
462 $chunk->{track
} = $chunk->{first
} + $trck - 1;
463 if (defined $chunk->{daoi
}) {
464 $chunk->{sect_size
} = $chunk->{daoi
}->{lfields
}->[($trck) ?
$trck-1 : $trck]->[1];
465 $chunk->{mode
} = $chunk->{daoi
}->{lfields
}->[($trck) ?
$trck-1 : $trck]->[2];
466 $chunk->{start
} = ($trck)
467 ?
$chunk->{daoi
}->{lfields
}->[$trck-1]->[5]
468 : $chunk->{daoi
}->{lfields
}->[$trck]->[4];
469 # track 0 starts at index 0
470 $chunk->{end
} = (defined($chunk->{daoi
}->{lfields
}->[$trck]))
471 ?
$chunk->{daoi
}->{lfields
}->[$trck]->[5]
472 : $chunk->{daoi
}->{lfields
}->[$trck-1]->[6];
473 # last track ends at next_index
474 } elsif (defined($chunk->{etnf
})) {
475 warn "WARNING Only DAO discs are fully supported for now\n";
476 $chunk->{track_size
} = $chunk->{etnf
}->{lfields
}->[($trck) ?
$trck-1 : $trck]->[1];
477 $chunk->{track_length
} = $chunk->{etnf
}->{lfields
}->[($trck) ?
$trck-1 : $trck]->[5];
478 $chunk->{sect_size
} = $chunk->{track_size
} / $chunk->{track_length
};
479 $chunk->{mode
} = $chunk->{etnf
}->{lfields
}->[($trck) ?
$trck-1 : $trck]->[2];
480 $chunk->{start
} = ($trck)
481 ?
$chunk->{etnf
}->{lfields
}->[$trck-1]->[0]
483 $chunk->{end
} = ($trck)
484 ?
$chunk->{start
} + $chunk->{etnf
}->{lfields
}->[$trck-1]->[1]
492 my ($file, $size) = @_;
494 my $n = read $file, $buf, $size;
495 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
503 C<nrgtool> processes one NRG CD image file. It finds and parses the IFF
504 footer, which contains the metadata; then it prints information or extracts
507 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
508 are split at index 1 points.
512 To view the result of the parsing:
514 nrgtool -v image.nrg | less
516 To show the debugging output of the parsing:
518 nrgtool -vv image.nrg nop
520 To view a hexdump of the CD-text chunk:
522 nrgtool image.nrg hexdump cdtx | less
526 nrgtool image.nrg list
528 To extract all the audio tracks of F<image.nrg> to F<image-XX.cdda>:
530 nrgtool -v -e .nrg -t '-%n%02d.cdda' image track 1
532 To extract a data disc to an iso file F<image.nrg.iso>:
534 nrgtool -v -t .iso image.nrg track 1 1
536 If a track contains sub-channel data, you may extract the audio/data with one
537 of the following commands:
539 raw96cdconv -v image.nrg.s01-t16
540 raw96cdconv -v --iso image.nrg.s01-t01
541 raw96cdconv -v --sector-size 2336 image.nrg.s01-t01
543 An audio track may be converted to WAV by sox(1):
545 sox -t .cdda -L image.nrg.s01-t16 image-16.wav
549 The default command is "info".
555 =item B<info> S<[ I<chunk_types> ]>
557 Print the chunks' structure resulting of the parsing of the IFF. Enbale
558 verbose mode to view the fields' values.
560 =item B<hexdump> S<[ I<chunk_types> ]>
566 =item B<list> S<[ I<session> S<[ I<track_pos> ]> ]>
568 List the tracks. In verbose mode, give some track attributes.
570 =item B<track> S<[ I<session> S<[ I<track_pos> ]> ]>
572 Extract the track (raw) data to file(s).
576 Extract the IFF header to a file.
580 List the known chunk types.
586 Options may be negated by prefixing them with "--no-" instead of "--".
590 =item B<-h>, B<--help>
592 =item B<-v>, B<--verbose>
594 Verbose: print some or more information, depending on the command.
596 If it is given twice, also print debugging information, notably during the
599 =item B<-I>, B<--iff>
601 IFF only: process a bare IFF file, previously extracted by the command B<iff>.
603 =item B<-n>, B<--no-act>
605 No Action: test the reading of files but do not write any files.
607 =item B<-f>, B<--force>
609 Force: overwrite existing files.
611 =item B<-1>, B<--nrgv1>
613 NRG Version: Treat the file as a first version (prior to Nero 5.5) NRG; the
614 default is to treat the file as a version 2 (Nero 5.5 and later).
617 =item B<-e>, B<--extension> I<ext>
619 NRG extension: appended to the given file name to obtain the NRG file name;
620 this way the generated files' names can be controlled somewhat.
622 =item B<-t>, B<--track-ext> I<ext_fmt>
624 Track Extension: printf format template for the file name extensions of the
625 tracks; the first argument to printf is the session number, the second the
626 track number, the third the session number; the default is ".s%02d-t%02d".
628 =item B<-i>, B<--iff-ext> I<ext>
630 IFF Extension: file name extension of the IFF.
636 No environment variables are used.
640 C<nrgtool> has been tested with images created by Nerolinux 4.0 only. Any
641 resulting incompatibilties with other versions of Nero may be reported to the
644 The NRG version 1 format is not supported because of the lack of
647 If you can reconstruct the same NRG from the extracted files, most probably
648 C<nrgtool> can handle the particular image:
650 nrgtool image.nrg iff
651 nrgtool image.nrg track
652 cat image.nrg.s??-t?? image.nrg.iff >image.nrg.nrgtool
653 cmp image.nrg image.nrg.nrgtool && echo "nrgtool can extract all the data"
657 G.raud <graud@gmx.com>
661 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>