And finally I wrote that unpacker! It was on the TODO list for 8 years in 2003, so it has been a goal that took 21 years to achieve!
I had to do a lot more reverse-engineering of the replayer to understand the format. But I was right, it was a Protracker packer. It's quite interesting, because it handles sound-effects by giving channel 3 its own pattern(s). I guess they didn't want that same hassle for the full game so they just used Protracker directly.
I'll update the Disposable Hero archive on UnExoticA with this rip soon. Here's the format description and unpacker
Code: Select all
# "Boys Without Brains" (BWB) Protracker packer format
# only seen in Disposable Hero demo on Amiga Format issue 47 coverdisk
#
# module data and sample data are stored separately
#
# In the mod, only channels 0,1,2 are used for music.
# The mod also defines sound effects, which play on channel 3.
# The replayer allows for the sound effect channel to be essentially
# playing another (one-channel) mod while the main music mod plays
#
# Boys Without Brains was the previous name of the team that created
# Disposable Hero, but they renamed themselves Euphoria for the game
#
# module data format: (offsets are relative to the start of the mod data)
# offset size what
# 0 4 ID: "BWB." (Boys Without Brains)
# 4 2 word offset to patternlist
# 6 2 word offset to pattern data offsets
# 8 2 word offset to pattern data
# 10 2 word offset to soundeffects (highest offset)
# 12 2 word offset to songstarts (lowest offset)
# 14 2 word unknown (unused?)
# 16 8*31 up to 31 sample info (stops at patternlist offset)
#
# sample info format: (same as protracker, but without sample name)
# offset size what
# 0 2 sample length in words
# 2 2 sample volume and finetune
# 4 2 repeat offset or 0 for no repeat
# 6 2 repeat length (usually 1)
#
# patternlist format:
# offset size what:
# 0 1 length of patternlist (l)
# 1 1 maximum length of patternlist (always $7F like protracker)
# 2 4*l patternlist: each byte is channel 0,1,2,3 pattern number
#
# pattern data offsets format:
# - the pattern data offsets are an array of words
# - each word is the offset into the pattern data where the pattern starts
#
# pattern data format:
# - the pattern data is a stream of bytes,
# broken up into multiple patterns by the pattern data offsets
# - each pattern is a stream of up to 64 rows of one channel of Protracker data
# - each instruction is one of 3 formats (binary):
# - 1: 11RRRRRR (I=N=F=X=0)
# - 2: 10RRRRRR 0INNNNNN IIIIFFFF XXXXXXXX
# - 3: 0INNNNNN IIIIFFFF XXXXXXXX (R=0)
\ - key:
# - R = repeat count
# - I = instrument number
# - N = note number (index into instruments finetune table-1, or 0 for no note)
# - F = effect number
# - X = effect parameter
#
# songstarts format:
# - the songstarts are an array of bytes
# - each byte is an index into patternlist
#
# soundeffects format:
# offset size what:
# 0 4*? soundeffect structures
# - these structure continue until end of the module data
#
# soundeffect structure:
# offset size what:
# 0 2 unknown
# 2 1 unknown
# 3 1 unknown
no warnings qw(qw);
my @notes =
qw(C-1 C#1 D-1 D#1 E-1 F-1 F#1 G-1 G#1 A-1 A#1 B-1 C-2 C#2 D-2 D#2 E-2 F-2 F#2 G-2 G#2 A-2 A#2 B-2 C-3 C#3 D-3 D#1 E-3 F-3 F#3 G-3 G#3 A-3 A#3 B-3);
my @periods =(
[qw(358 328 2FA 2D0 2A6 280 25C 23A 21A 1FC 1E0 1C5 1AC 194 17D 168 153 140 12E 11D 10D FE F0 E2 D6 CA BE B4 AA A0 97 8F 87 7F 78 71)], # tuning 0
[qw(352 322 2F5 2CB 2A2 27D 259 237 217 1F9 1DD 1C2 1A9 191 17B 165 151 13E 12C 11C 10C FD EF E1 D5 C9 BD B3 A9 9F 96 8E 86 7E 77 71)], # tuning 1
[qw(34C 31C 2F0 2C5 29E 278 255 233 214 1F6 1DA 1BF 1A6 18E 178 163 14F 13C 12A 11A 10A FB ED E0 D3 C7 BC B1 A7 9E 95 8D 85 7D 76 70)], # tuning 2
[qw(346 317 2EA 2C0 299 274 250 22F 210 1F2 1D6 1BC 1A3 18B 175 160 14C 13A 128 118 108 F9 EB DE D1 C6 BB B0 A6 9D 94 8C 84 7D 76 6F)], # tuning 3
[qw(340 311 2E5 2BB 294 26F 24C 22B 20C 1EF 1D3 1B9 1A0 188 172 15E 14A 138 126 116 106 F7 E9 DC D0 C4 B9 AF A5 9C 93 8B 83 7C 75 6E)], # tuning 4
[qw(33A 30B 2E0 2B6 28F 26B 248 227 208 1EB 1CF 1B5 19D 186 170 15B 148 135 124 114 104 F5 E8 DB CE C3 B8 AE A4 9B 92 8A 82 7B 74 6D)], # tuning 5
[qw(334 306 2DA 2B1 28B 266 244 223 204 1E7 1CC 1B2 19A 183 16D 159 145 133 122 112 102 F4 E6 D9 CD C1 B7 AC A3 9A 91 89 81 7A 73 6D)], # tuning 6
[qw(32E 300 2D5 2AC 286 262 23F 21F 201 1E4 1C9 1AF 197 180 16B 156 143 131 120 110 100 F2 E4 D8 CC C0 B5 AB A1 98 90 88 80 79 72 6C)], # tuning 7
[qw(38B 358 328 2FA 2D0 2A6 280 25C 23A 21A 1FC 1E0 1C5 1AC 194 17D 168 153 140 12E 11D 10D FE F0 E2 D6 CA BE B4 AA A0 97 8F 87 7F 78)], # tuning -8
[qw(384 352 322 2F5 2CB 2A3 27C 259 237 217 1F9 1DD 1C2 1A9 191 17B 165 151 13E 12C 11C 10C FD EE E1 D4 C8 BD B3 A9 9F 96 8E 86 7E 77)], # tuning -7
[qw(37E 34C 31C 2F0 2C5 29E 278 255 233 214 1F6 1DA 1BF 1A6 18E 178 163 14F 13C 12A 11A 10A FB ED DF D3 C7 BC B1 A7 9E 95 8D 85 7D 76)], # tuning -6
[qw(377 346 317 2EA 2C0 299 274 250 22F 210 1F2 1D6 1BC 1A3 18B 175 160 14C 13A 128 118 108 F9 EB DE D1 C6 BB B0 A6 9D 94 8C 84 7D 76)], # tuning -5
[qw(371 340 311 2E5 2BB 294 26F 24C 22B 20C 1EE 1D3 1B9 1A0 188 172 15E 14A 138 126 116 106 F7 E9 DC D0 C4 B9 AF A5 9C 93 8B 83 7B 75)], # tuning -4
[qw(36B 33A 30B 2E0 2B6 28F 26B 248 227 208 1EB 1CF 1B5 19D 186 170 15B 148 135 124 114 104 F5 E8 DB CE C3 B8 AE A4 9B 92 8A 82 7B 74)], # tuning -3
[qw(364 334 306 2DA 2B1 28B 266 244 223 204 1E7 1CC 1B2 19A 183 16D 159 145 133 122 112 102 F4 E6 D9 CD C1 B7 AC A3 9A 91 89 81 7A 73)], # tuning -2
[qw(35E 32E 300 2D5 2AC 286 262 23F 21F 201 1E4 1C9 1AF 197 180 16B 156 143 131 120 110 100 F2 E4 D8 CB C0 B5 AB A1 98 90 88 80 79 72)]);# tuning -1
die "Usage: $0 <modfile> <samplefile> <mod.output>\n" unless @ARGV == 3;
my $module = readbin($ARGV[0]);
my $samples = readbin($ARGV[1]);
die "bad module id" unless get('A[4]', 0, 4) eq 'BWB.';
# read offsets
my ($off_poslist, $off_pattstarts, $off_patterns, $off_sfx, $off_songstarts) = get ('n[5]', 4, 10);
die "weird order" unless $off_songstarts < $off_poslist and
$off_poslist < $off_pattstarts and
$off_pattstarts < $off_patterns and
$off_patterns < $off_sfx and
$off_sfx < length($module);
my $off_sampleinfo = 16;
my ($end_sampleinfo, $end_songstarts, $end_poslist, $end_pattstarts, $end_patterns, $end_sfx) =
($off_songstarts, $off_poslist, $off_pattstarts, $off_patterns, $off_sfx, length($module));
# read sampleinfo
my @samples;
for (my $p = $off_sampleinfo; $p < $end_sampleinfo; $p += 8) {
push @samples, [get('nCCnn', $p, 8)]; # length (words), finetune, volume, repeat, repeatlen
}
while (@samples < 31) {
push @samples, [0, 0, 0, 0, 1];
}
map {print "sample: ".join(' ',@{$_})."\n"} @samples;
my @patterns;
for (my $p = $off_pattstarts; $p < $end_pattstarts; $p += 2) {
my $rowp = $off_patterns + get('n', $p, 2);
my $end = ($p == ($end_pattstarts - 2))
? $end_patterns
: ($off_patterns + get('n', $p+2, 2));
die if $rowp > $end or $rowp > $end_patterns or $end > $end_patterns;
my (@row, $b1, $b2, $b3, $rep);
while ($rowp < $end) {
$b1 = getbyte($rowp++);
if ($b1 & 0x80) {
$rep = $b1 & 0x3F;
if ($b1 & 0x40) {
$b1 = $b2 = $b3 = 0;
}
else {
$b1 = getbyte($rowp++);
$b2 = getbyte($rowp++);
$b3 = getbyte($rowp++);
}
}
else {
$rep = 0;
$b2 = getbyte($rowp++);
$b3 = getbyte($rowp++);
}
my $inst = (($b1 & 0x40) >> 2) | (($b2 & 0xF0) >> 4);
my $note = $b1 & 0x3F;
my $fx1 = $b2 & 0x0F;
my $fx2 = $b3;
die "note value out of range ($note > $#notes)" if $note > $#notes;
die "instrument number out of range ($inst > ".scalar(@samples).")" if $inst > @samples;
for (0 .. $rep) {
push @row, [$inst, $note, $fx1, $fx2];
#printf "%s %02x %01x%02x\n", ($note > 0 ? $notes[$note-1] : '---'), $inst, $fx1, $fx2;
}
}
push @patterns, \@row;
}
my (%seen, @new_posns, @new_patterns);
for (my $p = $off_poslist+2+4; $p < $end_poslist; $p += 4) {
my ($ch0, $ch1, $ch2, $ch3) = get('C[4]', $p, 4);
my @pattern = ($patterns[$ch0], $patterns[$ch1], $patterns[$ch2], $patterns[$ch3]);
my $pattern;
print "pattern: $ch0 $ch1 $ch2 $ch3\n";
for my $row (0 .. 63) {
for my $ch (0 .. 3) {
my ($inst, $note, $fx1, $fx2) = @{$pattern[$ch]->[$row]};
($inst, $note, $fx1, $fx2) = (0,0,0,0) if $ch == 3; # always skip sfx channel
my $period = $inst == 0 || $note == 0 ? 0 : hex $periods[$samples[$inst - 1]->[1]]->[$note - 1];
printf "%s %02x %01x%02x | ", ($note > 0 ? $notes[$note-1] : '---'), $inst, $fx1, $fx2;
$pattern .= pack 'C[4]',
($inst & 0x10) | ($period >> 8),
($period & 0xFF),
(($inst & 0xF) << 4) | $fx1,
$fx2;
}
print "\n";
}
if (not exists $seen{$pattern}) {
push @new_patterns, $pattern;
$seen{$pattern} = $#new_patterns;
}
push @new_posns, $seen{$pattern};
}
# write module in protracker format
if (open my $fh, '>', $ARGV[2]) {
binmode $fh;
print $fh pack 'a[20]', ''; # module name
map {print $fh pack 'a[22]nCCnn', '', @{$_}} @samples; # sample info
print $fh pack 'CC', $#new_posns, 127; # song length
print $fh pack 'C[128]', @new_posns, (0) x 128; # song positions
print $fh 'M.K.'; # identifier
print $fh @new_patterns; # patterns
print $fh $samples; # samples
close $fh;
}
else {
die "Can't write to $ARGV[2]: $!\n";
}
sub get {
return unpack $_[0], substr $module, $_[1], $_[2];
}
sub getbyte {
return get('C', $_[0], 1);
}
sub readbin {
my $out;
die "Can't open $_[0]: $!\n" unless open my $fh, '<', $_[0];
binmode $fh;
die "Can't read $_[0]: $!\n" unless read $fh, $out, (-s $_[0]);
close $fh;
return $out;
}