proposal: doom: fix sound timing issues and add music

Tue, 21 Feb 2017 20:32:32 EST
qu7uux@[REDACTED]

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit

proposal: doom: fix sound timing issues and add music

this patchset attempts to 1) solve writes to /dev/audio slowing the whole program down and 2) add music playback. please excuse any lack of clarity in the following explanation; i have tried to keep it short, but if anything is unclear (or wrong) i am available for discussion.

the patch inlined below includes all the necessary changes. the individual parts also exist in my contrib under /n/contrib.9front.org/contrib/qwx/^(mus patch rc) .

i am attaching the file(1) and midi(1) patches described below for convenience.

known issues:

. midi: we may work around the sample loop taking too much time, but
  the issue still exists (for instance, on a t43, it is enough to slow
  the game down considerably when graphics scaling is done on top)

diff -r 9c4b9c67ff16 rc/bin/dmus —– /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rc/bin/dmus Wed Feb 22 02:32:47 2017 +0200 @@ -0,0 +1,35 @@ +#!/bin/rc +tmp=/tmp/dmus.$pid +wnote=() + +fn sigexit{ + if(! ~ $#wnote 0 && test -e $wnote) + echo kill >$wnote + rm -f $tmp $tmpm +} +fn sigint sigterm sigkill{ + exit +} + +while(msg=({read})){ + if(! ~ $#wnote 0){ + if(test -e $wnote) + echo kill &#62;$wnote + wait + wnote=() + } + if(~ $#msg 2 3){ + read -c $msg(2) &#60;/fd/1 &#62;$tmp + x={file $tmp} + if(~ $x(2) mus){ + mus <$tmp >$tmpm || exit + mv $tmpm $tmp + } + c=(‘a=1;’ games/midi -c ‘<$tmp’ ‘||’ ‘a=0;’ echo -n) + if(~ $#msg 3) + c=($c(1) ‘while(~ $a 1)’ $c(2-)) + eval $c & + echo -n >/fd/0 + wnote=/proc/^$apid^/notepg + } +} diff -r 9c4b9c67ff16 sys/man/1/mus —– /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sys/man/1/mus Wed Feb 22 02:32:47 2017 +0200 @@ -0,0 +1,25 @@ +.TH MUS 1 +.SH NAME +mus - MUS to MIDI converter +.SH SYNOPSIS +.B mus +[ +.I musfile +] +.SH DESCRIPTION +The MUS format is a simplified MIDI music format used in +.IR doom (1) +and several related games. +.PP +.I Mus +decodes MIDI music encoded in MUS format, either from +.B musfile +or from standard input, and produces a MIDI format file on standard output. +.SH “SEE ALSO” +.IR doom (1) , +.IR midi (1) +.SH SOURCE +.B /sys/src/games/mus.c +.SH HISTORY +.I Mus +appeared first for 9front (September, 2015). diff -r 9c4b9c67ff16 sys/src/cmd/file.c —– a/sys/src/cmd/file.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/cmd/file.c Wed Feb 22 02:32:47 2017 +0200 @@ -872,6 +872,8 @@

"BEGIN:VCARD\n",    "vCard",        12, "text/directory;profile=vcard",
"AT&#38;T",         "DjVu document",    4,  "image/vnd.djvu",
"Extended module: ",    "XM audio",     17, "audio/xm",

@@ -1653,4 +1655,3 @@

    print("face image depth %d\n", ldepth);
return 1;

}

diff -r 9c4b9c67ff16 sys/src/games/doom/d_main.c —– a/sys/src/games/doom/d_main.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/doom/d_main.c Wed Feb 22 02:32:47 2017 +0200 @@ -326,6 +326,7 @@

 if (!wipe)
 {
I_FinishUpdate ();              // page flip or blit buffer

@@ -347,6 +348,8 @@

I_UpdateNoBlit ();
M_Drawer ();                            // menu is drawn even on top of wipes
I_FinishUpdate ();                      // page flip or blit buffer

@@ -397,12 +400,6 @@

// Update display, next frame, with current state.
D_Display ();

– – // Sound mixing for the buffer is snychronous.

– I_UpdateSound();

diff -r 9c4b9c67ff16 sys/src/games/doom/i_sound.c —– a/sys/src/games/doom/i_sound.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/doom/i_sound.c Wed Feb 22 02:32:47 2017 +0200 @@ -4,6 +4,7 @@ #include “i_sound.h” #include “w_wad.h” // W_GetNumForName() #include “z_zone.h” +#include “m_argv.h”

/ The number of internal mixing channels, ** the samples calculated for each mixing step, @@ -12,7 +13,7 @@ /

/ Needed for calling the actual sound output. / –#define SAMPLECOUNT (512<<2) +#define SAMPLECOUNT (44100/TICRATE) #define NUM_CHANNELS 8

/ The actual lengths of all sound effects. / @@ -26,7 +27,7 @@ are modified and added, and stored in the buffer that is submitted to the audio device. / -signed short mixbuffer[SAMPLECOUNT2]; +uchar mixbuf[SAMPLECOUNT*4];

/ The channel step amount… / uint channelstep[NUM_CHANNELS]; @@ -67,6 +68,26 @@ int channelleftvol_lookup[NUM_CHANNELS]; int channelrightvol_lookup[NUM_CHANNELS];

+static int sndpid; +static int muspid = -1; +static int spfd[2], cpfd[2], mpfd[2]; +static int musread, muson; +static int musvol; + +static void sndproc(void) +{ + int n; + uchar u[65536]; + + for(;;){ + n = read(spfd[0], u, sizeof u); + if(n <= 0) + break; + if(write(audio_fd, u, n) != n) + break; + } +} + static void getsfx(char sfxname, int *len) {

uchar   *sfx;

@@ -124,14 +145,53 @@

return (void *)(paddedsfx + 8);

}

+static void readmusn(void) +{ + int n; + + if(!musread) + return; + n = readn(mpfd[1], mixbuf, sizeof mixbuf); + if(n < 0){ + fprint(2, “readmusn: disabling music: %r\n”); + I_ShutdownMusic(); + }else if(n != sizeof mixbuf) + I_StopSong(0); +} + +static int initprocs(void) +{ + int pid; + + audio_fd = open(“/dev/audio”, OWRITE); + if(audio_fd < 0){ + fprint(2, “initprocs: disabling sound: %r\n”); + return -1; + } + if(pipe(spfd) < 0) + sysfatal(“pipe: %r”); + switch(pid = rfork(RFPROC|RFFDG)){ + case -1: + fprint(2, “initprocs: %r\n”); + return -1; + case 0: + close(spfd[1]); + sndproc(); + exits(nil); + default: + close(spfd[0]); + sndpid = pid; + } + I_InitMusic(); + return 0; +} + void I_InitSound(void) {

int i;

/ This function loops all active (internal) sound @@ -170,117 +228,52 @@ / void I_UpdateSound(void) { – / Mix current sound data. – ** Data, from raw sound, for right and left. – / – register uint sample; – register int dl; – register int dr; + int l, r, i, v; + uchar p, e;

void I_ShutdownSound(void) {

if(audio_fd &#62;= 0) {

-int I_StartSound(int id, int vol, int sep, int pitch, int priority) +int I_StartSound(int id, int vol, int sep, int pitch, int) { – USED(priority); + if(audio_fd < 0) + return -1;

id = addsfx(id, vol, steptable[pitch], sep);
return id;

} @@ -471,53 +465,97 @@

void I_InitMusic(void) { –// printf(“PORTME i_sound.c I_InitMusic\n”); + int pid; + + if(M_CheckParm(“-nomusic”)) + return; + if(pipe(mpfd) < 0 || pipe(cpfd) < 0) + sysfatal(“pipe: %r”); + switch(pid = rfork(RFPROC|RFFDG|RFNOTEG|RFNAMEG)){ + case -1: + fprint(2, “I_InitMusic: %r\n”); + break; + case 0: + dup(cpfd[0], 0); + dup(mpfd[0], 1); + close(cpfd[0]); + close(cpfd[1]); + close(mpfd[0]); + close(mpfd[1]); + execl(“/bin/rc”, “rc”, “-c”, “dmus”, nil); + sysfatal(“execl: %r”); + default: + close(mpfd[0]); + close(cpfd[0]); + muspid = pid; + } }

void I_ShutdownMusic(void) { –// printf(“PORTME i_sound.c I_ShutdownMusic\n”); + musread = 0; + muson = 0; + if(muspid >= 0){ + postnote(PNGROUP, muspid, “interrupt”); + close(mpfd[1]); + close(cpfd[1]); + } + muspid = -1; }

void I_SetMusicVolume(int volume) { – USED(volume); –// printf(“PORTME i_sound.c I_SetMusicVolume\n”); + if(muspid < 0) + return; + musvol = volume; + musread = muson && volume > 0 ? 1 : 0; }

-void I_PauseSong(int handle) +void I_PauseSong(int) { – USED(handle); –// printf(“PORTME i_sound.c I_PauseSong\n”); + musread = 0; }

-void I_ResumeSong(int handle) +void I_ResumeSong(int) { – USED(handle); –// printf(“PORTME i_sound.c I_ResumeSong\n”); + if(muson && musvol > 0) + musread = 1; }

-int I_RegisterSong(void data) +void I_PlaySong(musicinfo_t m, int loop) { – USED(data); –// printf(“PORTME i_sound.c I_RegisterSong\n”); – return 0; + vlong v; + int n, l; + uchar b[4096]; + char msg[64]; + + if(muspid < 0) + return; + n = W_LumpLength(m->lumpnum); + l = snprint(msg, sizeof(msg), “%s %d %s\n”, m->name, n, loop ? “1” : “”); + if(write(cpfd[1], msg, l) != l || write(mpfd[1], m->data, n) != n) + goto fail; + if(read(cpfd[1], b, 1) < 0) + goto fail; + while(v = filelength(mpfd[1]), v > 0) + if(read(mpfd[1], b, v > sizeof b ? sizeof b : v) < 0) + goto fail; + muson = 1; + if(musvol > 0) + musread = 1; + return; +fail: + fprint(2, “I_PlaySong: fatal error: %r\n”); + I_ShutdownMusic(); }

-void I_PlaySong(int handle, int looping) +void I_StopSong(int) { – USED(handle, looping); –// printf(“PORTME i_sound.c I_PlaySong\n”); + char msg[] = “\n”; + + if(!muson) + return; + muson = 0; + musread = 0; + write(cpfd[1], msg, 1);

}

-void I_StopSong(int handle) –{ – USED(handle); –// printf(“PORTME i_sound.c I_StopSong\n”);

–}

-void I_UnRegisterSong(int handle) –{ – USED(handle); –// printf(“PORTME i_sound.c I_UnregisterSong\n”); –} diff -r 9c4b9c67ff16 sys/src/games/doom/i_sound.h —– a/sys/src/games/doom/i_sound.h Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/doom/i_sound.h Wed Feb 22 02:32:47 2017 +0200 @@ -35,7 +35,6 @@

// … update sound buffer and audio device at runtime… void I_UpdateSound(void); -void I_SubmitSound(void);

// … shut down and relase at program termination. void I_ShutdownSound(void); @@ -90,20 +89,17 @@ // PAUSE game handling. void I_PauseSong(int handle); void I_ResumeSong(int handle); –// Registers a song handle to song data. -int I_RegisterSong(void data); // Called by anything that wishes to start music. // plays a song, and when the song is done, // starts playing it again in an endless loop. // Horrible thing to do, considering. void I_PlaySong –( int handle, +( musicinfo_t m, int looping ); // Stops a song over 3 seconds. void I_StopSong(int handle); // See above (register), then think backwards -void I_UnRegisterSong(int handle);

diff -r 9c4b9c67ff16 sys/src/games/doom/s_sound.c —– a/sys/src/games/doom/s_sound.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/doom/s_sound.c Wed Feb 22 02:32:47 2017 +0200 @@ -548,7 +548,6 @@

    volume);
 }    

+vlong filelength(int);

#endif //——————————————————————————————————————– diff -r 9c4b9c67ff16 sys/src/games/midi.c —– a/sys/src/games/midi.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/midi.c Wed Feb 22 02:32:47 2017 +0200 @@ -16,6 +16,7 @@ int fd, ofd, div, tempo = 500000, ntrack; uvlong T; int freq[128]; +uchar out[8192], *outp = out;

void * emallocz(int size) @@ -110,8 +111,7 @@ void run(uvlong n) { – int samp, j, k, l, no[128]; – uchar *s; + int samp, j, k, no[128];

int t, f;
short u;
Tracker *x;

@@ -127,20 +127,22 @@

        for(k = 0; k &#60; 128; k++)
            no[k] += x-&#62;notes[j][k];
}

void @@ -175,6 +177,9 @@

case 0xC:
    get8(src);
    break;

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=dmus2

!/bin/rc

tmp=/tmp/dmus.$pid wnote=() mp3=()

fn sigexit{

if(! ~ $#wnote 0 &#38;&#38; test -e $wnote)
    echo kill &#62;$wnote
rm -f $tmp $tmp^m

} fn sigint sigterm sigkill{

exit

}

this overrides music in final doom and any custom music in pwads, since the

labels are hardcoded in the source.

fn getmp3{

switch($1){
case adrian
    f=$home/m/doom2.adrians.asleep.mp3
case ampie
    f=$home/m/doom2.bye.bye.american.pie.mp3
case betwee
    f=$home/m/doom2.between.levels.mp3
case bunny
    f=$home/m/doom.bunny.mp3
case countd count2
    f=$home/m/doom2.countdown.to.death.mp3
case dead dead2
    f=$home/m/doom2.the.demons.dead.mp3
case dm2ttl
    f=$home/m/doom2.intro.mp3
case dm2int
    f=$home/m/doom2.intermission.mp3
case doom doom2
    f=$home/m/doom2.doom.mp3
case ddtblu ddtbl2 ddtbl3
    f=$home/m/doom2.the.dave.d.taylor.blues.mp3
case e1m1
    f=$home/m/doom.at.dooms.gate.mp3
case e1m2
    f=$home/m/doom.the.imps.song.mp3
case e1m3
    f=$home/m/doom.dark.halls.mp3
case e1m4
    f=$home/m/doom.kitchen.ace.and.taking.names.mp3
case e1m5
    f=$home/m/doom.suspense.mp3
case e1m6 e3m6
    f=$home/m/doom.on.the.hunt.mp3
case e1m7 e2m5 e3m5
    f=$home/m/doom.demons.on.the.prey.mp3
case e1m8 e3m4
    f=$home/m/doom.sign.of.evil.mp3
case e1m9 e3m9
    f=$home/m/doom.hiding.the.secrets.mp3
case e2m1
    f=$home/m/doom.i.sawed.the.demons.mp3
case e2m2
    f=$home/m/doom.the.demons.from.adrians.pen.mp3
case e2m3 inter
    f=$home/m/doom.intermission.from.doom.mp3
case e2m4
    f=$home/m/doom.theyre.going.to.get.you.mp3
case e2m6
    f=$home/m/doom.sinister.mp3
case e2m7 e3m7
    f=$home/m/doom.waltz.of.the.demons.mp3
case e2m8
    f=$home/m/doom.nobody.told.me.about.id.mp3
case e2m9 e3m1
    f=$home/m/doom.untitled.mp3
case e3m2
    f=$home/m/doom.donna.to.the.rescue.mp3
case e3m3
    f=$home/m/doom.deep.into.the.code.mp3
case e3m8
    f=$home/m/doom.facing.the.spider.mp3
case evil
    f=$home/m/doom2.evil.incarnate.mp3
case in_cit
    f=$home/m/doom2.into.sandys.city.mp3
case intro
    f=$home/m/doom.title.mp3
case messag messg2
    f=$home/m/doom2.message.from.the.archvile.mp3
case openin
    f=$home/m/doom2.opening.to.hell.mp3
case read_m
    f=$home/m/doom2.end.mp3
case romero romer2
    f=$home/m/doom2.waiting.for.romero.to.play.mp3
case runnin runni2
    f=$home/m/doom2.running.from.evil.mp3
case shawn shawn2 shawn3
    f=$home/m/doom2.shawns.got.the.shotgun.mp3
case stalks stlks2 stlks3
    f=$home/m/doom2.the.healer.stalks.mp3
case tense
    f=$home/m/doom2.getting.too.tense.mp3
case the_da theda2 theda3
    f=$home/m/doom2.in.the.dark.mp3
case ultima
    f=$home/m/doom2.the.ultimate.challenge.mp3
case victor
    f=$home/m/doom.victor.mp3
}

} fn docmd{

getmp3 $msg(1)
if(~ $#mp3 0 || ~ $#f 0){
    read -c $msg(2) &#60;/fd/1 &#62;$tmp
    x=`{file $tmp}
    if(~ $x(2) mus){
        mus &#60;$tmp &#62;$tmp^m || exit
        mv $tmp^m $tmp
    }
    c=('a=1;' games/midi -c '&#60;$tmp' '||' 'a=0;' echo -n)
}
if not{
    read -c $msg(2) &#60;/fd/1 &#62;/dev/null
    c=('a=1;' audio/mp3dec '&#60;' $f '||' 'a=0;' echo -n)
}
if(~ $#msg 3)
    c=($c(1) 'while(~ $a 1)' $c(2-))
eval $c &#38;
echo -n &#62;/fd/0
wnote=/proc/^$apid^/notepg

}

while(msg=(`{read})){

if(! ~ $#wnote 0){
    if(test -e $wnote)
        echo kill &#62;$wnote
    wait
    wnote=()
}
if(~ $#msg 2 3)
    docmd

}

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=midi-bend

midi: ignore bend events

bend events are common and midi should not exit when encountering them. instead of actually implementing bends, which would complexify the program a lot, just ignore them.

diff -r 9c4b9c67ff16 sys/src/games/midi.c —– a/sys/src/games/midi.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/midi.c Wed Feb 22 02:06:18 2017 +0200 @@ -175,6 +175,9 @@

case 0xC:
    get8(src);
    break;

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=midi-pipe

midi: add optional use of standard input and output

this allows the use of midi in pipelines.

diff -r 9c4b9c67ff16 sys/src/games/midi.c —– a/sys/src/games/midi.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/midi.c Wed Feb 22 02:11:55 2017 +0200 @@ -187,11 +187,11 @@

        tempo |= get8(src);
        break;
    case 5:

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=midi-write

games/midi: work around erratic write delays causing stammering

the sample loop in run() takes a lot of cpu time, especially when the number of samples is large, and since /dev/audio is written to only afterwards, the resulting delays between writes cause cracking and stammering in playback. this works around this by writing to /dev/audio in even blocks from within the loop.

diff -r 9c4b9c67ff16 sys/src/games/midi.c —– a/sys/src/games/midi.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/midi.c Wed Feb 22 02:00:34 2017 +0200 @@ -16,6 +16,7 @@ int fd, ofd, div, tempo = 500000, ntrack; uvlong T; int freq[128]; +uchar out[8192], *outp = out;

void * emallocz(int size) @@ -110,8 +111,7 @@ void run(uvlong n) { – int samp, j, k, l, no[128]; – uchar *s; + int samp, j, k, no[128];

int t, f;
short u;
Tracker *x;

@@ -127,20 +127,22 @@

        for(k = 0; k &#60; 128; k++)
            no[k] += x-&#62;notes[j][k];
}

void @@ -243,8 +245,10 @@

            minx = x;
        }
    }

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=utf-8 Content-Disposition: attachment; filename=midi-dobend Content-Transfer-Encoding: 8bit

midi: implement pitch wheel

this is an aborted attempt to implement pitch bending (event 0xe0).

this maintains a list of active notes, storing note number, amplitude and bent frequency; this greatly reduces cpu time spent in run(), also alleviating the issue of delays between writes to /dev/audio being too great…

it blows chunks: – greatly complexifies midi note handling – pitch steps are discrete and too noticeable, actually worsening playback (blatant example: doom e2m2), but don’t yet know where T % f >= f/2 is coming from or how to fix this

→ i don’t yet know what i’m doing nor how to solve this correctly

diff -r 9c4b9c67ff16 sys/src/games/midi.c —– a/sys/src/games/midi.c Thu Feb 16 20:11:20 2017 +0100 +++ b/sys/src/games/midi.c Wed Feb 22 01:34:39 2017 +0200 @@ -3,19 +3,29 @@

enum { SAMPLE = 44100 };

+typedef struct Note Note; +typedef struct Tracker Tracker; + +struct Note { + int nt; + Tracker src; + int ch; + uchar a; + int f; + int ½f; + Note n; +} nl[256], np; + struct Tracker {

uchar *data;
char ended;
uvlong t;

-typedef struct Tracker Tracker;

int fd, ofd, div, tempo = 500000, ntrack; uvlong T; -int freq[128];

void * emallocz(int size) @@ -108,32 +118,23 @@ }

void -run(uvlong n) +run(uvlong dt) { – int samp, j, k, l, no[128]; + int samp, l;

uchar *s;

void +bend(Tracker src, int ch, int b) +{ + int f; + double d; + Note n; + + d = (b – 8192) / 4095.5; + d = pow(2, d / 12); + src->bend[ch] = d; + for(n = np.n; n != &np; n = n->n){ + if(n->src != src || n->ch != ch) + continue; + f = SAMPLE / (440 * d * pow(1.05946, n->nt – 69)); + n->f = f; + n->½f = f / 2; + } +} + +void +pop(Tracker src, int ch, int nt) +{ + Note n, p; + + for(p = &np, n = np.n; n != &np; p = n, n = n->n) + if(n->src == src && n->ch == ch && n->nt == nt){ + p->n = n->n; + memset(n, 0, sizeof n); + return; + } +} + +void +push(Tracker src, int ch, int nt, int a) +{ + int f; + Note n, p; + + for(p = &np, n = np.n; n != &np; p = n, n = n->n) + if(n->src == src && n->ch == ch && n->nt == nt){ + n->a = a; + return; + } + for(n = nl; n < nl + nelem(nl); n++) + if(n->src == nil) + break; + if(n == nl + nelem(nl)) + return; + n->src = src; + n->ch = ch; + n->nt = nt; + n->a = a; + f = SAMPLE / (440 * src->bend[ch] * pow(1.05946, nt – 69)); + n->f = f; + n->½f = f / 2; + p->n = n; + n->n = &np; +} + +void readevent(Tracker src) {

uvlong l;
int n,t;

@@ -228,9 +292,10 @@

    size = get32(nil);
    tr[i].data = emallocz(size);
    read(fd, tr[i].data, size);

—lZIgJA9Mle4DvSp2 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=file-midi

recognize MIDI and MUS files. id software’s MUS does not have an associated mimetype.

diff -r f97691e4d2a3 sys/src/cmd/file.c —– a/sys/src/cmd/file.c Fri Dec 25 17:05:05 2015 +0100 +++ b/sys/src/cmd/file.c Thu Dec 31 06:15:50 2015 +0200 @@ -872,6 +872,8 @@

"BEGIN:VCARD\n",    "vCard",        12, "text/directory;profile=vcard",
"AT&#38;T",         "DjVu document",    4,  "image/vnd.djvu",
"Extended module: ",    "XM audio",     17, "audio/xm",

@@ -1659,4 +1661,3 @@

    print("face image depth %d\n", ldepth);
return 1;

}

—lZIgJA9Mle4DvSp2—