1 (edited by hlide 2021-06-27 11:32:16)

Topic: [MZ-700] IN-GAME 1-bit PWM

Hi,

I'm the owner of an MZ-700 and coding some games on it.

First, let me introduce to its sound hardware:

- Loudpseaker 8Ohm/1W
- Notes are usually played through a square signal from 16Hz to 1.108404688MHz using i8253 counter 0 in mode 3.

PWM can be done by alternating mode 0 and mode 4 without setting counter 0.

In fact, we can also use mode 0 with setting count 0 to create a one-shot square signal with a variable duty: intersective PWM.

To be able to output sound in-game, I use an interrupt. Stock MZ-700 mostly uses /INT to toggle AM.PM flag and to readjust time through counters 1 and 2. Since my games are standalone, I usually shut down this interrupt to allow "fast" screen drawing using stack trick (POP/PUSH) because you can access VRAM only when horizontal blanking is on.

So I need another trick to be able use interrupts without disturbing the drawing code. It happens the clock of counter 1 is the same signal which freezes CPU when attempting to access VRAM when BLNK is 0. Interrupt is raised at falling edge of BLNK. So I use HALT instruction to wait for an ISR to toggle the sound output or not then transfer N VRAM blocks through stack then restore a valid stack and do that for 25 lines (in total, we have 2 x 28 x 25 characters/attributes to draw in 100 horizontal blanks - 312 for a frame). I won't dig in the details so let say what is possible:

- for a full 50 FPS, I need an ISR every 4 horizontal blanks to avoid screen corruption. PWM rate is 3902.8314Hz.
- for a full 25 FPS, I need an ISR every 3 horizontal blanks to avoid screen corruption. PWM rate is 5203.7752Hz.

I used IM2 a la Spectrum to set my ISR and here is the ISR:

sfx_isr:
        PUSH    HL
        PUSH    AF

    if DEBUG
        LD      HL,isr_count
        INC     (HL)
    endif

        LD      HL,$E006 ; rearm counter to get the next ISR
        LD      (HL),PIT_CNT2

        LD      A,(sfx_buffer) ; get $20 or $28 to toggle the output sound (PWM 0 or 100%)
sfx_isr_buffer_index equ $-2
        INC     L
        LD      (HL),A

        LD      HL,sfx_isr_buffer_index ; advance in buffer (256-byte ring page)
sfx_isr_on_off:
        NOP ; opcodes $34 (INC [HL]) for ON or $00 (NOP) for OFF

        POP     AF
        POP     HL

        EI
        RET

$20 is mode 0 (final sound output is 0) and $28 is mode 4 (final sound output is 1).

I then have a code to run in the game loop to fill the ring page on the fly from the data generated by pcm2pwm v1.

For 50 FPS, I need 78 bytes in the ring page to play per frame.
For 25 FPS, I need 104 bytes in the ring page to play per frame.

Post's attachments

mz8253.png 28.63 kb, file has never been downloaded. 

You don't have the permssions to download the attachments of this post.

2 (edited by hlide 2021-06-27 12:08:22)

Re: [MZ-700] IN-GAME 1-bit PWM

I am currently experimenting with an approach to create a volume effect. Instead of emitting a high or low signal with 100% duty every ISR, I have introduced (partially for the moment) the notion of 4-bit PCM instead of 1-bit PCM. Well, I realized that the mode 0 allowed me to create a low and then a high signal with a "programmable" duty. Indeed, as soon as the counter reaches the threshold, it switches to the high signal and stays there until the counter is reprogrammed.

So I thought I could have fun creating a 4-bit PCM with duties like this : 0/15, 1/15, 2/15, ..., 14/15, 15/15. Indeed with 1/15, the volume seems much lower.

Now the technique (and why only 4-bit PCM):

Instead of a 256-byte page containing the mode programming bytes of counter 0 (used for the production of its square to 1.108404688 MHz), I determine two contiguous tables. The first one continues to play the same role but indirectly because it contains indexes on the second page which contains code every 16 bytes to produce a square signal with a duty related to the index of the period of an ISR (one every 3.902825 KHz)

Here is the definition of the two pages:

    align 256
sfx_buffer:
        DS        256,$00
sfx_play_pwm_0:
        LD        HL,$E007 ; duty 0% !
        LD        (HL),$28
        JP        sfx_isr_ret
    align 16
sfx_play_pwm_1_15:
        LD        HL,$E007 ; 16-bit counter, duty 6,6...%
        LD        (HL),$30
        LD        L,$04
        LD        (HL),SFX_DUTY_1_15>>0
        LD        (HL),SFX_DUTY_1_15>>8
        JP        sfx_isr_ret
    align 16
...
sfx_play_pwm_14_15:
        LD        HL,$E007 ; 16-bit counter, duty 93,3...%
        LD        (HL),$30
        LD        L,$04
        LD        (HL),SFX_DUTY_14_15>>0
        LD        (HL),SFX_DUTY_14_15>>8
        JP        sfx_isr_ret
    align 16
sfx_play_pwm_15:
        LD        HL,$E007 ; duty 100% !
        LD        (HL),$20
        JP        sfx_isr_ret

I took 4-bit index because I only have 2 bytes left for the 1/15 to 14/15. By switching to 5-bit, I wouldn't be able to fit all the duties in one page of code.

Now I have to adapt the ISR :

sfx_isr:    PUSH    HL
        PUSH    AF
        
        LD        HL,$E006 ; rearm counter to get the next ISR
        LD        (HL),PIT_CNT2
        
        LD        HL,sfx_buffer ; current index stored in the first page
sfx_isr_buffer_index equ $-2
        LD        L,(HL) ; read a byte $i0 where i is the index
        INC        H ; go to the next page
        JP        (HL) ; jump to the code emitting a square signal with the right duty 

sfx_isr_ret:        
        LD        HL,sfx_isr_buffer_index ; next index for the next ISR

sfx_isr_on_off:
        NOP ; either $34 (INC [HL]) when playing is ON or $00 (NOP) if OFF

        POP        AF
        POP        HL

        EI
        RET

The ISR initially had 114 execution cycles. Here we have 115 cycles to which we must add either 30 cycles or 57 cycles depending on the desired duty: 145 or 172 cycles. The game always seems to run at 50 fps.

The reason I wanted to do this is twofold:

- to see if by this set of duty, we could create a volume effect and thus have the possibility to mix several sources.
- to test the possibility to make "intersective" PWM : https://en.wikipedia.org/wiki/Pulse-width_modulation.

Re: [MZ-700] IN-GAME 1-bit PWM

mp3? wav? :-)

Re: [MZ-700] IN-GAME 1-bit PWM

I'm not confident to build a tracker to generate those bytes so for now I used the pcm2pmw v1 tool (https://github.com/JeffAlyanak/pcm2pwm) in the 1-bit PCM case. I need to write a tool to encode a wave file into a 4-bit PCM using intersective PWM encoding.

Obviously WAV or MP3 bitstreams are too big to encode into bytes playable by the game. I don't know if I will use that player for music background or just for game sound effects.

Re: [MZ-700] IN-GAME 1-bit PWM

no. I meant to record what you managed to produce with this method on SHARP :-)

Re: [MZ-700] IN-GAME 1-bit PWM

Oh that! I used an emulator (my stock MZ-700 needs to be repaired). Due to the fact I need to use Audacity to amplify a wave stream extremely and to resample it to 3902.8314Hz to transform into a "1-bit PCM" wav file, It is not satisfying as it is very saturated.

I used that MP3 as source: Learn Soundation - Chip Music - Square Wave.mp3.

I made an horrible resampling of it: Sampler.wav.

And it sounds as such when playing SpaceRallyTest.mp4.

Be sure to lower the volume.

Post's attachments

SpaceRallyTest.7z 1.86 mb, 3 downloads since 2021-06-27 

You don't have the permssions to download the attachments of this post.

Re: [MZ-700] IN-GAME 1-bit PWM

:-) !!!!

Re: [MZ-700] IN-GAME 1-bit PWM

Great to see some in-game 1-bit music action, and also great to see the MZ-700 getting some love! Tbh I think it actually sounds surprisingly decent considering the sample rate.

There are some people on here that know more about 8253 wizardry than me, but a quick couple of thoughts:

- Beware of the Nyquist limit, ie. the highest frequency in the audio may at most be half of the sample rate. So if you replay at ~2Khz, you should apply a low-pass filter with a cutoff of ~1KHz before resampling.
- pcm2pwm isn't very well suited for harmonic content. Its main purpose is to encode percussive sounds in a very size-efficient manner.
- I'm not sure intersective PWM is worth it. In order to produce decent results, it requires a very high sample rate (likely at least 80KHz), which would be pretty hard to pull off on a 3.5MHz Z80 machine.
- It is possible to mix multiple channels without volume control. A method that works well at low sample rates is to have 2-4 pulse wave generators with a duty cycle below ~25%, and OR their outputs together (example).

Anyway, thanks for the detailed write-up, looking forward to hearing more from your 1-bit endeavours!

9 (edited by hlide 2021-06-29 20:44:58)

Re: [MZ-700] IN-GAME 1-bit PWM

Hi @utz !

Thanks for your advices.

First I must explain I'm not a musician (I even wear hearing aids with a disability of almost 90%). So this is not an easy task for me to add sound and that's why I joined here. I recently read a lot of information about PCM, PWM, PDM, and so on. I'm quite aware that having such low rate may not give all the expectations. 

1) "Beware of the Nyquist limit". I wasn't sure when and what kind of filter to pass. So I must apply a *LOW-PASS* filter with a cutoff of ~1902Hz *BEFORE* resampling it to ~3903Hz. Copy that. I recreate the CSND wave file from "CantSlowDown" through Beepola using Savage and apply those transformations to get a 1-bit PCM turned into compressed PWM bytes to "show" you the effect of 4-bit PCM when I set the H signal globally with a fix $10 (~6%), $040, $80, $C0 or $F0 (100%) and $00 for a L signal.

2) yeah, pcm2pwm is truncating crudely if I don't extremely saturate the wave file into 1-bit PCM. And we get a flat volume.

3) You can hear the result of a global "intersective PWM" for each wave file I captured (the ones ending with "<n>over15" (the same duty is applied globally on each file). The less "volume" is, the more we can hear the 3903Hz.

4) Interesting, I wish there is a way to port one engine in such a way it can fill the ring buffer and be adapted to the rate limit.

At worst case, I will use the in-game "engine" to make sound and noise effects through events.

I put several wav files into the archive:

- original musique done through Beepola using Savage
- audicity transformations before pcm2pwm
- different sound captures for different global duty.

Be sure to lower the volume.

Post's attachments

CSND-Beepola-Savage.7z 613.74 kb, 2 downloads since 2021-06-29 

You don't have the permssions to download the attachments of this post.

10

Re: [MZ-700] IN-GAME 1-bit PWM

Ah, now I understand a bit better how you want to generate the different volume levels.

2) You can specify high/low thresholds for pcm2pwm, ie.

$ pcm2pwm sound.wav hi lo

where hi and lo are 8-bit unsigned integers. Default is hi=252, lo=3. Lowering hi and raising lo will make the converter more sensitive.

3) To get rid of the parasite tone (3903Hz), mixing frequency would need to rise above the audible range. So ideally you'd mix at 20KHz or more. On Spectrum we usually mix around 15KHz, and rely on hardware filtering/smoothing to take care of any remaining noise/whining. Beware that emulators introduce additional noise, depending on how they implement sound. MAME's MZ-700 suffers from this, for example.
4). Let me try to construct a basic example. Late night coding attempt, and completely untested, but the idea is to do something like this:

; two-channel OR mixing (aka Squeeker method)
  ld hl,0        ; reset oscillator 2 state
  ld de,div2     ; osc2 clock [freq = sample_rate * div2 / $10000]
  exx
  ld hl,0        ; reset osc1 state
  ld de,div1     ; osc1 clock
  ld bc,ring_buffer   ; must be aligned to 256b page
  
loop
  add hl,de      ; update osc counter channel 1
  ld a,h
  add a,$e0      ; $100 - duty ($e0 = 12.5%)
  ld a,$27
  adc a,0        ; if duty threshold exceeded, A is now $28 
  exx
  add hl,de      ; same for channel 2
  ld c,a
  ld a,h
  add a,$e0
  ld a,0
  adc a,c        ; A is now $27, $28, or $29
  exx
  and #28        ; mask bits, A = $20 | $28
  ld (bc),a
  inc c
  jr z,loop

PS: You should be able to change your nick now. Let me know if it doesn't work, then I'll change it for you.

Re: [MZ-700] IN-GAME 1-bit PWM

Regarding nick change, I had a look on profile and see nowhere how to do so.

I will re-read the rest tomorrow. smile

Re: [MZ-700] IN-GAME 1-bit PWM

So I tried 4-bit PCM:

4-bit PCM

I guess even on a real stock MZ-700, the rate is too low for the frequency carrier to be filtered out without adding a low-pass RC at the loudspeaker.

I also tried the mode 2 which allows to output a high cycle pulse (1,180MHz!) by playing with the counter. I believe it is that you call PFM. But I can only get negative energy because I cannot invert the output to get the positive energy.

PFM

 
For now, let's forget the 4-bit PCM and be back to 1-bit PCM.

Currently here is what I get in "pure" 1-bit PCM->PWM:
1x1-bit PCM
This consists in making a duty at 0% or 100%. So, the maximum frequency is 1.9 KHz.

But I hope to double the resolution by a set of duty: 0%, 50% and 100%.

Indeed, I can use the mode and its counter to recreate a square signal with a 0%, 50% or 100% duty within an ISR.
1x1-bit PCM
I haven't tested it yet because it involves a change in the player or in the data to be read and transformed.