{$A+,B-,D+,E+,F-,G+,I-,L+,N-,O-,P-,Q-,R-,S-,T-,V+,X+,Y+}
{$M 16384,0,655360}

program modplay;

uses mixer,dos,crt;  { see bottom for MIXER code }

{ Program by Borek (Marcin Borkowski), Warsaw, Poland.
  You can find me in a Top Secret BBS, +48 2 6788783
  Fido 2:480/25, Pascal Net 115:4804/104

  This program needs fast machine, as it is written in
  plain vanilla Pascal. If you are looking for proffesional
  quality implementation of MOD player, keep trying
  (I'm sure you'll have to paid for that). If you are looking
  for a source to understand and start work by yourself -
  you've got what you are looking for! You may freely copy
  and use this source, as long as it is unchanged and states my
  name in the begining. If you find more profitable use of this
  code, feel free to share your profits with me! At least - let
  me know you were able to use it for your own purposes.

  This program should be accompanied by the MIXER unit source.

  Attention - this implementation of playing MOD's is BAD
  and will probably not work on some MOD's with changed
  order of playing patterns, also MOD's based on effects
  will not be played properly. Effects (and many other things -
  as sample volumes, finetuning) aren't implemented, and in
  fact that's only a sketch. Program is probably bugged, but
  for sure it plays 40% MOD's from my BBS in an acceptable way.

  Parts of code ripped from PCPGE, various sources from
  Ethan Brodsky and probably from SWAG. I can't remember
  source of every byte, but for sure 95% of code is mine.

  When this code was posted for the first time, I've got some
  stupid questions, here are stupid answers:
  1. Borland Pascal 7.0
  2. Sound Blaster 2.0
  3. Yes, I'm nearly bald. }

const
  TIMERINTR = 8;
  PIT_FREQ  = $1234DD;

type
  sampledata = array[0..65533]of byte;
  pattern    = array[0..63,0..15]of byte;
  patptr     = ^pattern;

var
  BIOSTimerHandler    : procedure;
  clock_ticks,counter : longint;

  playend : boolean;
  ticks,speed : word;
  divplayed : word;
  patplayed : word;
  patterns : array[0..255]of patptr;
  patternorder : array[0..127]of byte;
  fmod    : file;
  hdr     : array[1..1084]of byte;
  nofsamples : word;
  nofpatterns : word;
  OrigTextMode : integer;
  OldExit : pointer;
  timerset : boolean;
  samples : array[1..31]of record
                             sname : string[22];
                             length : word;
                             finet  : byte;
                             volume : byte;
                             repst,repend : word;
                           end;

function amiword(w : word): word;
{ Data in MOD files are usually in 68000 format. }
assembler;
asm
  mov ax,w
  xchg ah,al
end;

procedure nextdivision;
var
{ Sometimes data have to be treated as bytes,
  sometimes as words. Let's use some tricks! }
  divis : array[0..15]of byte;
  diwis : array[0..7]of word absolute divis;
  smp,tmp,freq : word;
  eff,arg : byte;
  i       : integer;
begin
  if divplayed=64 then
  begin
    divplayed:=0;
    inc(patplayed);
  end;
  if patplayed>=hdr[951] then
  begin
    playend:=true;
    EXIT
  end;

  gotoxy(1,43);
  write('Pattern: ',patplayed+1:2,'/',hdr[951],
        '    division: ',divplayed:2);
  move(patterns[patternorder[patplayed]]^[divplayed,0],divis,16);

  for i:=0 to 3 do
  begin
    smp:=(divis[4*i+0] and $F0)+divis[4*i+2] shr 4;  {sample number}
    tmp:=(amiword(diwis[2*i]) and $0FFF);      {sample period}
    if tmp<>0 then freq:=3546895 div tmp;
    if smp<>0 then startchannel(i+1,smp,64,freq)
              else if tmp<>0 then setchannelfrequency(i+1,freq);

    gotoxy(1,45+i);
    write(' channel ',i,': (',smp:2,') ',samples[smp].sname:22);
    if tmp<>0 then write(' Hz: ',freq:5,' ':25)
              else write(' Hz: ',' ':30);
    gotoxy(53,45+i);

    eff:=divis[4*i+2] and $0F;                 {effect number}
    arg:=divis[4*i+3];                         {effect argument}
    case eff of
      $0C : begin
              setchannelvolume(i+1,arg);
              write('volume change       ');
            end;
      $0F : if arg<>0 then
            begin
              speed:=arg;
              write('speed change        ');
            end;
      $0B : begin
              divplayed:=63;
              patplayed:=arg-1;
              write('pattern break       ');
            end;
      $0D : begin
              inc(patplayed);
              divplayed:=10*(arg shr 4)+arg and $0F-1;
              write('pattern jump        ');
            end;
      else if eff<>0 then
              write('unimplemented effect');
    end;
  end;
  inc(divplayed);
end;

procedure play;
interrupt;
begin
  inc(ticks);
  if ticks=speed then
  begin
    ticks:=0;
    nextdivision;
  end;
  clock_ticks := clock_ticks + counter;
  if clock_ticks >= $10000 then
    begin
      clock_ticks := clock_ticks - $10000;
      asm pushf end;
      BIOSTimerHandler;
    end
  else Port[$20] := $20;
end;

procedure CleanUpTimer;
begin
  Port[$43] := $34;
  Port[$40] := 0;
  Port[$40] := 0;
  SetIntVec(TIMERINTR, @BIOSTimerHandler);
end;

procedure SetTimer(TimerHandler : pointer; frequency : word);
begin
  clock_ticks := 0;
  counter := $1234DD div frequency;
  GetIntVec(TIMERINTR, @BIOSTimerHandler);
  SetIntVec(TIMERINTR, TimerHandler);
  Port[$43] := $34;
  Port[$40] := counter mod 256;
  Port[$40] := counter div 256;
  timerset:=true
end;

{$F+}
procedure shutdown;
begin
  if timerset then CleanUpTimer;
  exitproc:=oldexit
end;
{$F-}

procedure error(s : string);
begin
  textmode(OrigTextMode);
  writeln(s);
  close(fmod);
  HALT
end;

procedure openmod;
var
  s : string;
begin
  if paramcount=0 then error('Needs filename (*.mod) as parameter');
  assign(fmod,paramstr(1));
  reset(fmod,1);
  if IOResult<>0 then error('Can''t open file: '+paramstr(1));
  blockread(fmod,hdr,1084);
  move(hdr[1081],s[1],4);
  s[0]:=#4;
  if not((s='M.K.') or (s='M!K!') or (s='FLT4') or (s='4CHN')) then
    error('Invalid mod file format tag: '+s);
  nofsamples:=31;
  move(hdr[1],s[1],20);
  s[0]:=#1;
  while s[ord(s[0])]<>#0 do inc(s[0]);
  writeln('Song name: ',s);
  writeln
end;

procedure getsamples;
var
  i : integer;
  s : string;
  w : word;
  totalsamplength : longint;
  p : pointer;
begin
  writeln('Number     Name':20,'Length  Vol    RepSt   RepEnd':48);
  totalsamplength:=0;
  for i:=1 to nofsamples do with samples[i] do
  begin
    move(hdr[21+(i-1)*30],sname[1],21);
    sname[0]:=#1;
    while sname[ord(sname[0])]<>#0 do inc(sname[0]);
    move(hdr[43+30*(i-1)],w,2);    length:=amiword(w) shl 1;
    inc(totalsamplength,length);
    volume:=hdr[46+30*(i-1)];
    move(hdr[47+30*(i-1)],w,2);    repst:=amiword(w) shl 1;
    move(hdr[49+30*(i-1)],w,2);    repend:=repst+amiword(w) shl 1;
    writeln(' sample ',i:2,': ',sname:22,length:11,
            volume:5,repst:8,repend-2:8);
  end;
  nofpatterns:=(filesize(fmod)-1084-totalsamplength) div 1024;
  writeln;
  writeln('Number of different patterns in song: ',nofpatterns);
  seek(fmod,filesize(fmod)-totalsamplength);
  for i:=1 to nofsamples do
    with samples[i] do
      if length>2 then
      begin
        getmem(p,length);
        blockread(fmod,w,2);
        blockread(fmod,p^,length-2);
      { Convert sample to appropriate format. }
        for w:=0 to length-3 do inc(sampledata(p^)[w],128);
        addvoice(i,length-2,repst,repend-2,p)
      end;
  if IOResult<>0 then error('Something went wrong during samples reading.')
end;

procedure getpatterns;
var
  i : integer;
begin
  seek(fmod,1084);
  for i:=0 to nofpatterns-1 do
  begin
    new(patterns[i]);
    blockread(fmod,patterns[i]^,1024)
  end;
  seek(fmod,952);
  blockread(fmod,patternorder,128);
  close(fmod);
end;

procedure startplay;
begin
  speed:=6;
  divplayed:=0;
  patplayed:=0;
  SetTimer(@play,50);
  playend:=false;
end;

begin
  OrigTextMode:=LastMode;
  timerset:=false;
  TextMode(C80+Font8x8);
  clrscr;
  OldExit:=ExitProc;
  ExitProc:=@ShutDown;
  openmod;
  getsamples;
  getpatterns;
  startplay;
  repeat until keypressed or playend;
  textmode(OrigTextMode)
end.


{ MIXER }

{$A+,B-,D+,E-,F-,G+,I-,L+,N-,O-,P-,Q-,R-,S-,T-,V+,X+,Y+}
{$M 16384,0,655360}

unit mixer;

{ Program by Borek (Marcin Borkowski), Warsaw, Poland.
  You can find me in a Top Secret BBS, +48 2 6788783
  Fido 2:480/25, Pascal Net 115:4804/104

  If you can't recognize - it is Sound Blaster version.

  You may freely copy and use this source, as long it is
  unchanged and states my name in the begining. If you find
  more profitable use of this code, fell free to share your
  profits with me! At least - let me know you were able to
  use it for your own purposes.

  This unit should be accompanied by the MODPLAY source.

  This version of mixing (even at 44 kHz) works OK on my
  UMC 40 MHz machine (that's a little bit less than 486 50 MHz).
  If you want to make it work on 386DX 33MHz, you must 'unroll'
  the main loop and put used there data into 'hardcoded' variables.
  Such a version works on my old 386 and on several other
  computers of the same class, even with QEMM.

  If you stop this program by ctrl break, you may not be able to
  restart it without resetting your computer - and that's not the
  only one bug I know in the code.

  Parts of code ripped from PCPGE, various sources from
  Ethan Brodsky and probably from SWAG. I can't remember
  source of every byte, but for sure 95% of code is mine.}

interface

procedure addvoice(voice,_samplesize,_loopstart,_loopend : word;
                   sample : pointer);
procedure startchannel(channel,voice,volume,frequency : word);
procedure stopchannel(channel : word);
procedure setchannelfrequency(channel,frequency : word);
procedure setchannelvolume(channel,volume : word);

implementation

uses crt,dos;

const
{ Mixer data }
  max_num_voices = 32; { number of voices }
  max_num_channels = 4; { number of channels }
  PlayFreq = 22222; { samples played at, at 11111 sound is very bad. }
  playbufsize = 512; { Size of play buffer}
{ SB data - change it for your card settings. }
  SBIO    : word = 2;  { 2x0 }
  SBIRQ   : word = 7;

type
  sampledata = array[0..65533]of byte;
  _channel   = record
                 nvoice,position,increment,subposition,vol : word;
                 inloop,active : boolean
               end;

var
{ Pointers to samples. }
  voicesdata : array[1..max_num_voices]of ^sampledata;
{ Sizes of voices }
  voicessize : array[1..max_num_voices]of word;
{ Those two defines begining and end of loop in sample }
  voicesloopstart : array[1..max_num_voices]of word;
  voicesloopend : array[1..max_num_voices]of word;
{ Voice ready to use? }
  voicesdefined : array[1..max_num_voices]of boolean;
{ Which voice played in this channel? }
  channelsnvoice : array[1..max_num_channels]of word;
{ Which position in sample? }
  channelsposition : array[1..max_num_channels]of word;
{ How to increment subposition? }
  channelsincrement : array[1..max_num_channels]of word;
{ Which subposistion in position - it allows to change frequencies,
  as one byte of sample can be played several times. It gives not a
  perfect sound, but it works. To improove sound quality, one should
  use interpolation (and assembler :-) }
  channelssubposition : array[1..max_num_channels]of word;
{ Volume of channel }
  channelsvol : array[1..max_num_channels]of word;
{ Is sample in this channel loop? }
  channelsinloop : array[1..max_num_channels]of boolean;
{ Channel being played? }
  channelsactive : array[1..max_num_channels]of boolean;

{ SB addresses }
  DSP_RESET        : word;
  DSP_READ_DATA    : word;
  DSP_WRITE_DATA   : word;
  DSP_WRITE_STATUS : word;
  DSP_DATA_AVAIL   : word;

  timeconst        : byte;
  playbuf          : pointer;
  oldint,oldexit   : pointer;
  firstbuff        : boolean;

function carry : boolean;
inline($B0/$01/     {  mov al,01 }
       $72/$02/     {  jc @carryset }
       $30/$C0);    {  xor al,al }
                    { @carryset: }

function ResetDSP(base : word) : boolean;
begin
  base := base * $10;
  DSP_RESET := base + $206;
  DSP_READ_DATA := base + $20A;
  DSP_WRITE_DATA := base + $20C;
  DSP_WRITE_STATUS := base + $20C;
  DSP_DATA_AVAIL := base + $20E;
  Port[DSP_RESET] := 1;
  Delay(1);
  Port[DSP_RESET] := 0;
  Delay(1);
  if (Port[DSP_DATA_AVAIL] And $80 = $80) And (Port[DSP_READ_DATA] = $AA)
     then ResetDSP := true
     else ResetDSP := false;
end;

procedure WriteDSP(value : byte);
begin
  while Port[DSP_WRITE_STATUS] And $80 <> 0 do;
  Port[DSP_WRITE_DATA] := value;
end;

function ReadDSP : byte;
begin
  while Port[DSP_DATA_AVAIL] and $80 = 0 do;
  ReadDSP := Port[DSP_READ_DATA];
end;

function SpeakerOn: byte;
begin
  WriteDSP($D1);
end;

function SpeakerOff: byte;
begin
  WriteDSP($D3);
end;

procedure Playback;
var
  page,offset,size   : word;
begin
{ SB and DMA are working in autoinit modes - but DMA buffer is
  twice as long as SB buffer. Each time SB buffer is finished an
  IRQ is generated - a signal that next part of samples should be
  mixed. Play buffer has two parts - when one is played, second
  is being filled. Simple, uh? This version of procedure was checked
  on AWE 32, on SB 16 and on several clones of SB, but for sure
  it'll not work on some cards - especially on older versions
  that are not supporting autoinit mode. }
  firstbuff:=true;
  size := playbufsize-1;
  offset := Seg(playbuf^) Shl 4 + Ofs(playbuf^);
  page := (Seg(playbuf^) + Ofs(playbuf^) shr 4) shr 12;
{ DMA programming }
  Port[$0A] := 5;
  Port[$0C] := 0;
  Port[$0B] := $59; { DMA autoinit }
  Port[$02] := Lo(offset);
  Port[$02] := Hi(offset);
  Port[$83] := page;
  Port[$03] := Lo(size);
  Port[$03] := Hi(size);
  Port[$0A] := 1;
{ SB programming }
  WriteDSP($40);
  WriteDSP(timeconst);
  WriteDSP($48); { 8-bit sample type with autoinit}
  WriteDSP(Lo(playbufsize shr 1-1));
  WriteDSP(Hi(playbufsize shr 1-1));
  WriteDSP($1C) {???? I don't know why, but it is necessary }
end;

procedure mix;
{ Main procedure - mixes samples with appropriate frequencies and
  puts mixed signal into play buffer. If it's too slow for your
  computer, don't blame me - but translate procedure into assembler
  (or buy something faster :-) }
var
  i,j : integer;
  nvoice,sw  : word;
  pombuf : ^sampledata;
begin
{ Pointer to play buffer - is it first, or second part? }
  if firstbuff then pombuf:=@sampledata(playbuf^)[playbufsize div 2]
               else pombuf:=playbuf;
  for i:=0 to playbufsize div 2-1 do
  begin
    sw:=0;
    for j:=1 to max_num_channels do
      if channelsactive[j] then
      begin
        nvoice:=channelsnvoice[j];
{ That's mixing - without interpolation. }
        inc(sw, voicesdata[nvoice]^[channelsposition[j]] * channelsvol[j]);
{ Here is the most important thing - next two lines (excluding
  remarks:) are responsible for output frequency of sample.  }
        inc(channelssubposition[j],channelsincrement[j]);
{ That's a nasty trick - but it works. You can do it other ways,
  in Pascal, Assembler and so on. }
        if carry then
        begin
          inc(channelsposition[j]);
{ Now work with looped samples. }
          if channelsinloop[j] then
            if channelsposition[j] > voicesloopend[nvoice] then
                   channelsposition[j] := voicesloopstart[nvoice];
{ Maybe we should stop playing this sample? Or put it in a loop mode? }
          if channelsposition[j] > voicessize[nvoice] then
            if voicesloopstart[nvoice]<>0 then
            begin
              channelsposition[j] := voicesloopstart[nvoice];
              channelsinloop[j]:=true
            end
            else channelsactive[j]:=false;
        end;
      end;
    pombuf^[i]:=Lo(sw shr 6);
  end;
end;

procedure inthandler;
interrupt;
var
  w : word;
begin
  w:=Port[DSP_DATA_AVAIL];
  port[$20]:=$20;
  firstbuff:=not firstbuff;
  mix;
end;

procedure enableIRQ(n : byte);
begin
  port[$21]:=port[$21] and not (1 shl n)
end;

procedure disableIRQ(n : byte);
begin
  port[$21]:=port[$21] or (1 shl n)
end;

procedure allocmem(var p : pointer);
var
  adr : longint;
begin
{ Allocates memory not crossing page boundary ($X0000) }
  repeat
    getmem(p,playbufsize);
    adr:=longint(Seg(p^)) Shl 4 + Ofs(p^);
  until (adr and $FFFF)<$FFFF-playbufsize
end;

{$F+ }
procedure MixerExit;
begin
  ExitProc:=OldExit;
  WriteDSP($D0); { ??? }
  speakeroff;
  disableIRQ(SBIRQ);
  setintvec(8+SBIRQ,oldint);
  ResetDSP(SBIO);
end;
{$F- }

procedure initplayloop;
begin
  if not ResetDSP(SBIO) then HALT;
  allocmem(playbuf);
  getintvec(8+SBIRQ,oldint);
  setintvec(8+SBIRQ,@inthandler);
  enableIRQ(SBIRQ);
  speakeron;
  timeconst:=256-1000000 div PlayFreq;
  fillchar(playbuf^,playbufsize,#0);
  playback;
  OldExit:=ExitProc;
  ExitProc:=@MixerExit
end;

procedure addvoice(voice,_samplesize,_loopstart,_loopend : word;
                   sample : pointer);
begin
  voicesdata[voice]:=sample;
  voicessize[voice]:=_samplesize;
  voicesloopstart[voice]:=_loopstart;
  voicesloopend[voice]:=_loopend;
  voicesdefined[voice]:=true
end;

procedure startchannel(channel,voice,volume,frequency : word);
begin
  asm cli end;
  if not channelsactive[channel] then channelsactive[channel]:=true;
  channelsinloop[channel]:=false;
  channelsnvoice[channel]:=voice;
  channelsincrement[channel]:=(longint(frequency) shl 16-1) div PlayFreq;
  if (volume>=0) and (volume<=16) then channelsvol[channel]:=volume
                                  else channelsvol[channel]:=16;
  channelssubposition[channel]:=0;
  channelsposition[channel]:=0;
  asm sti end;
end;

procedure stopchannel(channel : word);
begin
  channelsactive[channel]:=false;
  channelsinloop[channel]:=false
end;

procedure setchannelfrequency(channel,frequency : word);
begin
  asm cli end;
  channelsincrement[channel]:=(longint(frequency) shl 16-1) div PlayFreq;
  asm sti end;
end;

procedure setchannelvolume(channel,volume : word);
begin
  asm cli end;  {}
  if (volume>=0) and (volume<=16) then channelsvol[channel]:=volume;
  asm sti end;  {}
end;

begin
  fillchar(voicesdata,sizeof(voicesdata),#0);
  fillchar(voicessize,sizeof(voicessize),#0);
  fillchar(voicesloopstart,sizeof(voicesloopstart),#0);
  fillchar(voicesloopend,sizeof(voicesloopend),#0);
  fillchar(voicesdefined,sizeof(voicesdefined),#0);
  fillchar(channelsnvoice,sizeof(channelsnvoice),#0);
  fillchar(channelsposition,sizeof(channelsposition),#0);
  fillchar(channelsincrement,sizeof(channelsincrement),#0);
  fillchar(channelssubposition,sizeof(channelssubposition),#0);
  fillchar(channelsvol,sizeof(channelsvol),#0);
  fillchar(channelsinloop,sizeof(channelsinloop),#0);
  fillchar(channelsactive,sizeof(channelsactive),#0);
  initplayloop
end.
