• Please review our updated Terms and Rules here

Continuing my sound card support (IMF)

I'll have to work backwards to find a frequency-to-card equation.

Here, try mine...

Code:
; Creative Music Source dual SA1099 Sound support
; Jason M. Knight, October 2014
; Intended for compilation with NASM

; labels starting with "i_" are for local/internal use only

BITS 16
CPU 8086

%include "TURBOPAS.MAC"

segment CONST

; CMS registers are read only, so we have to buffer these ourselves!
cmsOctaveStore dw 0x0000, 0x0000, 0x0000
cmsFreqBits    dw 0x0000
cmsNoiseBits   dw 0x0000

cmsFreqMap: ; convert 0..243 to 0..255 with SAA1099 freq curve
	db    0,   2,   3,   5,   7,   9,  11,  13
	db   15,  17,  19,  21,  24,  26,  28,  30
	db   32,  33,  35,  37,  39,  40,  42,  45
	db   47,  48,  50,  51,  53,  55,  57,  58
	
	db   60,  61,  63,  65,  66,  67,  68,  70
	db   71,  73,  74,  76,  77,  79,  80,  82
	db   83,  85,  86,  87,  88,  90,  91,  93
	db   95,  96,  97,  98, 100, 101, 103, 104
	
	db  105, 106, 108, 109, 111, 112, 113, 114
	db  115, 116, 118, 119, 120, 121, 123, 124
	db  125, 126, 127, 129, 130, 131, 132, 133
	db  134, 135, 136, 137, 138, 140, 141, 142
	
	db  143, 144, 145, 146, 148, 149, 150, 151
	db  152, 153, 154, 155, 156, 157, 158, 159
	db  160, 161, 162, 162, 163, 164, 165, 166
	db  167, 168, 170, 171, 172, 173, 174, 174
	
	db  175, 176, 177, 178, 179, 180, 181, 182
	db  182, 183, 184, 185, 186, 187, 188, 189
	db  190, 191, 192, 193, 193, 194, 195, 196
	db  196, 197, 198, 199, 200, 200, 201, 202
	
	db  203, 203, 204, 205, 206, 206, 208, 208
	db  209, 209, 210, 211, 212, 212, 213, 213
	db  214, 215, 216, 217, 217, 218, 218, 219
	db  219, 220, 221, 222, 222, 223, 224, 225
	
	db  225, 226, 226, 227, 227, 228, 228, 229
	db  230, 231, 232, 232, 233, 233, 234, 234
	db  235, 235, 236, 236, 237, 238, 239, 239
	db  240, 240, 241, 241, 242, 243, 243, 244
	
	db  244, 245, 245, 246, 246, 247, 247, 248
	db  249, 250, 250, 251, 251, 252, 252, 253
	db  253, 254, 254, 255
	
segment CODE

; let's do the math here instead of during execution
%define cmsFreqMin 32
%define cmsFreqMax 7823
%define cmsFreqLo  245
%define cmsFreqHi  489
%define cmsFreqMapOffsetMinusLo cmsFreqMap - cmsFreqLo

extern cmsSoundPort

i_WriteByteBoth:
; INPUT
;   dx = soundport
;   al = register
;   ah = data
; PRESERVES
;   all
	add   dx, 3
	out   dx, al
	dec   dx
	xchg  ah, al
	out   dx, al
	dec   dx
	xchg  ah, al
	out   dx, al
	dec   dx
	xchg  ah, al
	out   dx, al
	xchg  ah, al
	ret
	
i_WriteWordBoth:
; INPUT
;   dx = soundport
;   al = register
;   bx = data
; PRESERVES
;   all
	add   dx, 3
	out   dx, al ; oAL
	dec   dx
	xchg  bl, al ; BL = oAL, AL = oBL, BH = oBH
	out   dx, al ; oBL
	xchg  bh, al ; BL = oAL, AL = oBH, BH = oBL
	out   dx, al ; oBH
	dec   dx
	xchg  bl, al ; BL = oBH, AL = oAL, BH = oBL
	out   dx, al ; oAL
	dec   dx
	xchg  bh, al ; BL = oBH, AL = oBL, BH = oAL
	out   dx, al ; oBL
	xchg  bl, al ; BL = oBL, AL = oBH, BH = oAL
	out   dx, al ; oBH
	xchg  bh, al ; original order.
	ret
	
i_CmsSelectPort:
; INPUT
;   [bp + 6] == voice 0..11
; OUTPUT
;   bx = cms register (0..5)
;   dx = port (0x02?1 or 0x02?3)
	mov  bx, [bp + 6]
	mov  dx, [cmsSoundPort]
	inc  dx
	cmp  bx, 6
	js   .done
	sub  bx, 6
	inc  dx
	inc  dx
.done:
	ret
	
i_CmsFindBits:
; INPUT
;   al = register to set
;   bx = voice register 0..5
;   dx = chip register port (typically 0x02?1 or 0x02?3)
; OUTPUT
;   al == bitmask
;   bx == address of appropriate cmsFreqBits byte
;   dx == chip data port (typically 0x02?0 or 0x2?2)
; CORRUPTS
;   cl
	out  dx, al
	dec  dx
	mov  cl, bl
	mov  al, 1
	shl  al, cl
	mov  bx, cmsFreqBits
	test dx, 0x0002
	jz   .done
	inc  bx
.done:
	ret
	
i_CmsSetAL:
; INPUT
;   al = bit mask
;   bx = address of cmsBits byte
;   dx = chip data port
	or   al, [bx]
	mov  [bx], al
	out  dx, al
	ret
	
i_CmsMaskAL:
; INPUT
;   al = bit mask
;   bx = address of cmsBits byte
;   dx = chip data port
; CORRUPTS
;   al (masks off top two bits)
	not  al
	and  al, 0x3F
	and  al, [bx]
	mov  [bx], al
	out  dx, al
	ret
	
i_SetFreq:
; INPUT
;   ah = freq
;   bx = voice 0..5
;   dx = port 0x02?1 or 0x02?3
; OUTPUT
;   none
; CORRUPTS
;   al
	mov   al, bl
	or    al, 0x08
	out   dx, al
	dec   dx
	mov   al, ah
	out   dx, al
	inc   dx
	ret
	
i_SetOctave:
; INPUT
;   ch = octave
;   bx = voice 0..5
;   dx = port 0x02?1 or 0x2?3
; OUTPUT
;   none
; CORRUPTS
;   al, bx
	mov   al, bl
	shr   al, 1
	or    al, 0x10
	out   dx, al
	dec   dx
	shr   bx, 1
	mov   al, [bx + cmsOctaveStore]
	jc    .setHigh
; setLow
	and   al, 0xF0
	jmp  .done
.setHigh:
	and   al, 0x0F
	mov   cl, 4
	shl   ch, cl
.done:
	or    al, ch
	out   dx, al
	inc   dx
	mov   [bx + cmsOctaveStore], al
	ret
	
; procedure cmsReset
pProcNoArgs cmsReset
	mov   ax, ds
	mov   es, ax
	xor   ax, ax
	mov   di, cmsOctaveStore
	stosw
	stosw
	stosw
	stosw ; cmsFreqBits
	stosw ; cmsNoiseBits
	mov   dx, [cmsSoundPort]
	mov   cx, 0x20
.loopZero:
	call  i_WriteByteBoth
	inc   al
	loop  .loopZero
	mov   al, 0x1C
	mov   bx, 0x0102
	call  i_WriteWordBoth
	mov   al, 0x15
	call  i_WriteByteBoth
	dec   ax
	call  i_WriteByteBoth
	inc   ax
	inc   ax
	call  i_WriteByteBoth
	mov   cx, 6
	mov   ax, 0x8800
.loopVolume:
	call  i_WriteByteBoth
	inc   al
	loop  .loopVolume
	retf
	
; proceudre cmsSetAmplitude(left, right, voice:word);
pProcArgs cmsSetAmplitude
	call i_CmsSelectPort
	mov  al, bl
	out  dx, al
	mov  al, [bp + 8]
	mov  cl, 4
	shr  al, cl
	add  al, [bp + 10]
	dec  dx
	out  dx, al
	pRet 6
	
; procedure cmsEnableFreq(voice:word);
pProcArgs cmsEnableFreq
	call i_CmsSelectPort
	mov  al, 0x14
	call i_CmsFindBits
	call i_CmsSetAL
	pRet 2

; procedure cmsDisableFreq(voice:word);
pProcArgs cmsDisableFreq
	call i_CmsSelectPort
	mov  al, 0x14
	call i_CmsFindBits
	call i_CmsMaskAL
	pRet 2

; procedure cmsEnableNoise(voice:word);
pProcArgs cmsEnableNoise
	call i_CmsSelectPort
	mov  al, 0x15
	call i_CmsFindBits
	call i_CmsSetAL
	pRet 2
	
; procedure cmsDisableNoise(voice:word);
pProcArgs cmsDisableNoise
	call i_CmsSelectPort
	mov  al, 0x15
	call i_CmsFindBits
	call i_CmsMaskAL
	pRet 2
	
; procedure cmsSetFreq(freq, voice:word);
pProcArgs cmsSetFreq
	call  i_CmsSelectPort
	mov   ah, [bp + 8]
	call  i_SetFreq
	pRet  4
	
; procedure cmsSetOctave(octave, voice:word);
pProcArgs cmsSetOctave
	call i_CmsSelectPort
	mov   ch, [bp + 8]
	call  i_SetOctave
	pRet  4

; function cmsSetHz(hz, voice:word);
pProcArgs cmsSetHz
	; first let's turn off this voice
	call i_CmsSelectPort ; bx == register 0..5, dx == 0x2?1 or 0x2?3
	mov   di, bx
	mov   si, dx
	mov   al, 0x14
	call  i_CmsFindBits
	call  i_CmsMaskAL
	; determine freq and octave from hz
	mov   bx, [bp + 8]
	cmp   bx, cmsFreqMin
	jl    .exit
	cmp   bx, cmsFreqMax
	jg    .exit
	mov   ch, 3
	; I HATE "WHILE" LOGIC!!!
.loopDownOctave:
	cmp   bx, cmsFreqLo
	jg    .loopUpOctave
	shl   bx, 1
	dec   ch
	jmp   .loopDownOctave
.loopUpOctave:
	cmp   bx, cmsFreqHi
	jl    .calcFreq
	shr   bx, 1
	inc   ch
	jmp   .loopUpOctave
.calcFreq:
	mov   ah, [bx + cmsFreqMapOffsetMinusLo] 
	; AH = freq, CH = OCTAVE
	mov   bx, di
	mov   dx, si
	call  i_SetFreq
	call  i_SetOctave
	; turn this voice back on.
	mov   bx, di
	mov   al, 0x14
	call  i_CmsFindBits
	call  i_CmsSetAL
.exit:
	pRet  4

CMSSetHz does the trick. The "fun" part is looping the range to have the "octave" multiplier. It's not perfect, but the lookup table is the only fast way to get the frequency conversions right I've been able to find as DAMNED if I can figure out what the math for that is. Using the subtract and the lo frequency spreads the value range over a larger area for less rounding bugs -- though really since it can only accept BYTE values, do NOT expect accurate frequencies from a SA-1099. (see how many values on that scale are the same!)

The above codebase FINALLY seems to work on every twin SA1099 setup I throw at it... (including the VERY quirky ATI Stereo FX) Though any suggestions are more than welcome; was more focused on making it work on the handful of creative cards it was screwing up on (finally got one of the 1320B's it wasn't working with) than on making it as efficient as possible. Turned out it's the 'new' GAL's that are floating around (which is what my C and 2.0 had) work, the real one's didn't. :/ -- confirmed by swapping the GAL's between boards.

Though it's STILL more efficient than Adlib... I swear OPL is a more brain-dead chip than the 286.
 
Last edited:
Unfortunately, your code is not quite accurate. Looking at the CMS tech ref, it is implied that the range of allowable values are from 3 (A) to 242 (G#)... but looking at the SAA1099 datasheet, the actual input values are from 0 to 255 to hit frequencies outside of pure "A" and "G#". This means your translation table is neither accurate nor entirely necessary. Also, the CMS tech ref doesn't mention the amplitude envelope generator functionality of the SAA1099 at all (!). This continues the fine tradition of terrible programming docs I have come to expect from Creative.

Here is more complete info on the chips: http://www.sgistuff.net/mirrors/4dfaq/#appendixB

I've uploaded my copy of the datasheet here: ftp://ftp.oldskool.org/pub/misc/Hardware/Creative/saa-1099_data_sheet_OCR.pdf This is recommended as it has visual graphs of the envelope generator functions.
 
Unfortunately, your code is not quite accurate.
It IS on the CMS, and the CMS reference was in fact wrong... kind-of. As is the SAA1099 docs... The frequencies are based on the clock fed to the chip, and they calculated their values based on that clock.

Notice the output curve, I don't know how they're building their frequencies on the chip but it's NOT a straight line, not even close.

Hence why it skips fast at lower frequencies
db 0, 2, 3, 5, 7, 9, 11, 13

...and barely changes at all at high values.
db 249, 250, 250, 251, 251, 252, 252, 253
db 253, 254, 254, 255

The input values 0..255 only cover one octave, but again it's not linear -- there's a distinct curve to it. Mapping the charted curve to 245..489 as 0...243 is as close to 1:1 as is necessary for most frequencies the chip can make, then simply shift for frequencies above/below that range and change the octave value. If outputting over 1760hz and you need accuracy it might be worth doubling the table size, but it quickly becomes diminishing returns if only using word width granularity for the input frequency.

... and even if you were to try and do so, the frequency conversion rate would be a pain in the ass and you'd STILL need a table since 0..255 doesn't line up to a multiple of 55 anyways, and you need to be using a multiple of 55 since it's an ACTUAL octave multiplier... though if you wanted to use an arbitrary measurement that's NOT actually hz, it would be fine; though again without that curve it sounds really off.

I get the feeling they are doing some sort of square of the delay value or something with it, given the nature of that curve. In the previous thread I had about the CMS I was actually asking if anyone could explain that curve, and also why the numbers in the CMS and SAA1099 documentation don't even come CLOSE to the frequencies output in the implementation.

Also, the CMS tech ref doesn't mention the amplitude envelope generator functionality of the SAA1099 at all (!). This continues the fine tradition of terrible programming docs I have come to expect from Creative.
Probably because it sounded like ass Yamaha FM style whenever anybody tried to use it. You're better off leaving that little gem the hell alone. Sierra tried to use it, and that's why CMS on their games sounds worse than Tandy/jr.

But what do I know, for my ears I prefer Tandy/jr over Adlib... at least it has a pair and doesn't have a tinny buzz that makes me want to jab hot pokers in my ears. But again, I'm the heretic who says the same thing about SID and 99.99% of "ChipTunes" -- there are times I've heard PC speaker be less painful than SID/Adlib.
 
Back
Top