pouët.net

Go to bottom

Ports 330h/331h in WinXP’s NTVDM

category: code [glöplog]
 
I tried to write a small game as an example for my students who learn x86 assembly and I got into trouble making it produce sounds. I managed to make a small program that reproduces the problem (FASM syntax).

Code:include 'macro\proc16.inc' MIDIPORT_DATA = $0330 MIDIPORT_COMMAND = $0331 org 100h Start: stdcall MIDI.Initialize stdcall MIDI._WriteData, $90 stdcall MIDI._WriteData, $60 stdcall MIDI._WriteData, $7F xor ax, ax int 16h ret proc MIDI._WriteCommand\ bValue mov dx, MIDIPORT_COMMAND @@: in al, dx test al, $40 jnz @B mov ax, [bValue] out dx, al ret endp proc MIDI._WriteData\ bValue mov dx, MIDIPORT_COMMAND @@: in al, dx test al, $40 jnz @B mov dx, MIDIPORT_DATA mov ax, [bValue] out dx, al ret endp proc MIDI.Initialize stdcall MIDI._WriteCommand, $FF .WaitAck: mov dx, MIDIPORT_COMMAND @@: in al, dx test al, $80 jnz @B mov dx, MIDIPORT_DATA in al, dx cmp al, $FE jne .WaitAck stdcall MIDI._WriteCommand, $3F ; Tried to wait for acknowledge here, didn’t work either. ; Besides, in some sources there’s a note than acknowledge byte ; is not sent for the 3Fh command. ret endp


It works (i.e. produces a piano note) in DOSBox but fails in NTVDM (I use WinXP’s one). I tried different combinations and it turns out that a MIDI message gets processed only after the next one is sent. In other words, last MIDI message always gets delayed until the next one arrives.

Maybe someone has any ideas or experience to share? Or maybe someone could point me to an MS-DOS demo that uses ports 330h/331h to produce sound and that actually works when run under NTVDM?

Thanks.
added on the 2016-05-01 12:52:57 by Dmitry Dmitry
sounds like you're missing a close message byte in the protocol somewhere. and when you send the new note the input assumes the previous one is closed and processes it.

never tried messing with that though, so i could be wrong.
added on the 2016-05-01 14:52:37 by psenough psenough
Hello Dmitry
here is an example of a ntvdm working midi sound. And here is the source.

Several things can cause MIDI not to work on a real machine (not DOSBox) and these include BIOS settings enabling the MPU and enabling MPU in Windows : here is how to diagnose what you are missing.

So all in all in brief : search for a .mid on your xp installation and fire it up. If you have sound it means your order of initialisation in code is faulty (in which case you can look at most of my 256b on different ways to init correctly). If you don't have sound it means that your code is not at fault but you computer settings to enable MPU.
Also be aware 330h is default port, but it could very well be 331h...336h in your specific case.
Hi Dmitry,

first of all, you're right. I also developed a lot of tiny intros using MIDI (for example, Hypnoteye , Function or Descent ) which all use the same standard MIDI setup : Setup UART Mode and play the notes. While Hypnoteye and Descent are not different on WinXP (i usually develop on Windows 7 and test primarily on DosBox) ... Function is. I never really understood why, but your observation is actually on point: the note does not get played until the next one -> the following code plays a note on DosBox but doesn't on WinXP (to my experience right now, it totally should)

Code:mov dx,0x331 mov al,0x3f out dx,al dec dx mov al,0x90 out dx,al mov al,0x40 out dx,al mov al,0x7f out dx,al L: in al,0x60 dec al jnz L ret


That does not get noticed when repeated (same) notes or automatic procedural scales are played, but it does when you introduce human patterns or even go for interactive sound.

I grabbed some piece of code for a tiny piano player to verify this. Fine on DosBox, one note behind on NTVDM.

Since Baudsurfer, Lord Kelvin and many others (including me) definitely use exactly the technique described here (0x3F to port 0x331 and go from there), it could very well be that you (re)discovered something nobody was really aware of, since nobody really played "music" to a realistic degree in tiny intros, using UART mode on Windows XP, for quite a while (or didn't care enough, like myself with "Function" ;)

I'm not really sure about switching to different ports, like Baudsurfer suggested, but i think if the ports would be off, we would hear rather nothing, instead of the precise-one-note-behind behavour.

So my best guess is, something is strange with the NT-VDM on Windows XP, like :
Quote:
The Windows operating system's 16-bit subsystem is lacking in several areas which directly or indirectly affect VDMSound emulation:

Incomplete DPMI support in Windows NT/2000/XP results in a number of games not starting or crashing randomly when they communicate with the emulated sound card
Improper interrupt emulation in Windows NT/2000 (but not XP) results in some games hanging when they communicate with the emulated sound card (requiring patching via CLI2NOP.[10])
Improper PIC emulation in Windows results in games not being able to use normal (or intelligent) mode, limiting VDMSound's MPU-401 emulation support to UART-mode only.
DOSBox does not rely on the Windows 16-bit subsystem and is thus not subject to these limitations.

Source

... but i really don't know the details here. Thanks anyway for helping me pinpoint my own problems with sound ;) Maybe the "correct" way of playing a MIDI note in UART mode is actually different to what we have been done over the years, and it just needed you coming along to remind us ;)
added on the 2016-05-01 23:02:07 by HellMood HellMood
Thanks for your replies, everyone!

psenough
It indeed looks like the MPU requires something it doesn’t get. And yes, sending a few zero bytes (for example) makes the delayed note start its job. But what drives me crazy is that there’s no information in docs about that, about what bytes are expected and so on. A simple MIDI message “Note On” consists of 3 bytes and (depending on the place it is used) may be prepended by a MIDI timestamp, but this didn’t work in my case.

Baudsurfer
A was really happy to see that all of your demos with sound worked for me. But after I took one of them (a 128b “Deus faber”) and tried to remove graphics, interrupt handling, etc. leaving only sound related code, it turned out that it worked for me just because there were many similar notes playing in rapid succession, thus the fact that last one was being delayed got hidden from my ear.

Here’s a small piece of code I got after minimization:

Code: org 100h mov dx,331h mov al,3fh out dx,al dec dx ; These 4 lines can also be removed (program change) ; Doesn’t have any influence on the program’s behaviour ; except the instrument being used. mov al,0cah out dx,al mov al,60 out dx,al mov cx,2 @@: mov al,9ah out dx,al mov al,43h ; Replaced the note with a higher one out dx,al mov al,127 ; And increased the velocity to maximum out dx,al loop @B xor ax,ax int 16h ret


The loop there is written instead of the 1Ch interrupt handler. If I set cx to 2+, I can hear the sound. In fact, for higher notes the results doesn’t differ much. But if I set cx to 1, the sound is gone. (BTW, making cx too large causes a short noise before the sound actually starts, I guess, due to the number of notes being sent in rapid succession).

I wonder if there’s any single-note working example out there that works in NTVDM :-(
added on the 2016-05-01 23:16:23 by Dmitry Dmitry
HellMood
Your reply appeared while I was writing my previous post :-)

Thanks for the confirmation of the problem’s existence, at least now I can be sure it’s not only my WinXP+NTVDM+whatever_else_in_play.

I found a small MIDI player today written in QBasic (with sources) and it seems to work just fine. I haven’t checked it for one-note (more strictly: one-MIDI-message) case, but I noticed that there’re a few strange manipulations besides actual switching to UART mode and playing. Trying to investigate right now…
added on the 2016-05-01 23:23:11 by Dmitry Dmitry
@Dmitry, please read my post above yours ;)
added on the 2016-05-01 23:24:12 by HellMood HellMood
Dear Dmitry,
Quote:

A simple MIDI message “Note On” consists of 3 bytes and (depending on the place it is used) may be prepended by a MIDI timestamp, but this didn’t work in my case.


Not quite. Only if you have set midi mode status command, bank select status command, program change status command and channel select select status command, beforehand.

Also there are several reasons why you may not hear a note at one point, and it is not only "notes playing in rapid succession", it can be a missing MS-GM1 Midi soundpatch, it can be a too low octave/note for the sound patch(ie: instrument), it can be a too low octave/note for the sound patch(ie: instrument) so that it's frequency will not be hearable before sound builds up for another 4 seconds, it can be wrong note velocity, or it could be a sound volume >127 for example.

Quote:
what drives me crazy is that there’s no information in docs about that, about what bytes are expected and so on.


Code:mididata db 03fh ; mididata+000h cstcst set midi mode status command (03fh=set UART mode on) db 000h ; mididata+001h cstcst bank select status command db 000h ; mididata+002h cstvar program change status command (0c9h=channel#10, 0cah=channel#11) db 020h ; mididata+003h varvar program number status data=acoustic base (cf. en.wikipedia.org/wiki/General_MIDI) db 000h ; mididata+004h cstvar channel select status command (099h=channel#10, 09ah=channel#11) db 000h ; mididata+005h varvar note value status data (C/do C#/do# D/re D#/re# E/mi F/fa F#/fa# G/sol G#/sol# A/la A#/la# B/si) db 040h ; mididata+006h varvar note volume status data (ranges from 000h to 040h) db 07fh ; mididata+007h cstcst note one status command


Your main two sources of information are Wikipedia General Midi and Midi note chart. What is important to understand is that there are 16 different channel pairs : (cxh/9xh) while only channel 10 (cah/9ah) is reserved for percussion instruments only (that will enable "Percussions"). Hence why people often use cah/9ah pair in their examples. Playing on last bit of channel pair with xor 1 will alternate between strings and percussions though.

Quote:
I wonder if there’s any single-note working example out there that works in NTVDM :-(


Below is an unreleased sourcode of a small midi tracker I coded.

If anything it ought to enable you to structure your Midi messages (cf. mididata structure). Note it would be difficult in general to play Midi without relying on some form of timing vs. getting mildly-implemented status messages (for sake of melody timing).

For the rest it just takes time and practice, so don't give up

Code: ;-----------------------------------------------; ; TSR 128-byte MIDI tracker MP401 GM1 Soundfonts; ; ver. 0.98 ; ; written by Baudsurfer/Red Sector Inc. ;-----------------------------------------------; ; patterns in reverse order and nibble-crunched ; ;-----------------------------------------------; ; ax=general,dx=MPU data @ ,si=RTC delay ; ; bx=general,cx=pattern idx,di=track idx ; ; on entry : ds=cs,fs=0,ah=0,ch=0,di=0 ; ;-----------------------------------------------; org 100h ; Assemble as Fasm tracker.asm for 16-bit .com (XPSP3 tested) loadsong:mov dx,playsong ; set tsr isr vector lsw segment mov ax,251ch ; ah=function set interrupt vector, al=interrupt number=rtc system timer tick ds:dx=new isr vector int 21h ; general dos interrupt jmp $ playsong:push word cs ; isr called by cmos rtc tick 1024 times/second pop ds ; reset ds=cs in isr for correct data indexing mov si,songdata xor byte [si],1 ; set playing speed to 9.3 khz (18.2*1024/2) jz exitsong ; odd/even flipflop says not time to play another note lodsw ; ah=16 al=fliflop (don't care=3fh) si=tracks index (was mov si,traxidx) mov di,pattidx ; di=pattern index (base adressing size optimization) dec byte [di] ; [->test pattern end] check if switch track number required (was dec byte [pattidx]) jnz track ; else skip module: inc byte [si] ; increment track index (was inc word [traxidx]) mov byte [di],ah ; and reset pattern index (was mov byte [di],16 / mov byte [pattidx],16) track: mov bx,tracks ; [->get pattern from track] bl=track index (was mov bx,byte [cs:trackidx]) lodsb ; ax=track index (was mov ax,[si] / mov ax,[traxidx]) ! si now points to MIDI command note on 7fh xlatb ; al=track index (was mov bx,byte [cs:trackidx]) al=track value shl al,3 ; al=relative offset of pattern position value mov bl,byte [di] ; [->get 2 notes from from pattern index] (was mov bl,byte [pattidx]) shr bl,1 ; divide by 2 for dual note 2 nibbles pushf ; [->get odd/even note index from pattern index] save CF (was test byte [cs:patternidx],1) add al,bl ; bl=absolute note value nibble pair mov bx,patterns ; bx=absolute offset of pattern position value xlatb ; [get 2 notes from from pattern index->] al=instrument index popf ; [get odd/even note index from pattern index->] restore CF (was test byte [cs:patternidx],1) jc decrunch ; if pattern index odd then get note from MSNibble shr al,4 ; else if pattern index odd then get note from LSNibble decrunch: ;---- Load MIDI --------------------------------; <---- prequisites : none <destroys : al bx bp> ----> al=099h/09ah loadmidi:mov bx,instrmt ; ds:bx=address of instruments list mov bp,mididata ; bp=mididata+000h ; and al,00fh ; al=note/instrument nibble index from even/odd pattern index (aas ?:) ; jz sendsapi ; 000h=instrmt+000h=sapi vocoder xlatb ; al=instrument index (al=[bx+ax]) shr al,1 ; al=instrument value, cf=instrument index LSB (select channel part 1/4) mov [bp+005h],al ; note value status data=al salc ; al=255 if cf=1, al=0 if cf=0 (select channel part 2/4) sub al,066h ; al=099h/09ah (select channel part 3/4) mov [bp+004h],al ; channel select status command=al add al,030h ; al=0c9h/0cah (select channel part 4/4) mov [bp+002h],al ; program change status command=al sendmidi:mov dx,00331h ; dx=midi command port mov si,bp ; ds:si=address of midi data message outsb ; output midi command dec dx ; dx=midi data/status port 00330h mov cl,7 ; cl=length-1 of midi data message rep outsb ; output data/status to midi exitsong:iret ; return to caller ;-----------------------------------------------; b7b6b5b4b3b2b1<<1=note/instrument, b0=channel# (b0=0=>channel#11, b1=1=>channel#10) instrmt db 000h ; instrmt+000h 00000000b ; 00h,98h/00,152 ; #0 (n/a 32 of*98h) SAPI vocoder db 3ch ; instrmt+001h to tweak 10100000b ; 50h,98h/80,152 ; #1 (string 80 of 98h) GM1 synth db 2 ; instrmt+002h was 001h ; 00000001b ; 00h,99h/00,153 ; #2 (percussion 00 of*99h) GM1 silence db 049h ; instrmt+003h 01001001b ; 24h,99h/36,153 ; #3 (percussion 36 of 99h) GM1 Bass Drum 1 db 04bh ; instrmt+004h 01001011b ; 25h,99h/37,153 ; #4 (percussion 37 of 99h) GM1 Side Stick/Rimshot db 04dh ; instrmt+005h 01001101b ; 26h,99h/38,153 ; #5 (percussion 38 of 99h) GM1 Snare Drum 1 db 055h ; instrmt+006h 01010101b ; 2ah,99h/42,153 ; #6 (percussion 42 of 99h) GM1 Closed Hi-hat db 0a5h ; instrmt+007h 10100101b ; 52h,99h/82,153 ; #7 (percussion 82 of 99h) GM1 Shaker ;-----------------------------------------------; byte offset nibble description mididata db 03fh ; mididata+000h cstcst set midi mode status command (03fh=set UART mode on) db 000h ; mididata+001h cstcst bank select status command db 000h ; mididata+002h cstvar program change status command (0c9h=channel#10, 0cah=channel#11) db 020h ; mididata+003h varvar program number status data=acoustic base (cf. en.wikipedia.org/wiki/General_MIDI) db 000h ; mididata+004h cstvar channel select status command (099h=channel#10, 09ah=channel#11) db 000h ; mididata+005h varvar note value status data (C/do C#/do# D/re D#/re# E/mi F/fa F#/fa# G/sol G#/sol# A/la A#/la# B/si) db 040h ; mididata+006h varvar note volume status data (ranges from 000h to 040h) db 07fh ; mididata+007h cstcst note one status command ;-----------------------------------------------; byte offset description songdata db 01h ; songdata+000h binary flipflop pattsize db 10h ; pattern #0 (dummy placeholder) size of one uncrunched pattern=16 traxidx db 00h ; pattern #0 (dummy placeholder) track index pattidx db 01h ; pattern #0 (dummy placeholder) pattern index (off by 1 for pre-emptive dec @ start of isr ie: while vs for) patterns db 72h,32h,72h,32h,72h,32h,72h,31h ; pattern #1 nibble crunched (ie: pattern length=16!=8) and reversed from right to left (ie: first note=3!=7) db 37h,32h,72h,32h,72h,32h,72h,32h ; pattern #2 nibble crunched (ie: pattern length=16!=8) and reversed from right to left tracks db 0,1,0,1,0,0,0


Good luck !
Baudsurfer
Thanks for your effort. I’ll have a closer look in the evening but from what I managed to see in a few minutes…

Quote:
Not quite. Only if you have set midi mode status command, bank select status command, program change status command and channel select select status command, beforehand.

Could you give some more details about this? Especially about the implications of not selecting bank and program.

Every source I could find (including those linked from the Wikipedia article) describes MIDI messages as sequences of Status_byte + Data_bytes, where Status_byte has its MSB set. With a few exceptions like SysEx messages.

I did a quick test by running your sample program in Turbo Debugger and writing down the sequences of bytes being sent to 330h/331h in the form of a separate program. And it turns out on each timer tick it sends a 0x3f byte to the command port followed by 7 bytes to the data port, these ones for the first time:
Code:00 CA 20 9A 00 40 7F


I know what the italicized bytes are for (if I treat them as descried on MIDI.org) but then I see no sense in the “redundant” (?) bytes 00 and 7F. Besides, only the 4th ISR call actually starts some sound (and I guess, not for the last message). Maybe I miss something due to somewhat tricky way of message generation in your source code, I don’t know…

In fact, I guess, a small piece of code reliably producing a single well-known note that would work at your computer could give us a clue: if it worked at mine as well, I could find the difference and do a deeper search in that direction, if not, we could talk about some differences in NTVDM setup.
added on the 2016-05-02 11:43:00 by Dmitry Dmitry
Quote:

Could you give some more details about this? Especially about the implications of not selecting bank and program.

There aren't any. Not sending a bank select / program select will just play the default patch for a channel, which after just starting a program in NTVDM is of course a piano sound.
Considering the trouble you have when doing direct hardware access in real mode assembly language under an obsolete operating system, I would go back and rethink of point of the entire assignment. What is the useful skill to learn here?
added on the 2016-05-02 16:21:40 by Preacher Preacher
Dmitry,

Saga Musix answered one of your questions already.

To be thorough and iirc (being quite feverish atm) : you have all the clues you need really if you reread the thread imho. What is important to understand is that once you have assigned a sound to a channel (or a program of a sound bank to a channel in MIDI parlance), you need not anymore initialize that channel (cxh with x being channel number) anymore unless you wish to change the MIDI sample assigned to that channel.

Quote:
00 CA 20 9A 00 40 7F
I know what the italicized bytes are for (if I treat them as descried on MIDI.org) but then I see no sense in the “redundant” (?) bytes 00 and 7F


The first 00 byte is a command byte sent to the controller meaning "select status", it is meant to be sent just after byte 3fh "set UART mode on". It is in that order meant to be read as "I'm going to init something here". Depending on what you are seeking, you may call this either never (de facto piano sound already assigned to all channels except channel 10) , once (I assigned a slapbass and I'm always going to play that sound on that channel), or every time, (Yes sometimes I assign a slapbass, but sometimes I might assign a different bass to that same channel). When trying to reduce size of a program since it is already needed once it is included everytime to reduce size and also because it does not affect outcome on relatively short notes).

The last 7fh byte is the note on command I believe.

Nota Bene : there is also an 8th parameter you can pass wich is vibrato value.

In general, what I think you misunderstand is that if you play two notes sequentially on same channel yo uwill only hear last note if you do not wait long enough for the second note. This is the reason you have to rely on the Real Time Clock exclusively and not Statuses that are not always implemented perfectly (ie : play a note wait one second and play another note). You may not use RDTSC which does not work in VM86 environment (but works in pure Dos) so you therefore have to rely on dword at BDA adresss [40h:6ch] or [0:46ch] which is incremented 18.2 times/second (18.2 Hz).

And now an example (I didn't want to give you this but since you're insisting), let's say I want to play a single note (not the piano) but a chromatic percussion bank instrument called Celesta : it is listed here as instrument #9 so it's indexed value is 8. Let's say I want to play it octave/note D3, from here I can see it has decimal value 50, so 32h in hexadecimal.
Hence I can write to output sound on channel MIDI 9 :

org 100h
mov dx,331h
mov al,3fh
out dx,al
dec dx
mov al,000h
out dx,al
mov al,0c9h
out dx,al
mov al,08h
out dx,al
mov al,99h
out dx,al
mov al,32h
out dx,al
mov al,40h
out dx,al
mov al,07fh
jmp $


Quote:
Considering the trouble you have when doing direct hardware access in real mode assembly language under an obsolete operating system, I would go back and rethink of point of the entire assignment. What is the useful skill to learn here?


Several essentials to Operating system developpments (tsr, isr, ivt, rtc, timers in general, re-entrance mutex, addressing ports and general status commands messages such as DOR commands needed to address HDD of floppies for example). Some people like to learn and progress ;)
Quote:
Several essentials to Operating system developpments (tsr, isr, ivt, rtc, timers in general, re-entrance mutex, addressing ports and general status commands messages such as DOR commands needed to address HDD of floppies for example). Some people like to learn and progress ;)

I would imagine that there are better environments to learn these things than something that's not been in real use for years except in legacy systems. Learning and progression are all well and good, but aside from some truly special things, coding stuff for 16-bit DOS is regression instead of progression (and a game project would hardly teach you those things anyway). But I'll stop digressing the discussion.
added on the 2016-05-02 22:08:08 by Preacher Preacher
Baudsurfer
A-a-a-and… this short sample didn’t work for me: it successfully spins in the jmp $ loop but no sound. That is the problem I’m talking about.

And yes, I noticed the intentionally (?) skipped last out-instruction and added it. Seriously, I’m not one of those guys who ask to do my job: I did it the way it is suggested in docs, succeeded in DOSBox, failed in NTVDM, did a lot of testing, didn’t find any reasons for the behaviour and asked hoping someone might know something that is not expressed in the official documentation.

BTW, one more byte (with any value) sent after 7F in your example made it sound. That’s what I’ve noticed even before posting my question here but didn’t pay much attention to: sometimes it is not even necessary for a complete well-formed MIDI message to be sent, just enough zero bytes make it sound as well.

Quote:
The first 00 byte is a command byte sent to the controller meaning "select status", it is meant to be sent just after byte 3fh "set UART mode on".

Quote:
The last 7fh byte is the note on command I believe.

Can you tell me the source of this? I mean, I’ve read the whole “Sound Blaster series Hardware Programming Guide”, looked through the Technical reference manual for MPU-401 and have some experience writing MIDI software for Windows, but can’t remember anything like that. Having such source might give me more ideas and make my vision of the problem complete.

Docs say (if I read them correctly) that after sending 3F command and its acknowledgement one just have to send MIDI messages to the Data port. And MIDI specification says that a MIDI message is Status_byte + Data_bytes where Status_byte has its MSB set. And “Note on” command is said to be 1001 xxxx (bin). After reading the docs I’d say that bytes just get skipped until the first Status_byte (thus 00 is ignored?) and then 7F might be just a note number for the second note played using MIDI Running Status.

In fact, I can even “prove” that by removing those bytes and sending an F4 byte at the end, which is a reserved value for Status_byte that must be ignored but make an MPU-compatible device drop current running status.

Where am I wrong and where can I find more details about what you call “select status”, stuff like that?

Thanks in advance.
added on the 2016-05-02 23:01:51 by Dmitry Dmitry
Preacher
Quote:
Considering the trouble you have when doing direct hardware access in real mode assembly language under an obsolete operating system, I would go back and rethink of point of the entire assignment. What is the useful skill to learn here?

The idea is quite simple: throughout a-year-and-a-half course I try to show students that a lot of programming and design patterns that are widely used and advestised in our profession are easily applied even if your language doesn’t have them out of the box.

Real mode is taught due to its simplicity relative to other modes and according to the recommendations of our Ministry of education. They are taught to use Mode 13h, why not show them that even with these limited capabilities they can already write quite interesting things. And, rephrasing Baudsurfer, it is very useful to make them understand the way hardware really works, not through the OS-coloured glasses.
added on the 2016-05-02 23:23:58 by Dmitry Dmitry

login

Go to top