Jump to content
IGNORED

Playing back digitized sound on the tms9919


TheMole

Recommended Posts

I'm trying to play back some pre-calculated wave files in hopes of getting digital sound samples working. I'm using the following playback code (apologies for the wierd assembly syntax, this is running as embedded assembly in a C program):

	// Set up sound chip for sample playback
	__asm__(
		// Load sound chip address in r5
		"	li r5, 0x8400		 \n"
		// Mute channels 1 & 2
		"	li r6, 0x9fbf		 \n"
		"	movb r6, *r5		 \n"
		"	swpb r6			 \n"
		"	movb r6, *r5		 \n"
		// Mute channel 3 & noise channel
		"	li r6, 0xdfff		 \n"
		"	movb r6, *r5		 \n"
		"	swpb r6			 \n"
		"	movb r6, *r5		 \n"
		// Set channel 1 to max frequency (div by 1)
		"	li r6, 0x8100		 \n"
		"	movb r6, *r5		 \n"
		"	swpb r6			 \n"
		"	movb r6, *r5		 \n"
	);

	// Push samples
	__asm__(
		"	li r4, 0x6000		 \n"	// Cartridge space
		"	li r5, 0x8400		 \n"	// Sound chip address
		"	li r6, 0x2000		 \n"	// Play 8192 samples from cartridge bank
		"loop: 			 	 \n"
		"	movb *r4+, *r5		 \n"
		"	dec  r6      		 \n"
		"	jne  loop    		 \n"

 

The sample data lives in cartridge space, starting at 0x6000. The code above is meant to just blast through all 8192 bytes in the bank and push them to the sound chip, which is hopefully setup correctly.

 

The sample data looks as follows, which I believe looks correct (the 9 in the higher nibble indicating that I'm toggling the attenuation of channel 1 with the value of the lower nibble):

98 94 94 94 93 95 99 99 99 99 98 9A 9A 98 96 95 94 94 94 97 9A 9A 9A 9A 99 
96 94 95 99 98 99 97 94 95 95 94 99 9A 99 98 98 9A 9A 9A 99 99 97 94 94 95 
97 99 9B 9B 98 94 94 94 94 98 99 99 98 95 96 98 98 97 98 95 97 9A 9A 9B 9B 
9B 98 95 94 93 94 95 99 99 99 99 98 95 95 99 99 98 97 9A 9B 9A 9A 9A 99 95 
95 94 94 96 99 95 94 94 94 95 9A 9B 9B 9B 98 98 98 95 99 9B 9A 99 98 97 95 
95 98 99 95 94 95 94 95 9A 9A 99 98 99 98 98 99 9B 9B 9A 98 94 93 93 93 95 
9A 9B 9A 97 94 97 96 95 98 9A 9B 9A 9A 99 99 98 94 94 95 95 96 99 9A 99 97 
98 98 98 95 96 98 98 99 9A 9A 99 97 94 94 96 98 9A 9A 98 99 95 93 94 95 95 
9A 9B 9A 9A 99 96 95 94 95 9A 9A 9A 99 98 99 99 99 97 95 94 94 94 94 98 9A 
9A 9A 9A 95 94 94 95 98 9A 99 98 9A 98 95 96 95 96 98 95 95 95 98 99 9A 9B 
9A 99 98 95 94 96 98 99 98 99 98 96 97 99 9A 98 97 95 94 94 98 99 98 9A 9A 
96 99 98 98 98 95 95 98 9A 99 97 95 98 ...

 

However, when running this in js99er (I don't have access to a real console here in Hong Kong) I just hear a few rapid low thumps. I haven't done a precise calculation of the number of cycles between each sample, but I would've figured that if it's off by a little I'd still be able to recognize the audio even if it wasn't playing at exactly the correct speed. The sample has a samplerate of ~11khz.

 

Any thoughts on what I could be doing wrong?

Edited by TheMole
alignment in source code for readability
  • Like 1
Link to comment
Share on other sites

1 hour ago, TheMole said:

However, when running this in js99er (I don't have access to a real console here in Hong Kong) I just hear a few rapid low thumps. I haven't done a precise calculation of the number of cycles between each sample, but I would've figured that if it's off by a little I'd still be able to recognize the audio even if it wasn't playing at exactly the correct speed. The sample has a samplerate of ~11khz.

 

Any thoughts on what I could be doing wrong?

That won't work in JS99er. The audio process is generating 1024 samples at a time from the PSG in it's current state. The only way you will be able to generate digitized sound in JS99er is by toggling the audio gate (cru bit 24) like in Perfect Push. You can try in Classic99 instead where I think it works because of Dragon's Lair 

  • Like 1
Link to comment
Share on other sites

Yep, that should work. I just did something similar again recently.

 

Classic99 does this by simulating a separate DAC channel, any audio channel running more than 22khz or something (I forget the cutoff) switches to DAC mode, and the DAC channel is updated based on the CPU time, with the data transferred to the output buffer once a frame. I actually use the same output buffer for the audio gate and the "dac mode" output. ;)

 

There's a WAV file conversion tool in my video conversion toolchain that will output the bytes you've got there, if you need it. ;)

 

  • Like 1
Link to comment
Share on other sites

This is the actual code I used for 5500hz playback. In this case I left interrupts enabled, but in truth you don't hear the 60hz interrupts. However, it might affect the overall playback rate, since I tuned it based on the overall play time. ;)

 

Spoiler


* this code is at >8330 and handles the sound chip write
    clr *r2
    movb *r5+,@>8400
    clr @>6000
    b *r11

***** some deleted data - but it copies the above to >8330 to read samples from a banked ROM
***** Naturally the extra jump and extra paging of the cart reduces the maximum sample rate ;)
***** WP at >8300

* init sound chip
    li r0,>9FBF
    movb r0,@>8400
    swpb r0
    movb r0,@>8400
    li r0,>CFFF
    movb r0,@>8400
    swpb r0
    movb r0,@>8400
    li r0,>8100
    movb r0,@>8400
    swpb r0
    movb r0,@>8400
	
* set up the music playback
    LI R2,>601c     * start page
    LI R3,>6058     * last page
    LI R4,7042      * bytes on last page
    LI R5,>6000     * current address

LP1
* allow ints to happen
	LIMI 2
	LIMI 0
* RESET SCREEN TIMEOUT (any value ok)
	LI R0,>4000
	MOV R0,@>83D6
	
* play samples - low sample rate gives us some freedom
    bl @>8330
    
* do some delay - hand tuned, within 0.5 seconds of total length (46.3s) in Classic99
    li r0,7
dly
    dec r0
    jne dly
    
* update pointers    
    C r2,r3             * last page?
    jne notlast         * jump if not
    
    dec r4              * count down last bytes
    jne lp1             * jump if not done
    
finish
    limi 2              * just wait and let the user quit
    limi 0
    jmp finish
    
notlast
    ci r5,>8000         * check if done this page
    jne lp1             * jump if not
    
    inct r2             * next page
    li r5,>6000         * reset address
    jmp lp1             * loop around

 


 

  • Like 1
Link to comment
Share on other sites

2 hours ago, Asmusr said:

That won't work in JS99er. The audio process is generating 1024 samples at a time from the PSG in it's current state. The only way you will be able to generate digitized sound in JS99er is by toggling the audio gate (cru bit 24) like in Perfect Push. You can try in Classic99 instead where I think it works because of Dragon's Lair 

Ah, thanks, that would explain some things, indeed :)! Unfortunately, I can't easily run Classic99 on my mac, but I'll give it a go in Mame when I get a chance...

Link to comment
Share on other sites

1 hour ago, Tursi said:

There's a WAV file conversion tool in my video conversion toolchain that will output the bytes you've got there, if you need it. ;)

 

Indeed, I saw that, and I did steal your 8-to-4 bit mapping table to use in my own tool :).

  • Like 1
Link to comment
Share on other sites

2 hours ago, TheMole said:

Ah, thanks, that would explain some things, indeed :)! Unfortunately, I can't easily run Classic99 on my mac, but I'll give it a go in Mame when I get a chance...

Maybe it's improved, but last time I checked MAME can't handle digitized audio on the TI either. 

 

Link to comment
Share on other sites

On 6/11/2023 at 7:03 AM, TheMole said:

Ah, thanks, that would explain some things, indeed :)! Unfortunately, I can't easily run Classic99 on my mac, but I'll give it a go in Mame when I get a chance...

I use C99 on my M2 mac under Wine (you need the 64-bit C99 build).  Works well enough, you do lose some of the fancy graphics features and such that really on windows libraries, but the basics are solid.

 

  • Like 3
Link to comment
Share on other sites

On 6/11/2023 at 1:51 PM, Tursi said:

Classic99 does this by simulating a separate DAC channel, any audio channel running more than 22khz or something (I forget the cutoff) switches to DAC mode, and the DAC channel is updated based on the CPU time, with the data transferred to the output buffer once a frame. I actually use the same output buffer for the audio gate and the "dac mode" output.

I guess I could do the same. How do you detect that the audio channel running more than 22khz? Is it just that if a volume change takes place less than x CPU cycles after the last one you add it to the output buffer? 

Link to comment
Share on other sites

On 6/21/2023 at 10:14 AM, Asmusr said:

I guess I could do the same. How do you detect that the audio channel running more than 22khz? Is it just that if a volume change takes place less than x CPU cycles after the last one you add it to the output buffer? 

Nah, you can't play samples unless you set the channel frequency higher than audible frequencies. Normally, it's set to a count of 1 (roughly 48khz). However, on the Coleco at least we found some software that set a lower frequency. So I just set a somewhat arbitrary cutoff above the human hearing rate. Once the frequency is set that high (anything more than half your output sample rate), you aren't going to be generating audible tones anyway, and if you run the emulator sound at that rate, you'll get unpleasant noise anyway as the output frequency aliases with the emulated frequency.

 

So, if the sound channel is set above that magic frequency, then I start treating it as a DAC instead of a tone generator. Classic99 counts against CPU cycles, and when the volume changes, we fill in the digital output buffer with as many samples as should have elapsed for the previous volume. At the end of a frame, then we finish filling out the buffer for 1/60th of a second, and forward that to the audio system. So the rate of volume change doesn't have any impact on whether it's a DAC channel, it's just used as the value to fill the output buffer. By spacing out the samples against the CPU speed, we get samples in more or less the right spacing. By blocking it out at 1/60th of a second at a time, it stays in sync with the rest of the audio generation.

 

To labor the point with an example. At the start of any frame, I clear the DAC buffer (after transferring it to the audio system.) I remember the number of CPU cycles that we start the frame at. When the volume changes, I check the frequency. If it's above the cutoff, then we are in DAC mode, so I look at how many CPU cycles have executed since the top of frame. That tells me how far into the single-frame DAC buffer I am. I take the old volume, and fill the buffer with samples aligned to that volume (with the number of samples based on the number of CPU cycles and the output frequency). Then I remember where I left off, and remember the new volume level. Hopefully one of those two explanations makes sense. ;)

 

If you try to play samples by modulating the volume with an audible frequency, you actually hear that audible frequency (though the sample may also be recognizable too...). That was how I first figured out the high frequency requirement myself - in my first test I heard the tone, and realized I could set the tone generator to higher than the human hearing limit. ;)

 

  • Like 6
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...