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> ]> 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) = (2, '', '.iff', '.s%02d-t%02d');
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 ) 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);
54 my $Q = 'Q>'; # unpack template for 64-bit values
55 $chunk_types->{nero
} = {
57 fields
=> [ [ 'iff_offset', 8, $Q ] ],
59 $chunk_types->{sinf
} = {
61 fields
=> [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
63 $chunk_types->{etnf
} = {
65 fields
=> [ [ 'size', 4, 'N' ] ],
69 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
71 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
72 [ 'length', 4, 'N' ] ],
74 $chunk_types->{daoi
} = {
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' ] ],
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
} = {
90 fields
=> [ [ 'size', 4, 'N' ] ],
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
} = {
98 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
100 $chunk_types->{toct
} = {
102 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
104 $chunk_types->{dinf
} = {
106 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
108 $chunk_types->{cdtx
} = {
110 fields
=> [ [ 'size', 4, 'N' ] ],
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
} = {
118 fields
=> [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
120 $chunk_types->{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
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";
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";
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]
159 my ($nrg, $iff) = map shift, (0..1);
160 my $code = read_fixed
($nrg, 4);
161 my $type = typeofcode
($code);
163 warn "ERROR Unknown chunk code '" . printable
($code) . "'\n";
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}));
170 my $fields = $chunk_types->{$type}->{fields
};
173 fields
=> [ map { unpack($_->[2], read_fixed
($nrg, $_->[1])) } @
$fields ],
175 push(@
{$iff->{$type}}, $chunk);
176 if ($fields->[0]->[0] eq 'size') {
178 my $size = $chunk->{fields
}->[0];
179 $pos += $fields->[0]->[1]+$size;
180 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
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
194 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
196 $pos++ if ($pos % 2);
197 seek $nrg, $pos, SEEK_SET
;
199 return if ($type eq 'end');
204 for (keys %{$chunk_types}) {
205 defined($chunk_types->{$_}->{code
}) or next;
206 return $_ if ($code eq $chunk_types->{$_}->{code
});
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";
218 print "IFF offset: $offset\n" if ($verbose > 1);
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";
226 unless ($no_act or open $dst, ">", $dst_name) {
227 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
230 binmode $dst unless ($no_act);
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);
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
(@_) },
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 };
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
(
261 -sections
=> "SYNOPSIS|COMMANDS",
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
, @_);
280 warn "ERROR Unknown command '$cmd'\n";
285 unless (process_cmd
(\
@ARGV, $nrg_name)) {
286 print "There was an error.\n" if ($verbose);
294 my ($process, $filter) = @_;
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;
308 my ($type, $chunk) = 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);
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] );
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";
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) );
348 my ($process, $filter) = @_;
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;
367 my ($sess, $trck, $chunk) = map shift, (0..2);
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
}
374 # Extract the track at position $trck in session $sess from the image
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";
387 unless (defined($sect_size)) {
388 warn "ERROR Cannot find sector size of track $track_num\n";
391 printf " start: $start\tend: $end\n" if ($verbose > 1);
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";
399 unless ($no_act or open $dst, ">", $dst_name) {
400 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
403 binmode $dst unless ($no_act);
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);
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";
416 my $buf = read_fixed
($src, $left);
417 print $dst $buf unless ($no_act);
419 close $dst unless ($no_act);
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];
435 $chunk->{etnf
} = $iff->{etnf
}->[$etnf_n];
436 $chunk->{etnf_first
} = 1; # TODO
437 $chunk->{first
} = 1; # TODO
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)
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]
469 $chunk->{end
} = ($trck)
470 ?
$chunk->{start
} + $chunk->{etnf
}->{lfields
}->[$trck-1]->[1]
478 my ($file, $size) = @_;
480 my $n = read $file, $buf, $size;
481 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
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
493 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
494 are split at index 1 points.
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
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
535 The default command is "info".
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> ]>
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).
562 Extract the IFF header to a file.
566 List the known chunk types.
572 Options may be negated by prefixing them with "--no-" instead of "--".
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
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).
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.
618 No environment variables are used.
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
626 The NRG version 1 format is not supported because of the lack of
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"
639 G.raud <graud@gmx.com>
643 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>