Playing .wav files on DOSBox's Sound Blaster device

前端 未结 2 459
一向
一向 2020-12-18 14:07

I want to make a program in assembly/8086/masm/dosbox that turns the keyboard into various musical instruments so i need to be able to play some .wav files to produce the re

相关标签:
2条回答
  • 2020-12-18 14:34

    This present a demo program that plays a specific WAV file (to avoid introducing a RIFF parser to the already too-long-for-SO code.
    The program has been tested in DOSBox, but a lot of things can go wrong on different configurations.

    Finally, I was forced to split the code into two answers.
    This is part 1.

    Though the question may classify as off-topic1 I believe it could be a precious resource to have on this site.
    So I'm attempting to respond it.

    A few notes on the environment:

    • I've used TASM as the assembler, there is no particular reason behind this choice but childhood memories.
      The code should be compatible with MASM.

    • I'm using DOSBox to emulate a DOS environment.
      DOSBox ships with a preconfigured SoundBlaster 16 card.
      TASM can be run under DOSBox without any problem.

    A scanned version of the TASM 5 manual2 is available online.
    Though no uncommon syntax has been used, being unfamiliar with the assembler directives makes any code harder to read and understand.
    The TASM 5 pack is available online.

    Assembling, general source format and debugging

    As a matter of convenience, the code developed for this answer can be found on GitHub.
    The binary format is the MZ executable with memory model SMALL, one data segment named _DATI3 and one code segment named _CODE.
    Each segment is defined multiple times for convenience4, both segments are PUBLIC so all these different definitions are merged together by the linker, resulting in just two segments5.

    The sources target the 8086 as per OP request.

    The sources use conditional macro and symbolic values6 in order to be configurable, only three values need to be adjusted eventually.
    The default values match the default configuration of DOSBox.
    We will see the configuration soon.

    Due to the not elementary nature of this task, debugging is essential.
    To facilitate it, TASM and TLINK can be instructed to generate, and include, debugging symbols.
    Coupled with the use of TD debugging is greatly simplified.

    Assemble the sources with

    tasm /zi sb16.asm
    tlink /v sb16.obj 
    

    to generates full debugging symbols.
    Use td sb16 to debug the program.
    Some notes on debugging:

    • Sometimes DOSBox crashes.
    • During debugging the DOS environment can be corrupted if the program acts incorrectly or is terminated earlier. Be ready to restart DOSBox often.
    • Place an int 03h (opcode CC) instruction where you want TD to break. This is handy to debug the ISR.

    Soundcard configuration

    The SoundBlaster 16 (SB16) had a simple DSP that when filled with digital samples converted them into an analogue output.
    To read the samples the card took advantage of a special transfer mode called Direct Memory Access (DMA), the chip that handled such transfers was capable of handling 4x2 in flight data movements.
    The SB16 had a jumper, or a switch, to configure the channel to use to read the samplings.

    When a block of sampling was over the card requested the attention of the CPU through an interrupt, the chip handling the interrupts had 8x2 request lines.
    The SB16 had another jumper to select the Interrupt ReQuest line (IRQ) to use.

    Finally, as every legacy device, the SB16 was mapped in the IO address space where it occupied sixteen continuous bytes.
    The starting address, a.k.a. base address, of this block was configurable too. A part was fixed and a part was variable, the base address had a form of 2x0h where x was configurable.

    All these options are reflected in the DOSBox configuration file.
    The program given has been tested with these options7:

    [sblaster]
    sbtype=sb16
    sbbase=220
    irq=7
    dma=1
    hdma=5
    sbmixer=true
    oplmode=auto
    oplemu=default
    oplrate=44100
    

    Sources configuration

    Though this is a premature introduction to the sources, it is handy to present the configuration constants now that we have just seen the DOSBox configurations.

    In the file cfg.asm there are these constants

    ;IO Base
    SB16_BASE   EQU 220h
    
    ;16-bit DMA channel (must be between 5-7)
    SB16_HDMA   EQU 5
    
    ;IRQ Number
    SB16_IRQ    EQU 7   
    

    The values here must reflect the ones present in the DOSBox config file.
    Every other constant defined in the file is for the use of the program and not intended to be modified unless you know what you are doing8.

    The cfg.asm has nothing else of interest and won't be discussed again.

    How to play samples

    After a long introduction, we are now ready to see how to play a buffer of samples.
    A very good and synthetic reference is [available here]tutorial/documentation on the SB16 here.
    This answer is basically an implementation of what is written there, with some verbose explanation.

    These are the step we will follow:

    To playback a buffer of samples the step requested are:

    • Allocate a buffer that does not cross a 64k physical page boundary
    • Install an interrupt service routine
    • Program the DMA controller for background transfer
    • Set the sampling rate
    • Write the I/O command to the DSP
    • Write the I/O transfer mode to the DSP
    • Write the block size to the DSP (Low byte/high byte)

    The goal is to play this WAV file of a Super Mario Bros coin.

    Sources organization

    There are seven files:

    • sb16.asm is the main file that includes the others.
      It performs the steps above.
    • cfg.asm contains the configuration constants.
    • buffer.asm contains the routines for allocating the samples buffer.
    • data.asm contains the routines that fill the buffer.
      This is the file to edit to adapt the source to other goals.
    • isr.asm contains the routines that set the ISR and the ISR itself.
    • dma.asm contains the routines that program the DMA.
    • dsp.asm contains the routines that program the DSP.

    In general, the files are short.

    The sample buffer

    The high-level process is as follow: the card is given a buffer to read, when done it triggers an interrupt and stops; the software then update the buffer and restart the playback.
    The drawback with this method is that it introduces pauses in the playback that present themselves as audible "clicks".
    The DMA and the DSP support a mode called auto-initialize where, when the end of the buffer is reached, the transfer and the playback start over from the start.
    This is good for a cyclic static buffer but won't help for an ever-updating buffer.

    The trick is to program the DMA to transfer a block twice as large as the block the DSP is programmed to read. This will make the card generate an interrupt at the middle of the buffer.
    The software will then resume the playback immediately and then update the half just read. This is explained in the diagram below.

    How big should the buffer be?

    I have chose a size of 1/100 sec at 44100 samples per second, mono, 16-bit per sample. This is 441 samples times 1 audio channel times 2 bytes per sample.
    This is the block size. Since we have two blocks, the buffer size should be twice as much.
    In practice, it is four times as much (in the end, it is about 3.5 KiB).

    The big problem with the buffer is that it must not cross a physical 64KiB boundary9.
    Note that this is not the same as not crossing a 64KiB logical boundary (which is impossible without changing segment).

    I couldn't find a suitable allocation routine in the Ralf Brown Interrupt List, so I proceeded by abstracting the behaviour in two routines.

    • AllocateBuffer that must set the variables bufferOffset and bufferSegment with the far pointer to the allocated buffer of size at least BLOCK_SIZE * 2.
      Upon return, if CF it means the procedure failed.
    • FreeBufferIfAllocated that is called to free the buffer. It is up to this procedure to check if a buffer was effectively allocated or not.

    The default implementation statically allocates in the data segment a buffer that is twice as needed, as said.
    My reasoning was that if this unmoveable buffer crosses a 64KiB boundary than it is split into two halves, L and H, and it is true that L + H = BLOCK_SIZE * 2 * 2.
    Since the worst case scenario is when L = H, i.e. the buffer is split in the middle, the double size gives a size of BLOCK_SIZE * 2 * 2 / 2 = BLOCK_SIZE * 2 in the worst case scenario for both L and H.
    This guarantees us that we can always find a half as large as BLOCK_SIZE * 2, which is what we needed.

    The AllocateBuffer just find an appropriate half and set the value of the far pointer mentioned above. FreeBufferIfAllocated does nothing.

    Note that by "buffer" I mean two "blocks" and a "block" is the unit of playback.

    What format should the buffer use?

    To keep the things simple, the DSP is programmed to playback 16-bit mono samplings.
    However, the procedures that fill the blocks have been abstracted into data.asm.

    • UpdateBuffer is called by the ISR to update a block.
      The parameters are

      AX = Block number (Either 0 or 1) BX = Block mask (0 for block 0, 0ffffh for block 1)

      They are used to compute the offset into the buffer with this code

      ;Set ES:DI to point to start of the current block
      
      mov di, WORD PTR [bufferSegment]
      mov es, di
      mov di, BLOCK_SIZE
      and di, bx
      add di, WORD PTR [bufferOffset]
      

      The rest of the procedure read a block of samples from the WAV file.
      If the file has ended, the file pointer is reset back to the beginning to implement a cycling playback.

      BEWARE You are called in an ISR context, while the ACK and the EOI have already been issued, you must not clobber any register.
      Failing to respect this rule will result in difficult to understand bugs and possibly freezes.

      • InitBuffer is called one at the beginning to initialize the buffer if needed.
        The current implementation opens the file coin.wav10, read the sample rate and set the file pointer to the data section.
        This procedure uses the CF to signal an error. If the CF is set, an error has been encountered and DX holds a pointer to a $ terminated string that will be printed.

      • FinitBuffer used at the end to free the buffer resources.
        The buffer memory itself is freed as said above.
        This is called even if InitBuffer fails.

    We will talk about the WAV reading below.

    Installing the ISR

    I assume you are familiar with the IVT.
    I suggest reading about the twos 8259A PIC used to routes IRQs.

    In shorts:

    • There are 15 IRQ lines, from 0 to 15, 2 excluded.
    • An IRQ line must be enabled (unmasked) before the use.
    • After an IRQ has been served, an End of Interrupt (EOI) must be sent to the PIC that served it. IRQs above 7 are served by both PICs.
    • IRQ 0-7 are mapped to interrupt numbers 08h-0fh, IRQ 8-15 to 70h-78h

    The file isr.asm is very short.
    The routine SwapISRs swap the current ISR pointer for the IRQ of the SB16 with a local pointer.
    Initially, this pointer points to the ISR Sb16Isr, so that the first call to SwapISRs will install our ISR.
    The second call will restore the original one.

    Sb16Isr does a few things:

    • It acknowledges the IRQ to the SB16 (more on this later).
    • It files the EOI to the PIC(s).
    • It calls UpdateBuffer.
    • It updates the block number and block mask passed to the routine above.

    NOTE SwapISRs also toggles the bit for the IRQ mask. It assumes that the IRQ is masked at the beginning of the program. You may want to change this to a more robust setting (or restart DOSBox if you abruptly interrupt the program).

    Programming the DMA controller

    The SB16 was an ISA card, it couldn't read the memory directly.
    To solve this problem the DMA chip, 8357 was invented.
    It had four channels, independently configurable, that when triggered performed a read from the memory to the ISA bus or vice-versa.
    There were two DMA controllers, the first one handled only 8-bit data transfers and channels 0-3.
    The second one used 16-bit data transfers and handled the channels 4-7.

    We are going to use the 16-bit transfers so, the DMA channel must be one of 5-7 (channel four is a bit special).
    The SB16 can also use 8-bit transfers, so it has two configurations for the DMA channel: one for the 8-bit moves and one for the 16-bit moves.

    Each channel, but channel four, has three parts:

    • A 16-bit start address.
    • A 16-bit counter for the size.
    • An 8-bit page number.

    The address is a physical address (linear)! So in theory only the first 64KiB were accessible.
    The page number was used as the upper part of the address.
    However, the counter logic is still 16-bit, so the pointer to the data to read/write still wrap around at 64 KiB boundaries (should be 128 KiB for 16-bit).

    The dma.asm files contain a single routine SetDMA that given the logical start address and the size, program the DMA.
    There isn't anything esoteric here besides a few arithmetic to compute the value to use.

    The mode is Single mode and auto-initialization is on.
    The document about the SB16 programming liked at the beginning has a very clear step-by-step procedure on this.

    Programming the DSP

    The SB16 IO layout was as follow:

    ADDR    READ                        WRITE   
    
    2x6h    DSP Reset*                  DSP reset**
    
    2xAh    DSP Read***  
    
    2xCh    DSP Write                   DSP write (command and data)
            (bit7 set if ok to write)
    
    2xEh    DSP Read Status****
            (bit7 set if ok to read)
    
    2xFh    DSP 16-bit interrupt acknowledge 
    
    * bit 7 set after the reset completes
    ** toggle bit 7, with a 3us interval between setting and clearing, to start a reset
    *** Wait for reading a 0AAh after a reset
    **** Also used to ACK 8-bit IRQs
    

    The file dsp.asm contains the basic routines ResetDSP, WriteDSP and ReadDSP that performs a reset, write a byte to the DSP after waiting for right conditions, read a byte from the DSP.

    The DSP is used through commands.

    • To set the sampling of the playback use the command 41h, followed by the low byte of the sampling frequency and then by the high byte.
      The routine SetSampling takes the sampling frequency in AX and set it.

    • To playback use the command b6h, followed by a mode byte and then by the block length (two bytes, low byte first).
      The routine StartPlayback takes the sampling frequency in AX, the mode byte in BL and the size in CX and start a playback (after setting the sample rate).
      Note that the DSP doesn't need to know the address of the buffer, it just triggers the channel request pin of the DMA and it will have the data on the bus.
      It is the DMA that have to know where the buffer is.

    • To stop a playback use the command d5h.
      StopPlayback does this.

    Playing the WAV file

    What the demo program do is playing the coin.wav file.
    This is file is specific it is a 16-bit mono file.

    The demo program doesn't parse the full RIFF format (you can see this nice page, it is hardwired to work with that specific file.
    Though any file with identical format, yet different data, should do.

    After the steps introduce at the beginning, the program simply wait for a keystroke.
    After that it performs all the de-initializations (including stopping the playback) and exit.

    To continue from here, you have "only" to properly implement the routine in data.asm.
    It should be straightforward to make each key plays a different file.

    If the number of file is small I would open all the files in InitBuffer, then in sb16.asm implement a loop liked

    xor ah, ah
    int 16h
    
    cmp al, ...
    je ...
    
    cmp al, ...
    je ...
    

    where each jump gets the file handle to play. (a lookup table would be better).
    Then:

    1. Reset the file pointer of the file to play to the start of the samples.
    2. xchg the new file pointer with fileHandle (used by UpdateBuffer).

    I leave to you how to make the playback stop when the key is released and resume when it is pressed.

    Code

    sb16.asm

    .386
    .387
    .MODEL SMALL
    
    .STACK
    
    
    INCLUDE cfg.asm
    INCLUDE buffer.asm
    
    
    _DATI SEGMENT PARA PUBLIC 'DATA' USE16
    
     ;This is the segment to the buffer for the sampling
    
     samplingBuffer   dw 0 
    
     ;Strings
    
     strErrorBuffer   db "Cannot allocate or find a buffer for the samplings :(", 24h
    
     strPressAnyKey   db "Press any key to exit", 13, 10, 24h
     strBye           db "Sound should stop now", 13, 10, 24h
    
    _DATI ENDS
    
    INCLUDE data.asm
    INCLUDE isr.asm
    INCLUDE dsp.asm
    INCLUDE dma.asm
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE, DS:_DATI, ES:_DATI
    
    __START__:
    
     ;Basic initialization
    
     mov ax, _DATI
     mov ds, ax
    
    
    
     ;S E T   T H E   N E W   I S R
    
     call SwapISRs
    
    
    
     ;A L L O C A T E   T H E   B U F F E R
    
     call AllocateBuffer
     mov dx, OFFSET strErrorBuffer
    jc _error
    
    
    
     ;I N I T   T H E   B U F F E R
    
     call InitBuffer
    jc _finit_buffer
    
    
     ;S E T U P   D M A
    
     mov si, WORD PTR [bufferSegment]
     mov es, si
     mov si, WORD PTR [bufferOffset]
     mov di, BLOCK_SIZE * 2
     call SetDMA
    
    
    
     ;S T A R T   P L A Y B A C K
    
     call ResetDSP
    
     mov ax, WORD PTR [sampleRate]            ;Sampling
     mov bx, FORMAT_MONO OR FORMAT_SIGNED     ;Format
     mov cx, BLOCK_SIZE                       ;Size
     call StartPlayback
    
    
    
     ;W A I T
    
     mov ah, 09h
     mov dx, OFFSET strPressAnyKey
     int 21h
    
     xor ah, ah
     int 16h
    
    
    
     ;S T O P
    
     call StopPlayback
    
     mov dx, OFFSET strBye
    
    _finit_buffer:
    
     ;F R E E   B L O C K   R E S O U R C E S
     call FinitBuffer
    
    
     ;E R R O R   H A N D L I N G
    
     ;When called DX points to a string
    
    _error:
     ;R E S T O R E   T H E   O L D   I S R s
    
     call SwapISRs
    
    
    
     call FreeBufferIfAllocated
    
     mov ah, 09h
     int 21h
    
     ;E N D
    
    _end:
     mov ax, 4c00h
     int 21h
    
    _CODE ENDS
    
    END __START__
    

    cfg.asm

     ;These are the only configurable constants
    
     ;IO Base
     SB16_BASE   EQU 220h
    
     ;16-bit DMA channel (must be between 5-7)
     SB16_HDMA   EQU 5
    
     ;IRQ Number
     SB16_IRQ    EQU 7
    
     ;These a computed values, don't touch them if you don't know what
     ;you are doing
    
     ;REGISTER NAMES
    
     REG_DSP_RESET      EQU SB16_BASE + 6
     REG_DSP_READ       EQU SB16_BASE + 0ah
     REG_DSP_WRITE_BS   EQU SB16_BASE + 0ch
     REG_DSP_WRITE_CMD  EQU SB16_BASE + 0ch
     REG_DSP_WRITE_DATA EQU SB16_BASE + 0ch
     REG_DSP_READ_BS    EQU SB16_BASE + 0eh
     REG_DSP_ACK        EQU SB16_BASE + 0eh
     REG_DSP_ACK_16     EQU SB16_BASE + 0fh
    
     ;DSP COMMANDS
    
     DSP_SET_SAMPLING_OUTPUT   EQU 41h
     DSP_DMA_16_OUTPUT_AUTO    EQU 0b6h
     DSP_STOP_DMA_16           EQU 0d5h
    
     ;DMA REGISTERS
    
     REG_DMA_ADDRESS    EQU 0c0h + (SB16_HDMA - 4) * 4
     REG_DMA_COUNT      EQU REG_DMA_ADDRESS + 02h
    
     REG_DMA_MASK       EQU 0d4h
     REG_DMA_MODE       EQU 0d6h
     REG_DMA_CLEAR_FF   EQU 0d8h
    
    
     IF SB16_HDMA - 5
        REG_DMA_PAGE       EQU 8bh      
     ELSE
        IF SB16_HDMA - 6
           REG_DMA_PAGE       EQU 89h
        ELSE
           REG_DMA_PAGE       EQU 8ah
        ENDIF
     ENDIF
    
     ;ISR vector
     ISR_VECTOR            EQU ((SB16_IRQ SHR 3) * (70h - 08h) + (SB16_IRQ AND 7) + 08h) * 4
    
     PIC_DATA       EQU (SB16_IRQ AND 8) + 21h
     PIC_MASK               EQU 1 SHL (SB16_IRQ AND 7)
    

    dma.asm

    .8086
    .MODEL SMALL
    
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE
    
    
     ;ES = buffer segment
     ;SI = buffer offset
     ;DI = count
     SetDMA:
      push dx
      push ax
      push cx
      push bx
      push si
    
      ;Disable SB16 DMA channel
    
      mov dx, REG_DMA_MASK
      mov al, 4 + SB16_HDMA MOD 4
      out dx, al
    
      ;Clear counter FF
    
      mov dx, REG_DMA_CLEAR_FF
      out dx, al
    
      ;Set transfert mode
    
      mov dx, REG_DMA_MODE
      mov al, 58h + SB16_HDMA MOD 4
      out dx, al
    
    
      ;Set address (in WORDs)
    
      ;SSSS SSSS SSSS SSSS 0000
      ;0SSS SSSS SSSS SSSS S000
    
      mov bx, es
      shr bx, 0dh        ;BL = addr[20:16]  
      mov cx, es
      shl cx, 3          ;CX = addr[15:0]
      shr si, 1
      add cx, si
      adc bx, 0             
    
      mov dx, REG_DMA_ADDRESS
      mov al, cl
      out dx, al
      mov al, ch
      out dx, al
    
      mov dx, REG_DMA_PAGE
      mov al, bl
      out dx, al
    
      ;Set count
    
      mov ax, di
      shr ax, 1
    
      mov dx, REG_DMA_COUNT
      out dx, al
      mov al, ah
      out dx, al
    
      ;Enable DMA channel
    
      mov dx, REG_DMA_MASK
      mov al, SB16_HDMA MOD 4
      out dx, al
    
      pop si
      pop bx
      pop cx
      pop ax
      pop dx
    
      ret
    
    _CODE ENDS
    

    isr.asm

    .8086
    .MODEL SMALL
    
    _DATI SEGMENT PARA PUBLIC 'DATA' USE16
    
     ;This is a pointer to the ISR we will install 
    
     nextISR         dw OFFSET Sb16Isr
                     dw _CODE
    
     ;This is the internal status managed by the ISR
    
     blockNumber     dw 0
     blockMask       dw 0
    
    _DATI ENDS
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE, DS:_DATI, ES:_DATI
    
     ;Swaps two far pointers
    
     ;DS:SI = ptr to ptr1
     ;ES:DI = ptr to ptr2
     SwapFarPointers:
      push bx
    
      mov bx, WORD PTR [si]
      xchg WORD PTR es:[di], bx
      mov WORD PTR [si], bx
    
      mov bx, WORD PTR [si+02h]
      xchg WORD PTR es:[di+02h], bx
      mov WORD PTR [si+02h], bx
    
      pop bx
      ret  
    
     ;Swaps the ISR vector of the IRQ of the card with a saved value
    
     SwapISRs:
      push es
      push si
      push di
      push dx
      push ax
    
      cli
    
      mov si, OFFSET nextISR
      xor di, di
      mov es, di
      mov di, ISR_VECTOR
      call SwapFarPointers
    
      sti
    
      ;Toggle PIC mask bit
      mov dx, PIC_DATA
      in al, dx
      xor al, PIC_MASK
      out dx, al
    
      pop ax
      pop dx
      pop di
      pop si
      pop es
      ret  
    
    
     ;This is the ISR
    
     Sb16Isr:
      push ax
      push dx
      push ds
      push es
    
      ;Ack IRQ to SB16
    
      mov dx, REG_DSP_ACK_16
      in al, dx
    
      ;EOI to PICs
    
      mov al, 20h
      out 20h, al
    
    IF SB16_IRQ SHR 3 
      out 0a0h, al
    ENDIF
    
      mov ax, _DATA
      mov ds, ax
    
      mov ax, WORD PTR [BlockNumber]
      mov bx, WORD PTR [BlockMask]  
      call UpdateBuffer
    
      not bx
      inc ax
      and al, 01h
    
      mov WORD PTR [BlockNumber], ax
      mov WORD PTR [BlockMask], bx
    
      pop es
      pop ds
      pop dx
      pop ax
      iret
    
    
    _CODE ENDS
    

    data.asm

    .8086
    .MODEL SMALL
    
    _DATI SEGMENT PARA PUBLIC 'DATA' USE16
    
      strWaveFile           db "coin.wav", 0
      strFileNotFound       db "File not found!", 24h
      strFileError          db "Error while reading WAV file!", 24h
    
      fileHandle            dw 0
    
      sampleRate            dw 0
    
    _DATI ENDS
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE, DS:_DATI, ES:_DATI
    
     ;This is called to update the block given
    
    
     ;AX = Block number (Either 0 or 1)
     ;BX = Block mask (0 for block 0, 0ffffh for block 1)
     UpdateBuffer:
      push es
      push di
      push bx
      push ax
      push si
      push cx
      push dx
    
      ;Set ES:DI to point to start of the current block
    
      mov di, WORD PTR [bufferSegment]
      mov es, di
      mov di, BLOCK_SIZE
      and di, bx
      add di, WORD PTR [bufferOffset]
    
      ;Read from file
    
      push ds
    
      mov ax, es
      mov ds, ax
      mov dx, di
    
      mov ah, 3fh
      mov bx, WORD PTR [fileHandle]
      mov cx, BLOCK_SIZE
      int 21h
    
      pop ds  
    
      ;Check if EOF
    
      cmp ax, BLOCK_SIZE
      je _ub_end
    
      mov ax, 4200h
      mov bx, WORD PTR [fileHandle]
      xor cx, cx
      mov dx, 44d
      int 21h
    
     _ub_end:
      pop dx
      pop cx
      pop si
      pop ax
      pop bx
      pop di
      pop es
      ret
    
     ;This is called to initialize both blocks
     ;Set CF on return (and set DX to the offset of a string) to show an error and exit
     InitBuffer:
      push ax
      push bx
    
      ;finit
    
      ;xor ax, ax
      ;mov bx, ax
      ;call UpdateBuffer
    
      ;inc al
      ;not bx
      ;call UpdateBuffer
    
    
      mov ax, 3d00h
      mov dx, OFFSET strWaveFile
      int 21h
    
      mov dx, OFFSET strFileNotFound
      mov WORD PTR [fileHandle], ax
     jc _ib_end
    
      ;Read sample rate
    
      mov bx, ax
      mov ax, 4200h
      xor cx, cx
      mov dx, 18h
      int 21h
    
      mov dx, OFFSET strFileError
     jc _ib_end
    
      mov ah, 3fh
      mov bx, WORD PTR [fileHandle]
      mov cx, 2
      mov dx, OFFSET sampleRate
      int 21h
    
      mov dx, WORD PTR [sampleRate]             ;DEBUG
    
      mov dx, OFFSET strFileError
     jc _ib_end
    
      ;Set file pointer to start of data
    
      mov ax, 4200h
      mov bx, WORD PTR [fileHandle]
      xor cx, cx
      mov dx, 44d
      int 21h
    
    
     _ib_end:
      pop bx
      pop ax
      ret
    
    
     ;Closed to finalize the buffer before exits
    
     FinitBuffer:
      push ax
      push bx
      push dx
    
      mov bx, WORD PTR [fileHandle]
      test bx, bx
     jz _fib_end
    
      mov ah, 3eh
      int 21h
    
     _fib_end:
      pop dx
      pop bx
      pop ax
      ret
    
    _CODE ENDS
    

    1 For example because it asks for a non-trivial amount of code or for a resource.

    2 Beware that the Table Of Content has some pages switched.

    3 _DATA is already defined.

    4 Each source file redefine those segments if used.

    5 The symbols _DATI and _CODE can be used to denote the segment part of the starting address of the final segments.

    6 I don't remember the exact technical name for the EQU values.

    7 These values are DOSBox defaults but be sure to check the config file anyway.

    8 Specially because TASM lacks supports for a lot of conditional and a bit of bit-arithmetic is needed to set some value.

    9 This should be 128KiB for 16-bit DMA, which we are using, but I don't remember exactly and didn't want to experiment.

    10 Beware of DOS limitations on file names.

    0 讨论(0)
  • 2020-12-18 14:56

    This present a demo program that plays a specific WAV file (to avoid introducing a RIFF parser to the already too-long-for-SO code. The program has been tested in DOSBox, but a lot of things can go wrong on different configurations.

    Finally, I was forced to split the code into two answers.
    This is part 2.

    dsp.asm

    .8086
    .MODEL SMALL
    
     FORMAT_MONO     EQU 00h
     FORMAT_STEREO   EQU 20h
     FORMAT_SIGNED   EQU 10h
     FORMAT_UNSIGNED EQU 00h
    
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE
    
    
     ResetDSP:
      push ax
      push dx
    
      ;Set reset bit
    
      mov dx, REG_DSP_RESET
      mov al, 01h
      out dx, al
    
      ;Wait 3 us
    
      in al, 80h
      in al, 80h
      in al, 80h
    
      ;Clear reset bit
    
      xor al, al
      out dx, al
    
      ;Poll BS until bit 7 is set
    
      mov dx, REG_DSP_READ_BS
    
     _rd_poll_bs:
      in al, dx
      test al, 80h
     jz _rd_poll_bs
    
      ;Poll data until 0aah
    
      mov dx, REG_DSP_READ
    
     _rd_poll_data:
      in al, dx
      cmp al, 0aah
     jne _rd_poll_data
    
      pop dx
      pop ax
      ret
    
    
    
     ;AL = command/data
     WriteDSP:
      push dx
      push ax
    
      mov dx, REG_DSP_WRITE_BS
    
     _wd_poll:
      in al, dx
      test al, 80h
     jz _wd_poll
    
      pop ax
    
      mov dx, REG_DSP_WRITE_DATA
      out dx, al
    
      pop dx
      ret
    
    
     ;Return AL 
     ReadDSP:
      push dx
    
      mov dx, REG_DSP_READ_BS
    
     _rdd_poll:
      in al, dx
      test al, 80h
     jz _rdd_poll
    
      pop ax
    
      mov dx, REG_DSP_READ
      in al, dx
    
      pop dx
      ret
    
    
     ;AX = sampling
     SetSampling:
      push dx
    
      xchg al, ah
    
      push ax
    
      mov al, DSP_SET_SAMPLING_OUTPUT
      call WriteDSP
    
      pop ax
    
      call WriteDSP
    
      mov al, ah
      call WriteDSP
    
      pop dx
      ret
    
    
     ;Starts a playback
    
     ;AX = Sampling
     ;BL = Mode
     ;CX = Size
     StartPlayback:
    
      ;Set sampling
    
      call SetSampling
    
      ;Start playback command
    
      mov al, DSP_DMA_16_OUTPUT_AUTO
      call WriteDSP
      mov al, bl            
      call WriteDSP                            ;Format 
      mov al, cl
      call WriteDSP                            ;Size (Low)
      mov al, ch   
      call WriteDSP                            ;Size (High)
    
      ret
    
     ;Stops the playback
    
     StopPlayback:
       push ax
    
       mov al, DSP_STOP_DMA_16
       call WriteDSP
    
       pop ax
       ret
    
    _CODE ENDS
    

    buffer.asm

    .8086
    .MODEL SMALL
    
    
     ;Block size is 1/100 of a second at 44100 samplings per seconds
    
     BLOCK_SIZE      EQU 44100 / 100 * 2
    
     ;Buffer size allocated, it is twice the BLOCK_SIZE because there are two blocks.
     ;Size is doubled again so that we are sure to find an area that doesn't cross a
     ;64KiB boundary
     ;Total buffer size is about 3.5 KiB
    
     BUFFER_SIZE     EQU  BLOCK_SIZE * 2 * 2
    
    _DATI SEGMENT PARA PUBLIC 'DATA' USE16
    
     ;This is the buffer
    
     buffer            db BUFFER_SIZE DUP(0)
    
    
     bufferOffset      dw OFFSET buffer
     bufferSegment     dw _DATI
    
    _DATI ENDS
    
    
    _CODE SEGMENT PARA PUBLIC 'CODE' USE16
     ASSUME CS:_CODE, DS:_DATI
    
    
     ;Allocate a buffer of size BLOCK_SIZE * 2 that doesn't cross
     ;a physical 64KiB
     ;This is achieved by allocating TWICE as much space and than
     ;Aligning the segment on 64KiB if necessary
    
    
     AllocateBuffer:
      push bx
      push cx
      push ax
      push dx
    
      ;Compute linear address of the buffer
    
      mov bx, _DATI
      shr bx, 0ch
      mov cx, _DATI
      shl cx, 4
      add cx, OFFSET buffer
      adc bx, 0                                 ;BX:CX = Linear address
    
    
    
      ;Does it starts at 64KiB?
    
      test cx, cx
     jz _ab_End                                ;Yes, we are fine
    
      mov dx, cx
      mov ax, bx
    
      ;Find next start of 64KiB
    
      xor dx, dx
      inc ax
    
      push ax
      push dx
    
      ;Check if next boundary is after our buffer
    
      sub dx, cx
      sub ax, bx
    
      cmp dx, BUFFER_SIZE / 2
    
      pop dx
      pop ax
    
     jae _ab_end
    
    
    
      mov bx, dx
      and bx, 0fh
      mov WORD PTR [bufferOffset], bx
    
      mov bx, ax
      shl bx, 0ch
      shr dx, 04h
      or bx, dx
      mov WORD PTR [bufferSegment], bx
    
     _ab_end:
      clc
    
      pop dx
      pop ax
      pop cx
      pop bx
    
      ret
    
    
     ;Free the buffer
    
     FreeBufferIfAllocated:
    
      ;Nothing to do
    
      ret
    
    
    
    
    _CODE ENDS
    
    0 讨论(0)
提交回复
热议问题