VOGONS


First post, by n0p

User metadata
Rank Member
Rank
Member

Hi.
Have someone thought about why mushroom.com (Magic Mushroom Ad) PC speaker PWM sound is so good? Almost no noise. It's wave file (mushroom.ovl) is reduced to 6-bit (40h max value) in the file and has about 11Khz, but it's output differs (in good way) from anything i've heard. via PC speaker. It has some black magic inside, installs on int 08 (absolutely positive that's fake offset or self modifying code) and 09 (terminate), halts all other processing (hlt), but it uses port 41h together with 42h and 61h to produce digital sound and 08 int is set to some strange offset.
Would love to get some help from a reverser. Attached the one i have.

Attachments

  • Filename
    mushroom.zip
    File size
    176.44 KiB
    Downloads
    50 downloads
    File comment
    Magic Mushroom Ad
    File license
    Public domain
Last edited by n0p on 2023-08-25, 19:16. Edited 1 time in total.

Reply 1 of 15, by bregolin

User metadata
Rank Member
Rank
Member

Quite interesting, but it seems to me that this has more to do with the wave file instead of the playback code. The wave file is 10khz µ-law with little-endian byte ordering, which seems to sound better when played back using PWM than regular PCM

IBM Aptiva 2162 - P55 166 MMX, 32MB, CS4237B + Wavetable, ATI Mach64 2MB / Win98SE
Custom PIII 750, 64MB, SB AWE64, Voodoo 3 3000 AGP / Win98SE
Sony Vaio z505 SuperSlim - PIII 550, 192MB, YMF744, NeoMagic 256AV+ / Win98SE

Reply 2 of 15, by bregolin

User metadata
Rank Member
Rank
Member

Actually, looking at the waveform, it looks like it has been processed so that the waveform only has negative amplitude values, I think this might be the trick

IBM Aptiva 2162 - P55 166 MMX, 32MB, CS4237B + Wavetable, ATI Mach64 2MB / Win98SE
Custom PIII 750, 64MB, SB AWE64, Voodoo 3 3000 AGP / Win98SE
Sony Vaio z505 SuperSlim - PIII 550, 192MB, YMF744, NeoMagic 256AV+ / Win98SE

Reply 3 of 15, by n0p

User metadata
Rank Member
Rank
Member
bregolin wrote on 2023-08-25, 18:31:

Actually, looking at the waveform, it looks like it has been processed so that the waveform only has negative amplitude values, I think this might be the trick

I would agree, but i just made a simple PWM .com with part of .ovl data and noise is there 😀 original one sound much better.
Code is usual: throw bytes to 42h port (maybe that's me not getting someting?):

push cs
pop ds
cld

mov si, offset wave
mov cx, offset start - offset wave

loop1:

lodsb
out 42h, al
push cx
mov cx,28
aaa1: loop aaa1
pop cx

loop loop1

i'me testing on DOSBox ECE with 300 cycles and Book8088 just in case. DOSBox PWM sounds a bit better, but pretty close to real one overall

Attachments

Reply 4 of 15, by mkarcher

User metadata
Rank l33t
Rank
l33t

The mushroom demo has alerady been reverse engineered and used as basis for a linux PC speaker sound driver in 1995. See https://www.ibiblio.org/pub/Linux/kernel/patc … pcsndrv-1.0.tgz , especially the file pcsnd-kit/tech-notes.

Reply 5 of 15, by n0p

User metadata
Rank Member
Rank
Member
mkarcher wrote on 2023-08-25, 19:46:

The mushroom demo has alerady been reverse engineered and used as basis for a linux PC speaker sound driver in 1995. See https://www.ibiblio.org/pub/Linux/kernel/patc … pcsndrv-1.0.tgz , especially the file pcsnd-kit/tech-notes.

Thanks! TBH, i've never seen this kind of PWM and would love to see a logic/graph of the approach. But pascal code makes that easy to implement.

Reply 6 of 15, by n0p

User metadata
Rank Member
Rank
Member

Well, i should have known that won't be that easy 😀
I have implemented both routines (except 8088 is too slow for fully implement first one, so values are divided by 2)
MUSHR.COM - implemented full logic of second tech example.
Same noise as if i could simply use that 42h port.
MUSHR2.COM - just to check how first code from techdoc would work on 8088.
Sources attached.
--
So the question stays. MUSHROOM.COM definitely does something (and looks like interpolation is not the real answer) to sound good on XT with PC speaker.

Attachments

Reply 7 of 15, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

Regardless of which CPU speed I run it at, each time I launch mushroom.com I just hear a brief crackle through my speakers and nothing else. How are you supposed to use this program?

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 8 of 15, by n0p

User metadata
Rank Member
Rank
Member

Sound like you configuration is too fast for it and it registers key press right on start when you still holding Enter. I'm using DOSBox ECE with cycles set to 300 - works perfectly.
As it waits for key on INT 09h - make sure you press enter really quick, or it'll quit.
Or - i've attached patched mushroom.com with removed int 09 handler installation, so it'll play till end regardless of key presses.

Attachments

Reply 9 of 15, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

The patched version works in DOSBox-X.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 10 of 15, by n0p

User metadata
Rank Member
Rank
Member

Extracted all needed code, made two working examples (uber_v1.com and uber_v2.com).
uber_v1 - exact code as in original. 9Khz input, 18Khz output
uber_v2 - interpolation removed, 18Khz both.
Sound identical to me, so interpolation code is not critical.
Jingle included right in .com file to simplify testing.
Source here
https://github.com/jinshin/UberPWM/
My comments and labels are there, it uses all three timer channels to generate sound. But i so far cannot get that together the logic.
1. Channel 0 set to call interrupt 18356 times/sec. Ok
2. Channel 1 is set to interrupt on count, 555 times/sec. How it differs from square-wave mode?
3. Channel 2 is set to one shot and can actually be never written again, but it certainly takes effect on generation - code in start of INT 08, connecting and disconnecting channel 2 from speaker
What happens when port 41h recieves AX as input?
Why it's MSB set to 20h and it looks like it needs this exact value (91 times per second?)

Anyway, working code is there on github, if anyone want to help - i'd be grateful.

Best!

Reply 11 of 15, by mkarcher

User metadata
Rank l33t
Rank
l33t
n0p wrote on 2023-08-27, 14:00:
1. Channel 0 set to call interrupt 18356 times/sec. Ok 2. Channel 1 is set to interrupt on count, 555 times/sec. How it differs […]
Show full quote

1. Channel 0 set to call interrupt 18356 times/sec. Ok
2. Channel 1 is set to interrupt on count, 555 times/sec. How it differs from square-wave mode?
3. Channel 2 is set to one shot and can actually be never written again, but it certainly takes effect on generation - code in start of INT 08, connecting and disconnecting channel 2 from speaker
What happens when port 41h recieves AX as input?
Why it's MSB set to 20h and it looks like it needs this exact value (91 times per second?)

Channel 1 is for memory refresh, and it is supposed to be set to 12h (18), so generate a refresh pulse every 15.6µs. If you set it to higher values, you risk corrupting RAM, but you gain performance. Setting it to 20h is likely acceptable unless it is very warm in your room, as the refresh rate of 12h is sufficient at the maximum allowed chip temperature of the RAM chips (70°C typically), and slower refresh is OK at lower temperatures. Setting the refresh rate lower increases performance, and might be required to make the code work at 4.77MHz. If the IRQ0 rate would be 40h, not 41h, I also would guess the idea is that the refresh timing is in a fixed phase relation to the timer, reducing the jitter.

The difference between square-wave and "interrupt on count" is that interrupt-on-count generates an output pulse of a single timer clock (838ns), but "square wave" generates a 50% duty cycle wave. This has a direct consequence for software reading the current count. In interrupt-on-count, the timer chip just counts down by one each clock, as one would expect. In square-wave mode, the timer counts down by two each clock, and toggles the output every time it expires.

Reply 12 of 15, by n0p

User metadata
Rank Member
Rank
Member

Thanks!
So far removed code for channel 1 completely, figured out that "out 41h,ax" makes "out 41h,al" and "out 42h,ah" in one shot.
I didn't know you could do that - and that led me to thinking that channel 1 was used in sound generation.
And 20h counter value for port 41h also used as EOI signal.
Very optimized code indeed.
Now reversed code looks quite simple to understand what actually happens.

Reply 13 of 15, by n0p

User metadata
Rank Member
Rank
Member
mkarcher wrote on 2023-08-27, 19:16:

The difference between square-wave and "interrupt on count" is that interrupt-on-count generates an output pulse of a single timer clock (838ns), but "square wave" generates a 50% duty cycle wave. This has a direct consequence for software reading the current count. In interrupt-on-count, the timer chip just counts down by one each clock, as one would expect. In square-wave mode, the timer counts down by two each clock, and toggles the output every time it expires.

I need a hand.

in	al,61h
or al,2
out 61h,al
;speaker state is high now

mov al,92h
out 43h,al


interrupt_start:

;load counter for channel 2
mov al,somevalue
out 42h,al

;connect channel 2
;as speaker enabled, it will start countdown on channel 2
;for first clk output will be high, but low until count if finished
;and set speaker to low
in al,61h
or al,1
out 61h,al
;speaker is most probably low here, unless a very small value is loaded into counter

;disconnect channel 2
in al,61h
and al,0FEh
out 61h,al

;and it will stay low

jmp interrupt_start

I definitely missing something on hardware level interaction.
Value for one-shot timer for channel 2 is loaded into LSB
Then speaker is connected to channel 2 (Bit 0 set to 1). This triggers one-shot countdown. Wiki says output will be high for one clk, then low until counter finishes.
But right next command disconnects speaker from channel 2 (Bit 0 set to 0), leaving it at low until next trigger.
Doesn't make sense to me, as it should generate small number of very shot high pulses on speaker.
What am i missing?

Reply 14 of 15, by mkarcher

User metadata
Rank l33t
Rank
l33t
n0p wrote on 2023-08-28, 19:12:

But right next command disconnects speaker from channel 2 (Bit 0 set to 0), leaving it at low until next trigger.
Doesn't make sense to me, as it should generate small number of very shot high pulses on speaker.
What am i missing?

It seems you don't correctly understand how the two low-order bits of port 61 are interfacing with the speaker.

Bit 0 is directly connected to the "channel 2 gate" input of the timer chip. The function of the "gate" input depends on the counter mode. In the mushroom demo, channel 2 operates in hardware-retriggerable one-shot mode. This is one of the two modes in which the "gate" input does not gate the counting, but instead the rising edge of that input starts the counting. So the duration of the low pulse at the output of timer 2 is not related to the duration the gate pin is high. Most other PC speaker drivers use the "interrupt on terminal count" mode, in which the pulse (output low) begins immediately after writing a byte to port 42h, which seems more efficient, because you can avoid the 8-bit I/O instructions to port 61h.

Bit 1 of port 61h is ANDed with the "channel 2 output" pin (look up the polarity (A ND vs. NAND) if required) in a way that the transistor that drives the speaker never gets turned on while bit 1 is low, but the transistor follows the output if bit 1 is high. You can put the system into a state in which the speaker transistor is permanently turned on (some games, especially those that use PWMing, do this, likely by accident). If the transistor is turned on permanently, the speaker reproduces the noise on the +5V line. So if you happen to hear quiet low-volume buzzing noise from the speaker after running an application that uses the speaker, this is likely what happened.

It seems strange that the mushroom demo uses mode 1 ("mushroom") instead of mode 0 ("standard") which requires the gate pin to be pulsed. On the other hand, around 25 years ago, i tried modifying the Linux PC speaker kernel driver to use the later "standard" algorithm, and found out that the "standard" algorithm seems to be more sensitive to jitter than the "mushroom" one: The Linux PC speaker driver (the one I linked some posts before) does not disable other interrupts during playback (after all, Linux is a multi-tasking system!), and after modifying the driver to use the standard algorithm and omit the port 61h stuff, I could hear a quite loud buzzing noise whenever I moved the mouse (which invokes interrupts on the serial port at a constant frequency), but in the initial way the driver worked, the quality still was degraded, but no obvious buzzing happened during mouse moves. Up to today, I do not understand the difference between using mode 0 and using mode 1, if you always pulse the gate pin directly directly after loading a new value.

In the PC architecture, timer channel 2 is the most versatile one, because that is the only channel that has a programmable gate pin. Channels 0 and 1 have the gate pin tied high. Also, as long as you don't set bit 1 of port 61h, the output of timer 2 has no observable function, so you can use the timer channel 2 for software-internal purposes as you like without disturbing PC operation. I like to use timer 2 for micro-benchmarking: I set it up in a down-counting mode (typically mode 2, "rate generator"), and then I can just set bit 0 of port 61h during execution of the code fragments that need to be timed, so the "timing enable" operation is minimal and can be kept separate from the timer set-up and timer read-back code. If you use mode 2, every rising edge of the gate will reset the count, so will will read back only the duration of the latest code fragment that sets port 61h, bit 0. On the other hand, modes 0 and mode 4 don't do so, so you can accumulate the duration of multiple timed code blocks.

Reply 15 of 15, by n0p

User metadata
Rank Member
Rank
Member

That's what i was missing, thank you!
To sum up:
GATE2 and OUT2 on 8253 are different pins and GATE2 doesn't control OUT2 in this mode, but counter state does control OUT2.
Speaker is controlled by AND state of OUT2 and bit 0 of port 61h.
So connecting and disconnecting GATE2 simply starts the countdown, which generates two states on OUT2, LOW and then HIGH to pulse the speaker. __|‾‾‾
So original tech-doc is absolutely right, i just was too dumb to understand that it does correspond to actual code in Mushroom at that point.
Thanks again!
--
To add. Using mode 0 provides same result, with only small difference - a little louder pop on speaker disable. That even not a problem.
Handler for int 08 is reduced to

int08_handler:
lodsb
dec bx
jz short terminate

out 42h,al
mov al,ch
;Signal EOI, ch is 20h
out 20h,al
;avoid stack overflow, add 6 to SP
add sp,di
sti
hlt

and that's great. I believe without interpolation code and unneeded port outs this would run on XT without dram refresh modification.

Attachments

  • speaker.png
    Filename
    speaker.png
    File size
    9 KiB
    Views
    700 views
    File comment
    Speaker diagram
    File license
    Public domain