Page 1 of 1

Disposable Hero demo rip: BWB ?

Posted: Fri Jul 11, 2003 8:34 pm
by kyz
Finally, after being on the TODO list for 8 years, I've ripped the music from the Amiga Format demo of Disposable Hero.

I've ripped the replay routine, the mods (I'm not sure of their exact sizes) and the seperate sampledata. I've worked out the most basic elements of the mods.

These are definately some form of Protracker packer. The signature they begin with is "BWB.", which I've never heard of. Does anyone know this packer ID?

You can grab the rip so far from http://kyz.mine.nu/misc/dh.lzx

If noone knows, I'll just have to write the unpacker myself. BTW, mod1 is the main demo level tune, not present in the full game. The boss subsong is very similar to the full game's level2 boss subsong. mod2 is very similar to the shop song in the full game.

Re: Disposable Hero demo rip: BWB ?

Posted: Sat Feb 25, 2017 11:15 pm
by kyz
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;
}