[comp.sys.ibm.pc] Playing digitized sound

lukrw@vax1.cc.lehigh.edu (05/28/90)

Since there seems to be quite a bit of interest in playing digitized
sound on PC speakers, I'm including the source for a small Turbo Pascal
program to demonstrate the technique.  This program can be easily
modified for any type of sample data.  -- Kevin Weiner

---------------------------------------------------------------------------
program spkr;

{ This Turbo Pascal program demonstrates a technique for playing
  digitized sound through the speaker on faster 286's or 386's.  This
  method employs pulse width modulation at 16KHz to produce the equivalent
  of 6-bit resolution.  Basically, the system timer interrupt is taken
  over and speeded up to 16KHz, rather than the normal 18.2 Hz.  At each
  interrupt, the next sound sample (scaled to a range of 0-72) is used as
  the duty cycle for the speaker timer, i.e., the amount of time the
  output stays high between interrupts.  Low-pass filtering by the ear is
  mainly responsible for reconstruction of the original signal.  This
  technique is equivalent to that being employed in "many times"
  over-sampling systems, where it is cheaper to use very high speed 1-bit
  DACs and low pass filter the output, rather than using costly
  high-resolution DACs at lower rates. 

  Sound input to the program is assumed to be 16-bit samples in Turtle
  Beach SampleVision format, though it would be fairly easy to substitute
  another source.  The program expects one command line parameter, which
  is the name of the SampleVision file.  Rate conversion to 16KHz is done
  assuming the nearest 8KHz increment in the sampling range 8KHz to 48KHz. 

  There is currently a problem which has yet to be satisfactorily
  resolved.  Occasionally a high-pitched shrill will be heard over the
  sound.  This appears to related to the initialization sequence of the
  programmable interval timer.  In general, however, the sound quality
  is very intelligible, especially for voice.  Note that unless the
  original signal is highly compressed, the low sound level of the pc
  speaker will result in very soft output.  In fact, pushing the sample
  level beyond clipping may actually help in some cases.  (Remember, this
  is meant to be merely functional, not audiophile quality).  Good Luck! }

{$r-,s-}

uses dos, crt;

const

  blocksize = 16383;                   { Samples are stored in up to 30  }
  maxblocks = 30;                      {  blocks of 16K }

type

  loop =   record                      {Misc SampleVision data structures}
             start, stop: longint;
             looptype: byte;
             count: integer;
           end;

  marker = record
             name: packed array [1..10] of char;
             pos: longint;
           end;

  block = array [1..blocksize] of byte;   {One chunk of sample values}

  blockptr = ^block;

var

  header:  record                                   {SV file header}
             id: packed array [1..18] of char;
             ver: packed array [1..4] of char;
             comment: packed array [1..60] of char;
             name: packed array [1..30] of char;
             samplesize: longint;
           end;

  trailer: record                                   {SV file trailer}
             fill: integer;
             loops: array [1..8] of loop;
             markers: array[1..8] of marker;
             note: byte;
             rate, smpte, cycle: longint;
           end;

  headersize, trailersize: integer;

  infile: file;
  iname: string;

  buf: array [1..blocksize] of integer;
  blocks: array [1..maxblocks] of blockptr;
  nblocks, lastblocksize: integer;
  currentblock: blockptr;
  blocknum, samplenum, samplelen: integer;
  nsamples, ticks: longint;
  nextsample, rateinc, ratemask, alt: byte;

  irqmask: byte;                       {IRQ mask for PIC}
  savevec8: pointer;                   {Saved interrupt vector}

function inport(x: integer): byte;
  inline($5a/$eb/$00/$ec);             {Read port}

procedure cli;
  inline($fa);                         {Clear interrupts}

procedure sti;
  inline($fb);                         {Enable interrupts}

procedure restore;

  { Restore DOS timer and update system time }

  var
    dostime: ^longint;

  begin
    cli;
    port[$43] := $34;                  {Restore counter 0}
    port[$40] := $ff;
    port[$40] := $ff;

    port[$61] := inport($61) and $fc;  {Turn off speaker}

    dostime := ptr($0040,$006c);       {Fix DOS time of day counter}
    ticks := ticks + samplenum;
    ticks := ticks div rateinc;
    if ratemask <> 0 then ticks := ticks * 2;
    dostime^ := dostime^ + ticks * $48 div $ffff;

    setintvec(8, savevec8);            {Restore old interrupt}
    port[$21] := irqmask;
    sti;
  end;

procedure timerint; interrupt;

  {Routine called at each timer interrupt (16K times/sec)}

  begin
    port[$42] := nextsample;           {Set speaker pulse width}

    { Prepare sample for next interrupt so there is no delay.
      The funny formulas just allow for proper stepping through
      the array at different sample rates. }

    samplenum := samplenum + rateinc and alt;
    alt := alt xor ratemask;

    if samplenum > samplelen then      {At end of block}
      begin
        inc(blocknum);
        if blocknum > nblocks then
          restore                      {Done}
        else
          begin                        {Set up next block}
            currentblock := blocks[blocknum];
            if blocknum = nblocks then
              samplelen := lastblocksize;
            samplenum := 1;
            ticks := ticks + blocksize;    {Keep track of total interrupts }
          end;                             { so we can fix up system clock }
      end;

    nextsample := currentblock^[samplenum];

    port[$20] := $20;                  {Ack int to PIC}

  end;

procedure idle;

  { Just waste time here while interrupt routine does the real work }

  begin
    repeat
    until blocknum > nblocks;
  end;

procedure startsound;

  { Initialize the actual sound process }

  begin
    samplenum := 1;                    {Prepare first sample}
    if nblocks > 1 then
      samplelen := blocksize
    else
      samplelen := lastblocksize;
    blocknum := 1;
    currentblock := blocks[blocknum];
    nextsample := currentblock^[1];
    ticks := 0;

    cli;                               {Disable interrupts}
    irqmask := inport($21);            {Save interrupt mask}
    port[$21] := $fe;                  {Enable only timer int}
    getintvec(8, savevec8);            {Get int 8 address}
    setintvec(8, @timerint);           {Substitute interrupt routine}
    port[$43] := $90;                  {Ctr 2, mode 0, LSB only}
    port[$42] := $00;                  {Init ctr 2}
    port[$61] := inport($61) or 3;     {Enable speaker, gate counter}
    port[$43] := $34;                  {Ctr 0, mode 2, LSB+MSB}
    port[$40] := $48;                  {LSB; period = 72 * .84 microsec}
    port[$40] := $00;                  {MSB}
    sti;                               {Re-enable interrupts}
  end;

procedure initsamp;

  { Set up to read sample data }

  var i: integer;

  begin
    iname := paramstr(1);
    if iname = '' then
      begin
        writeln('File name not specified');
        halt;
      end;
{$i-}
    assign(infile, iname);
    reset(infile, 1);
{$i+}
    if ioresult <> 0 then
      begin
        writeln('File ',iname,' not found');
        halt;
      end;
    headersize := sizeof(header);
    trailersize := sizeof(trailer);
    blockread(infile, header, headersize, i);
    if i <> headersize then halt;
  end;

procedure getsample;

  { Load sample data }

  var
    i, k, b, size, len: integer;
    done: boolean;
    ptr: blockptr;

  begin
    initsamp;
    nsamples := header.samplesize;
    if nsamples > memavail then        {Read as much as will fit in memory}
      nsamples := memavail div blocksize * blocksize;
    nblocks := nsamples div blocksize;
    lastblocksize := nsamples - nblocks * blocksize;
    if lastblocksize <> 0 then
      nblocks := nblocks + 1;
    write('Reading');
    k := 1;
    done := false;
    repeat
      if k = nblocks then
        size := lastblocksize
      else
        size := blocksize;
      blockread(infile, buf, size*2, len);
      write('.');
      len := len div 2;
      if len <> size then
        begin
          writeln('Error reading file');
          halt;
        end;
      new(ptr);
      blocks[k] := ptr;
      for i := 1 to len do
        begin
          b := buf[i] div 920 + 36;      {Scale to range of 0-72 (samples
                                          are 16 bits).  Change 920 to a
                                          smaller value to increase volume,
                                          but beware of clipping. }
          if b > 72 then
            b := 72
          else if b < 0
            then b := 0;
          ptr^[i] := b;
        end;
      k := k + 1;
      if k > nblocks then done := true;
    until done;
    blockread(infile, trailer, trailersize, i);

    { Compute parameters for rate conversion }
    alt := 7;
    k := trailer.rate div 1000;
    case k of                                           {Rate centers}
      5..12:  begin rateinc := 1; ratemask := 7; end;       {8K}
      13..20: begin rateinc := 1; ratemask := 0; end;       {16K}
      21..28: begin rateinc := 3; ratemask := 7; end;       {24K}
      29..36: begin rateinc := 2; ratemask := 0; end;       {32K}
      37..44: begin rateinc := 5; ratemask := 7; end;       {40K}
      45..52: begin rateinc := 3; ratemask := 0; end;       {48K}
      else
        begin
          writeln('Sample rate not supported: ',trailer.rate);
          halt;
        end;
    end {case};

    close(infile);
    writeln('Done');
  end;


begin
  getsample;
  startsound;
  idle;
end.