From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on inbox.vuxu.org X-Spam-Level: X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=ham autolearn_force=no version=3.4.4 Received: (qmail 29024 invoked from network); 4 May 2021 13:09:12 -0000 Received: from 1ess.inri.net (216.126.196.35) by inbox.vuxu.org with ESMTPUTF8; 4 May 2021 13:09:12 -0000 Received: from wopr.sciops.net ([216.126.196.60]) by 1ess; Tue May 4 09:04:03 -0400 2021 Received: (qmail 33363 invoked from network); 4 May 2021 06:03:54 -0700 Received: from 100.43.142.88.rev.sfr.net (HELO aib.nope) (qwx@88.142.43.100) by wopr.sciops.net with SMTP; 4 May 2021 06:03:54 -0700 Message-ID: From: qwx Date: Tue, 04 May 2021 15:03:51 +0200 To: 9front@9front.org MIME-Version: 1.0 Content-Type: text/plain; charset="US-ASCII" Content-Transfer-Encoding: 7bit List-ID: <9front.9front.org> List-Help: X-Glyph: ➈ X-Bullshit: rails dependency solution Subject: [9front] games/opl3: use correct sampling rate Reply-To: 9front@9front.org Precedence: bulk Hello, umbraticus recently fixed an issue with realtime applications where opl3 would buffer audio instead of writing it immediately. We simultaneously tried to simplify the sampling rate stuff but broke it in the process. dmid uses the same sample rate as the chip for music, but other applications do not. opl3 and its older version opl2 (not in 9front) read an input stream of commands in basically IMF format, something used in other id Software games and some others, which assumes a given input sampling rate: 700 Hz for Wolfenstein 3D music, 560 Hz for Commander Keen, 60 Hz for Ultima 6, etc. The opl3 emulation on the other hand is not really intended to run at a sampling rate different that the chip's 49.716 kHz sampling rate. Previously, we assumed it runs at 44.1 kHz and just used the input rate as a divisor to get the number of samples per delay tic. From what I understand, the correct way to use it for accurate emulation is to run the opl chip emulator at its intended sampling frequency, then downsample to 44.1 kHz. This means better output but more code. I opted to just fork pcmconv to deal with the resampling. The alternative is to basically do the same as before rev 8433, except with no buffering, but at accuracy/quality loss. imho the change is worth it if only just for the sake of doom :) The patch below implements the idea, and also adds some missing error checks. This has been tested with doom, and with the IMF files from the above games via opl2 [1]. Testing the opl2 shit requires extracting the IMFs from the data and stripping their headers (the latter done with imf.c from [2]). I can provide extracted IMFs if anyone wants them. Otherwise, you can also try playing Ultima 6's M files directly with [3]. Any thoughts/objections? Thanks, qwx [1] http://shithub.us/qwx/opl2/HEAD/info.html [2] http://shithub.us/qwx/weu/HEAD/info.html [3] http://shithub.us/qwx/u6m/HEAD/info.html diff -r f98f914bcb45 sys/man/1/opl3 --- a/sys/man/1/opl3 Sat May 01 15:33:31 2021 -0400 +++ b/sys/man/1/opl3 Tue May 04 14:29:02 2021 +0200 @@ -17,7 +17,11 @@ The emulated chip is programmed by a stream of commands either from .I file or from standard in. -It then synthesizes a number of stereo 16 bit little-endian samples for a sampling rate of 44.1 kHz, +It then synthesizes stereo 16-bit little-endian PCM samples +at the chip's sampling rate, 49.716 kHz, +resamples them to +.IR audio (3)'s +default 44.1 kHz rate, and writes them to standard out. .PP Commands are 5 bytes wide, in little-endian byte order: @@ -42,14 +46,24 @@ It is a multiple of a command period, during which the .SM OPL3 chip may be sampled before processing the next command. -The period itself is the inverse of the sampling rate, 44100 Hz by default. -This rate can be set using the +The period itself is the inverse of the input stream's sampling rate, +by default the same as the chip's output sampling rate. +The .B -r -parameter. +parameter +sets the input sampling rate. .SH SOURCE .B /sys/src/games/opl3 .SH "SEE ALSO" .IR audio (3) +.PP +Yamaha +``YMF262 Manual'', +1994. +.PP +V. Arnost +``Programmer's Guide to Yamaha YMF 262/OPL3 FM Music Synthesizer'', +version 1.12 dated Nov. 23rd 2000. .SH HISTORY .I Opl3 first appeared in 9front (July, 2018), based on diff -r f98f914bcb45 sys/src/games/dmid.c --- a/sys/src/games/dmid.c Sat May 01 15:33:31 2021 -0400 +++ b/sys/src/games/dmid.c Tue May 04 14:29:02 2021 +0200 @@ -8,7 +8,7 @@ typedef struct Chan Chan; typedef struct Trk Trk; enum{ - Rate = 44100, + Rate = 49716, /* opl3 sampling rate */ Ninst = 128 + 81-35+1, Rwse = 0x01, @@ -236,7 +236,7 @@ e = freq[n] + (d % 0x1000) * (freq[n+1] - freq[n]) / 0x1000; if(o->c->i->fixed) e = (double)(int)e; - f = (e * (1 << 20)) / 49716; + f = (e * (1 << 20)) / Rate; for(b=1; b<8; b++, f>>=1) if(f < 1024) break; diff -r f98f914bcb45 sys/src/games/opl3/opl3m.c --- a/sys/src/games/opl3/opl3m.c Sat May 01 15:33:31 2021 -0400 +++ b/sys/src/games/opl3/opl3m.c Tue May 04 14:29:02 2021 +0200 @@ -6,6 +6,10 @@ void opl3wr(int, int); void opl3init(int); +enum{ + OPLrate = 49716, /* 14318180Hz master clock / 288 */ +}; + void usage(void) { @@ -16,15 +20,18 @@ void main(int argc, char **argv) { - int rate, r, v, dt, fd; + int rate, n, r, v, fd, pfd[2]; uchar sb[65536 * 4], u[5]; + double f, dt; Biobuf *bi; fd = 0; - rate = 44100; + rate = OPLrate; ARGBEGIN{ case 'r': rate = atoi(EARGF(usage())); + if(rate <= 0 || rate > OPLrate) + usage(); break; default: usage(); @@ -35,14 +42,41 @@ bi = Bfdopen(fd, OREAD); if(bi == nil) sysfatal("Bfdopen: %r"); - opl3init(rate); - while(Bread(bi, u, sizeof u) > 0){ + opl3init(OPLrate); + if(pipe(pfd) < 0) + sysfatal("pipe: %r"); + switch(rfork(RFPROC|RFFDG)){ + case -1: + sysfatal("rfork: %r"); + case 0: + close(0); + close(pfd[1]); + dup(pfd[0], 0); + execl("/bin/audio/pcmconv", "pcmconv", "-i", "s16c2r49716", "-o", "s16c2r44100", nil); + sysfatal("execl: %r"); + default: + close(1); + close(pfd[0]); + } + f = (double)OPLrate / rate; + dt = 0; + while((n = Bread(bi, u, sizeof u)) > 0){ r = u[1] << 8 | u[0]; v = u[2]; opl3wr(r, v); - if(dt = (u[4] << 8 | u[3]) * 4){ /* 16-bit stereo */ - opl3out(sb, dt); - write(1, sb, dt); + dt += (u[4] << 8 | u[3]) * f; + while((n = dt) > 0){ + if(n > sizeof sb / 4) + n = sizeof sb / 4; + dt -= n; + n *= 4; + opl3out(sb, n); + write(pfd[1], sb, n); } } + if(n < 0) + sysfatal("read: %r"); + close(pfd[1]); + waitpid(); + exits(nil); }