• Please review our updated Terms and Rules here

Reserve a 64k page in MS-DOS

Mills32

Experienced Member
Joined
Sep 25, 2018
Messages
149
Location
Spain
So I continue programming a game engine for dos, and one of the last things I'd like to add, is a sound player (for sound blaster and compatibles).

My goal is to use a 64K block to store tiny samples (sfx, and a few drums for music at 11025 and 8000 Hz), so that the engine can play them very fast.

I got code samples that use "single cycle playback" to reproduce sounds. The problem is, the buffer storing the samples must be inside a ram "page":

Code:
MS-DOS system memory
Page 	Segment:Offset address
0 	0000:0000 - 0000:FFFF
1 	1000:0000 - 1000:FFFF
2 	2000:0000 - 2000:FFFF
3 	3000:0000 - 3000:FFFF
etc ...

If a malloc generates a 64K buffer, it will always pick a location with offset > zero, so the final part of the buffer will be at the next page, and the sound card will play noise, unless you set the DMA to the next page.

You can generate a 32K buffer inside a page with this code (from root42):

Code:
void assign_dma_buffer()
{
  unsigned char* temp_buf;
  long linear_address;
  short page1, page2;

  temp_buf = (char *) malloc(32768);
  linear_address = FP_SEG(temp_buf);
  linear_address = (linear_address << 4)+FP_OFF(temp_buf);
  page1 = linear_address >> 16;
  page2 = (linear_address + 32767) >> 16;
  if( page1 != page2 ) {
    dma_buffer = (char *)malloc(32768);
    free( temp_buf );
  } else {
    dma_buffer = temp_buf;
  }
  linear_address = FP_SEG(dma_buffer);
  linear_address = (linear_address << 4)+FP_OFF(dma_buffer);
  page = linear_address >> 16;
  offset = linear_address & 0xFFFF;
}


I can reduce the sample quality to fit the sound in that 32 K buffer, but it would be awesome to have 64K :cool:.

Alternatives:
-Store every sound at a different location, calculate its page : offset, and set the dma parameters every time a sample has to be played, (as the samples are tiny, it is rare they will hit a page barrier).
-Copy sounds to the 32K buffer, every time I need them (memcpy).

But these two alternatives are slower.


So I thought I could reserve the ram page using assembly, but I just can't understand very well how to use the directives (segment, at, para).

I read turbo assembler docs and I could not find any sample assigning a specific address to a segment. The only thing I found is "page" which aligns the address to 256 bytes.

Code:
sound SEGMENT page ;align to 256 bytes

org 0

_sound_data   label	word
dw      07fffh dup (00000h)

sound ends

I tried to use that array from c, it crashed :oops:.

I also printed the adress to see if something works:

Code:
linear_address = FP_SEG(sound_data);
linear_address = (linear_address << 4)+FP_OFF(sound_data);

printf("DMA RAM Page = %i;\n",linear_address >> 16);
printf("DMA RAM OFFSET = 0x%04X;\n\n",linear_address & 0xFFFF);

Results:
DMA RAM Page = 6;
DMA RAM OFFSET = 0x32D0;

I want to get: page 2 (for example) and offset 0x0000 (always 0).
 
Last edited:
The first thing is not to put the org 0 within the SEGMENT declaration.

I don't think that will fix your problem - but it is not correct and won't help.

I will have another look at the Borland Turbo Assembler manual. Incidentally, which version of Borland TASM are you using?

Dave
 
Your segment declaration looks OK (except for the 'org' statement).

Do you have a "MODEL LARGE" within your assembler source file?

Are you using TLINK? If so, you should be able to get a map file produced by the linker so we can see where your segment and data label is located.

Incidentally, if you assign a segment to a fixed location - you have to make darn sure that nothing else is using that area - otherwise crash city! This is generally not good practice unless you allocate a piece of fixed memory and use a MK_FP to access it.

Dave
 
Thanks.

I have a .model large in the assembly file.
I use Turbo Link 5.0. The map file looks like this

Code:
 Start  Stop   Length Name               Class

 00000H 0283DH 0283EH _TEXT              CODE
 0283EH 0317EH 00941H SBPLAY_TEXT        CODE
 03180H 03180H 00000H SAMPLES_TEXT       CODE
 03180H 03180H 00000H _FARDATA           FAR_DATA
 03180H 03180H 00000H _FARBSS            FAR_BSS
 03180H 03180H 00000H _OVERLAY_          OVRINFO
 03180H 03180H 00000H _1STUB_            STUBSEG
 03180H 0368DH 0050EH _DATA              DATA
 0368EH 0368FH 00002H _CVTSEG            DATA
 03690H 03695H 00006H _SCNSEG            DATA
 03696H 03696H 00000H _CONST             CONST
 03696H 036A7H 00012H _INIT_             INITDATA
 036A8H 036A8H 00000H _INITEND_          INITDATA
 036A8H 036A8H 00000H _EXIT_             EXITDATA
 036A8H 036A8H 00000H _EXITEND_          EXITDATA
 036A8H 0378BH 000E4H _BSS               BSS
 0378CH 0378CH 00000H _BSSEND            BSSEND
 03790H 0380FH 00080H _STACK             STACK
 03900H 138FDH 0FFFEH SOUND              
      

Program entry point at 0000:0000

It looks like it is really allocating a 64K (-1 byte) block for the sound :), and I could read "_sound_data" bytes from c (sound_data[0]).
But the offsets are not aligned as I want, and I still can't use that array to store the sounds because the program crashes.
 
The MAP file looks good.

Let's worry about the crashing presently.

I don't think you are converting the linear address to the DMA RAM page and DMA RAM OFFSET. EDIT: Ah, you are shifting the linear_address right by 16 bits to get your definition of page... The linear address is therefore 632D0. There seems to be some confusion regarding a segment and an offset I think. The offset is a byte offset relative to a segment. You appear to consider an offset should be relative to a 64K page. What is your sound card actually expecting? A linear address?

Can you just printf() out FP_SEG(sound_data) and FP_OFF(sound_data) for me please?

Dave
 
Last edited:
There seems to be some confusion regarding a segment and an offset I think. The offset is a byte offset relative to a segment. You appear to consider an offset should be relative to a 64K page. What is your sound card actually expecting? A linear address?
Dave

I still don't understand very well the memory, there are surely a lot of things wrong :).
I read the sound card needs a buffer inside a "physical 64K block". For my purposes, I think this means the 64K buffer must start at a "linear" address like these: 0x00010000,0x00020000.

The sound playback function needs two parameters to start playing a sound:
page = linear_address >> 16;
offset = linear_address & 0xFFFF;

If all the samples are inside one page, I set the page once, and then I just have to set the offset to play any part of the buffer.

Can you just printf() out FP_SEG(sound_data) and FP_OFF(sound_data) for me please?
Dave

FP_SEG(sound_data) = 0x04A7
FP_OFF(sound_data) = 0x0690
 
Last edited:
>>> I still don't understand very well the memory, there are surely a lot of things wrong.

Very possibly! Just think of the learning opportunities though!

The memory that has been allocated has to be 'compatible' with the requirements of the hardware. The question is whether they meant that the memory has to START on a 64K boundary or has to be wholly resident WITHIN a 64K block.

If the former, then this solution will not work. If the latter - then what we are doing is correct (with the exception that you are aligning the segment to a 256 byte boundary (page) instead of a 16 byte boundary (para)). This is not a problem - it just wastes a bit of memory.

Your FP_SEG and FP_OFF are not what I was expecting. I was expecting FP_OFF to be 0...

Can you post your full C and assembler code somewhere?

Is it possible you still have sound_data defined both within the assembler part and the C part? Clutching at straws - but been there, done that and got the tee shirt!

Dave
 
I have gone back through the previous posts and I am not convinced that the sound card (or its driver) would require the buffer to START on a 64K boundary (a page as you call it).

A segment (a unit of 16-bytes or 1 paragraph) should contain the entire buffer though.

A segment 0 of 0 and an offset of 0 refers to physical location 0.

If you have defined a segment and have a label within it starting at the beginning of the segment, then the reported offset should be 0 (relative to a segment).

If the sound card is using DMA, then the DMA device will require the linear address - which would be the (segment * 16) + offset - where the offset should be 0.

This is my starting point...

EDIT: Did you make _sound_data label PUBLIC in your assembler code incidentally?

Dave
 
Last edited:
I have gone back through the previous posts and I am not convinced that the sound card (or its driver) would require the buffer to START on a 64K boundary (a page as you call it).

A segment (a unit of 16-bytes or 1 paragraph) should contain the entire buffer though.

A segment 0 of 0 and an offset of 0 refers to physical location 0.

If you have defined a segment and have a label within it starting at the beginning of the segment, then the reported offset should be 0 (relative to a segment).

If the sound card is using DMA, then the DMA device will require the linear address - which would be the (segment * 16) + offset - where the offset should be 0.

This is my starting point...

EDIT: Did you make _sound_data label PUBLIC in your assembler code incidentally?

Dave

Thanks. I´m creating a reduced version of the player to post here, I now load a "big" 64K sound, to test when it starts to fail.

I'm declaring "global _sound_data" in assembly so that C code can see it.
 
This is a very common problem for any device using DMA--that is, crossing a "physical" 64K boundary.

The usual procedure is to allocate (INT 21H/ AH=48H) twice as much as you need, located the 64K physical boundary offset and release any excess memory. It's pretty simple. Because this is a memory-hungry procedure, it's good to do this before you allocate any other memory. Needless to say, you need to keep two memory addresses around--the address of the block that DOS returns (so you can release it) and the simplified segment/offset=0 address that you'll be using. You can also compute the DMA address, which, if a 16-bit DMA, is a word+page address, or for 8-bit byte address+page.

If you're running on a PS/2 using Microchannel DMA, things may be quite a bit different.
 
Last edited:
This is a very common problem for any device using DMA--that is, crossing a "physical" 64K boundary.

The usual procedure is to allocate (INT 21H/ AH=48H) twice as much as you need, located the 64K physical boundary offset and release any excess memory. It's pretty simple. Because this is a memory-hungry procedure, it's good to do this before you allocate any other memory.

That's why I tried to define a location in assembly. Anyway I don't know how to do what you say.

For the moment this is a reduced player that works on dosbox (it assumes base 220; irq 7; dma 1; ). It allocates a 64K block in C (another in assembly to test), copies a sample to the block defined in C, and plays it. It reaches a 64K boundary and plays noise at the end.

View attachment SBplay.zip
 
You don't need to use assembly.

When you say "64K Boundary", do you mean a physical or logical boundary? For example, a segment:eek:ffset of 0x4000:0 is a physical and logical 64K boundary, but 0x4001:0 is not--only a logical one. If you're dealing with physical boundaries, it helps to convert the segment/offset to a linear 20-bit address; that is, 0x1234:0x5678 is equivalent to 0x179b8.

Note that DOS Interrupt 0x21, 0x48 returns a lone segment address; that is, an address where the offset is assumed to be 0. So figuring out where the 64K boundary is should be pretty simple--the lower 12 bits of the segment address will be 000.
 
Looking at the IBM PC AT hardware - the 8237 DMA device only contains 16 bit registers for specifying the address and count.

The DMA registers are extended further by an additional hardware latch - thus supplying the upper address lines.

This means the the 8273 buffer has to fully reside within a 64K physical segment (an address of the form 0xn0000). The paragraph and page alignment, therefore, will not work.

As Chuck has identified, you will either have to pre-allocate an entire 64K physical page or allocate 128K bytes of memory and then work out where the 64K physical page starts and use that as the start of your physical buffer - safe in the knowledge that you have available 64K of contiguous space. This is a little wasteful though...

You could also write a small TSR containing two (2) contiguous 64K segments of unused data, calculate the ideal start of the 64K physical boundary and return to DOS - but storing this segment address in a convenient place for your program to locate.

This will (of course) eat up 128K of memory before you start...

Depending upon what processor you are using - and whether you have any extended memory installed - you could use a little trick to access 4MB of memory within REAL address mode. I used this trick when I wrote a PDP-11 emulator in assembler to run under DOS. I had full access to 4MB of memory (for the PDP-11 memory space) but all from within a real mode DOS program. I am not sure how the DMA device (or more correctly, the DMA interface software library) would cope with this though!

Dave
 
Last edited:
I'm targeting 8088-86/286 with 640k or even less, and this is a bit more complicated than I expected.

I think I can live with my first code, with a 64k buffer in which i can be sure 32k are inside a 64 physical block. After all, I will only use a few tiny samples that fit in 32K. That 64k buffer will also be used for temporary graphics storage, so there is no wasted ram :).

Also I didn't explore the posibility of 4 bit pcm, maybe it does not sound too bad, and that would increase a lot the amount of samples I can store.

Thanks a lot.
 
Last edited:
The ‘trick’ will definitely NOT work on an 8088/8086 and not with <= 640K RAM.

Sounds like sticking with a 32K buffer within a 64K memory block is the way to go. You can always modify it later if you wish...

Just think of how much you have learnt in the process though!

Good luck.

Dave
 
I think the OP is really overthinking this. Chuck's solution is the safest: Allocate 128K, find the 64K DMA page boundary within that, and use that for 64K of samples. Yes, some RAM is wasted; you can either figure out what else to store there, or ignore it.

Another solution is what most sound drivers do: They allocate a small buffer inside a DMA page boundary, like 2K. Then to play a sound, they copy the first 2K into that buffer, play it, and then use the sound card's interrupt to notify when the sound is finished playing, at which point another 2K is copied and played, etc. and this repeats until the sound is finished playing. You can have more than 64K sample data this way. This has more info and examples: ftp://ftp.oldskool.org/pub/drivers/Creative/Sound Blaster Series Hardware Programming Guide/

A third solution is to just go with an audio library. http://www.thegleam.com/ke5fx/misc/AIL2.ZIP is still available and comes with an API that lets you play sound with DIGPAK drivers (included) which support Sound Blaster and many other devices, and there is no limitation on sound length. And you can play music too (its primary function).
 
I finally decided to just use 32K, and try adpcm compressed format which is supported by all sound blaster cards.
 
Depending on the allocation scheme, you also have to be careful.

Historically, many C based systems locate bookeeping information for the block, BEFORE the block.

So, if you malloced 100 bytes, the runtime may well allocate, say, 108 at address X, and then return address X+8, using the 8 byte for links, and status and what not.

If you're trying to allocate an entire 64K block, you need to make sure that the memory runtime doesn't have any issues with being at the 64K boundary. It's straightforward to see a naive runtime, or one using the wrong assumptions, to potentially have problems with a 64K boundary, especially if it tries to allocated 65536 + 8 bytes (or whatever).

So, just something to consider.
 
I finally decided to just use 32K, and try adpcm compressed format which is supported by all sound blaster cards.

Careful, it's not IMA ADPCM or MS ADPCM, but a custom ADPCM that you can only create (as far as I know) by compressing your samples using VEDIT or VEDIT2, which is software that comes with Sound Blaster or Sound Blaster Pro cards (available online). You convert your 8-bit audio into a .VOC file, load it into VEDIT or VEDIT2, compress it, save it back out, and then extract the compressed block out of the resulting .VOC file. You must also use different playback commands when sending data to the DSP.

It also sounds worse than IMA ADPCM.
It also limits you to Sound Blaster cards, as not all SB compatible cards can handle the compressed format.
It also limits you to 12KHz or lower sample rates, as the decompression playback can't go any faster than that.
 
Last edited:
Careful, it's not IMA ADPCM or MS ADPCM, but a custom ADPCM that you can only create (as far as I know) by compressing your samples using VEDIT or VEDIT2, which is software that comes with Sound Blaster or Sound Blaster Pro cards (available online). You convert your 8-bit audio into a .VOC file, load it into VEDIT or VEDIT2, compress it, save it back out, and then extract the compressed block out of the resulting .VOC file. You must also use different playback commands when sending data to the DSP.

It also sounds worse than IMA ADPCM.
It also limits you to Sound Blaster cards, as not all SB compatible cards can handle the compressed format.
It also limits you to 12KHz or lower sample rates, as the decompression playback can't go any faster than that.

Thanks a lot for the info!.
 
Back
Top