Skip to content

Commit e7515f6

Browse files
authored
Refactor OPL constants and improve comments; add internal tests for operator behavior; fix issue with E0 register writes (#1)
1 parent 485bcd6 commit e7515f6

File tree

5 files changed

+165
-25
lines changed

5 files changed

+165
-25
lines changed

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
12
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

opal.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ package opl2
2525

2626
// Various constants
2727
const (
28-
OPL3SampleRate = 49716
28+
OPL3SampleRate = RoundedOPLRATE
2929

3030
NumChannels = 18
3131
NumOperators = 36
@@ -443,9 +443,9 @@ func (o *operator) SetWaveform(wave uint16) {
443443
//
444444
// Rof is set as follows depending on the KSR setting:
445445
//
446-
// Key scale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
447-
// KSR = 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3
448-
// KSR = 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
446+
// Key scale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
447+
// KSR = 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3
448+
// KSR = 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
449449
//
450450
// Note: zero rates are infinite, and are treated separately elsewhere
451451
func (o *operator) ComputeRates() {

operator.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ package opl2
3636
//DUNNO Keyon in 4op, switch to 2op without keyoff.
3737
*/
3838

39-
//Masks for operator 20 values
39+
// Masks for operator 20 values
4040
const (
4141
cMaskKSR = 0x10
4242
cMaskSustain = 0x20
@@ -122,7 +122,7 @@ func (o *Operator) SetupOperator() {
122122
}
123123

124124
// UpdateAttack updates the attack rate on the envelope
125-
//We zero out when rate == 0
125+
// We zero out when rate == 0
126126
func (o *Operator) UpdateAttack(chip *Chip) {
127127
rate := uint8(o.reg60 >> 4)
128128
if rate != 0 {
@@ -305,7 +305,7 @@ func (o *Operator) Write80(chip *Chip, val uint8) {
305305

306306
// WriteE0 writes data to register 0xE0 on the operator
307307
func (o *Operator) WriteE0(chip *Chip, val uint8) {
308-
if (o.regE0 ^ val) != 0 {
308+
if (o.regE0 ^ val) == 0 {
309309
return
310310
}
311311
//in opl3 mode you can always selet 7 waveforms regardless of waveformselect

opl2.go

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ const (
4848
)
4949

5050
const (
51+
OPLClock4 = 14318180 //Hz
52+
OPLPrecision = 288 // Amount of precision for internal calculations
53+
// (9 channels * 8 bits * 4x oversampling to support OPL3's 18 channels)
54+
5155
// OPLRATE is the sampling rate that the OPL2/3 outputs samples at, normally
5256
// all internal calculations are defined by it.
53-
OPLRATE = 14318180.0 / 288.0
54-
57+
OPLRATE = float64(OPLClock4) / float64(OPLPrecision)
58+
RoundedOPLRATE = (OPLClock4 + OPLPrecision - 1) / OPLPrecision
5559
cTremoloTableSize = 52
5660

5761
//Try to use most precision for frequencies
@@ -107,7 +111,7 @@ func init() {
107111
}
108112
}
109113

110-
//How much to substract from the base value for the final attenuation
114+
// How much to substract from the base value for the final attenuation
111115
var cKslCreateTable = [16]uint8{
112116
//0 will always be be lower than 7 * 8
113117
64, 32, 24, 19,
@@ -125,15 +129,15 @@ var cFreqCreateTable = [16]uint32{
125129
m1(8), m1(9), m1(10), m1(10), m1(12), m1(12), m1(15), m1(15),
126130
}
127131

128-
//We're not including the highest attack rate, that gets a special value
132+
// We're not including the highest attack rate, that gets a special value
129133
var cAttackSamplesTable = [13]uint8{
130134
69, 55, 46, 40,
131135
35, 29, 23, 20,
132136
19, 15, 11, 10,
133137
9,
134138
}
135139

136-
//On a real opl these values take 8 samples to reach and are based upon larger tables
140+
// On a real opl these values take 8 samples to reach and are based upon larger tables
137141
var cEnvelopeIncreaseTable = [13]uint8{
138142
4, 5, 6, 7,
139143
8, 10, 12, 14,
@@ -143,7 +147,7 @@ var cEnvelopeIncreaseTable = [13]uint8{
143147

144148
var cExpTable = make([]uint16, 256)
145149

146-
//PI table used by WAVEHANDLER
150+
// PI table used by WAVEHANDLER
147151
var cSinTable = make([]uint16, 512)
148152

149153
//Layout of the waveform table in 512 entry intervals
@@ -157,19 +161,19 @@ var cSinTable = make([]uint16, 512)
157161

158162
var cWaveTable = make([]int16, 8*512)
159163

160-
//Distance into WaveTable the wave starts
164+
// Distance into WaveTable the wave starts
161165
var cWaveBaseTable = [8]uint16{
162166
0x000, 0x200, 0x200, 0x800,
163167
0xa00, 0xc00, 0x100, 0x400,
164168
}
165169

166-
//Mask the counter with this
170+
// Mask the counter with this
167171
var cWaveMaskTable = [8]uint16{
168172
1023, 1023, 511, 511,
169173
1023, 1023, 512, 1023,
170174
}
171175

172-
//Where to start the counter on at keyon
176+
// Where to start the counter on at keyon
173177
var cWaveStartTable = [8]uint16{
174178
512, 0, 0, 0,
175179
0, 512, 512, 256,
@@ -180,23 +184,23 @@ var cMulTable = make([]uint16, 384)
180184
var cKslTable = make([]uint8, 8*16)
181185
var cTremoloTable = make([]uint8, cTremoloTableSize)
182186

183-
//Start of a channel behind the chip struct start
187+
// Start of a channel behind the chip struct start
184188
var cChanOffsetTable = make([]uint16, 32)
185189

186-
//The lower bits are the shift of the operator vibrato value
187-
//The highest bit is right shifted to generate -1 or 0 for negation
188-
//So taking the highest input value of 7 this gives 3, 7, 3, 0, -3, -7, -3, 0
190+
// The lower bits are the shift of the operator vibrato value
191+
// The highest bit is right shifted to generate -1 or 0 for negation
192+
// So taking the highest input value of 7 this gives 3, 7, 3, 0, -3, -7, -3, 0
189193
var cVibratoTable = [8]int8{
190194
1 - 0x00, 0 - 0x00, 1 - 0x00, 30 - 0x00,
191195
1 - 0x80, 0 - 0x80, 1 - 0x80, 30 - 0x80,
192196
}
193197

194-
//Shift strength for the ksl value determined by ksl strength
198+
// Shift strength for the ksl value determined by ksl strength
195199
var cKslShiftTable = [4]uint8{
196200
31, 1, 2, 0,
197201
}
198202

199-
//Generate a table index and table shift value using input value from a selected rate
203+
// Generate a table index and table shift value using input value from a selected rate
200204
func envelopeSelect(val uint8) (index uint8, shift uint8) {
201205
if val < 13*4 { //Rate 0 - 12
202206
shift = 12 - (val >> 2)
@@ -211,9 +215,7 @@ func envelopeSelect(val uint8) (index uint8, shift uint8) {
211215
return
212216
}
213217

214-
/*
215-
Generate the different waveforms out of the sine/exponetial table using handlers
216-
*/
218+
// Generate the different waveforms out of the sine/exponetial table using handlers
217219
func makeVolume(wave int, volume int) int {
218220
total := wave + volume
219221
index := total & 0xff

opl2_internal_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package opl2
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// Use the native OPL rate for deterministic table values in tests.
8+
const (
9+
testRate = RoundedOPLRATE
10+
)
11+
12+
// Test that waveform writes propagate to the operator (reg E0) and refresh
13+
// the derived waveform start/mask data. This guards the regression where the
14+
// register change early-returned and never updated the operator state.
15+
func TestOperatorWaveformWriteUpdates(t *testing.T) {
16+
chip := NewChip(testRate, false)
17+
// Enable waveform selection (otherwise waveFormMask would clamp values).
18+
chip.WriteReg(0x01, 0x20)
19+
20+
op := chip.GetOperatorByIndex(0)
21+
if op == nil {
22+
t.Fatalf("nil operator returned")
23+
}
24+
initialStart := op.waveStart
25+
26+
chip.WriteReg(0xE0, 0x01) // select waveform 1
27+
28+
expectedStart := int(cWaveStartTable[1]) << cWaveSh
29+
if op.waveStart != expectedStart {
30+
t.Fatalf("waveStart not updated: got %d, want %d (initial %d)", op.waveStart, expectedStart, initialStart)
31+
}
32+
if op.regE0 != 0x01 {
33+
t.Fatalf("regE0 not stored: got 0x%02x, want 0x01", op.regE0)
34+
}
35+
}
36+
37+
// Validate percussion key-on/off handling driven by register BD toggles.
38+
// Ensures bass drum key state tracks the BD bit correctly when percussion mode is enabled.
39+
func TestPercussionKeyOnOff(t *testing.T) {
40+
chip := NewChip(testRate, false)
41+
42+
// Enable percussion mode without bass drum key-on.
43+
chip.WriteReg(0xBD, 0x20)
44+
if chip.ch[6].op[0].keyOn != 0 || chip.ch[6].op[1].keyOn != 0 {
45+
t.Fatalf("bass drum keyOn should be cleared after enabling percussion: op0=%02x op1=%02x", chip.ch[6].op[0].keyOn, chip.ch[6].op[1].keyOn)
46+
}
47+
48+
// Turn on bass drum bit.
49+
chip.WriteReg(0xBD, 0x30)
50+
if chip.ch[6].op[0].keyOn&0x2 == 0 || chip.ch[6].op[1].keyOn&0x2 == 0 {
51+
t.Fatalf("bass drum keyOn not set: op0=%02x op1=%02x", chip.ch[6].op[0].keyOn, chip.ch[6].op[1].keyOn)
52+
}
53+
54+
// Turn off bass drum bit while keeping percussion enabled.
55+
chip.WriteReg(0xBD, 0x20)
56+
if chip.ch[6].op[0].keyOn != 0 || chip.ch[6].op[1].keyOn != 0 {
57+
t.Fatalf("bass drum keyOn not cleared: op0=%02x op1=%02x", chip.ch[6].op[0].keyOn, chip.ch[6].op[1].keyOn)
58+
}
59+
60+
// Disable percussion entirely; state should remain cleared.
61+
chip.WriteReg(0xBD, 0x00)
62+
if chip.ch[6].op[0].keyOn != 0 || chip.ch[6].op[1].keyOn != 0 {
63+
t.Fatalf("bass drum keyOn not cleared after percussion disable: op0=%02x op1=%02x", chip.ch[6].op[0].keyOn, chip.ch[6].op[1].keyOn)
64+
}
65+
}
66+
67+
// Verify 4-operator synth mode selection matches the bit combinations in
68+
// regC0 for paired channels when OPL3 is active.
69+
func TestFourOpSynthSelection(t *testing.T) {
70+
chip := NewChip(testRate, true)
71+
// Enable OPL3 features explicitly; Setup leaves opl3Active disabled.
72+
chip.WriteReg(0x105, 0x01)
73+
if chip.opl3Active == 0 {
74+
t.Fatalf("opl3Active not enabled")
75+
}
76+
77+
// Enable 4-op on logical channels 0/3 (bit 0).
78+
chip.WriteReg(0x104, 0x01)
79+
80+
ch0 := chip.GetChannelByIndex(0) // logical channel 0
81+
ch1 := chip.GetChannelByIndex(3) // logical channel 3 (pair with bit 0)
82+
if ch0 == nil || ch1 == nil {
83+
t.Fatalf("failed to get four-op channel pair")
84+
}
85+
86+
// Set synth bits to pattern 01 -> sm3FMAM for the paired channels.
87+
ch0.WriteC0(chip, 0x02) // bit0 = 0 (change!=0)
88+
ch1.WriteC0(chip, 0x03) // bit0 = 1 (change!=0)
89+
90+
if ch0.synthHandler < sm4Start {
91+
t.Fatalf("four-op handler not selected: got %v (ch0=0x%02x ch1=0x%02x)", ch0.synthHandler, ch0.regC0, ch1.regC0)
92+
}
93+
}
94+
95+
// Confirm WriteAddr maps ports correctly, matching the C++ helper logic.
96+
func TestWriteAddrMapping(t *testing.T) {
97+
opl2Chip := NewChip(testRate, false)
98+
opl3Chip := NewChip(testRate, true)
99+
opl3Chip.WriteReg(0x105, 0x01) // enable opl3Active
100+
101+
if got := opl2Chip.WriteAddr(0, 0xAA); got != 0xAA {
102+
t.Fatalf("opl2 port0 mapping mismatch: got 0x%x", got)
103+
}
104+
if got := opl2Chip.WriteAddr(2, 0x05); got != 0x105 {
105+
t.Fatalf("opl2 port2 special mapping mismatch: got 0x%x", got)
106+
}
107+
if got := opl2Chip.WriteAddr(2, 0x06); got != 0x06 {
108+
t.Fatalf("opl2 port2 normal mapping mismatch: got 0x%x", got)
109+
}
110+
if got := opl3Chip.WriteAddr(2, 0x11); got != 0x111 {
111+
t.Fatalf("opl3 port2 mapping mismatch: got 0x%x", got)
112+
}
113+
if got := opl2Chip.WriteAddr(1, 0x22); got != 0 {
114+
t.Fatalf("unexpected mapping for unused port: got 0x%x", got)
115+
}
116+
}
117+
118+
// Ensure block generation does not panic and produces the expected frame counts
119+
// for mono (OPL2) and stereo (OPL3) output buffers.
120+
func TestGenerateBlockSizes(t *testing.T) {
121+
samples := uint(8)
122+
123+
opl2Chip := NewChip(testRate, false)
124+
opl2Buf := make([]int32, samples)
125+
opl2Chip.GenerateBlock2(samples, opl2Buf)
126+
127+
opl3Chip := NewChip(testRate, true)
128+
opl3Buf := make([]int32, samples*2)
129+
opl3Chip.GenerateBlock3(samples, opl3Buf)
130+
131+
if len(opl2Buf) != int(samples) {
132+
t.Fatalf("opl2 buffer length changed: got %d", len(opl2Buf))
133+
}
134+
if len(opl3Buf) != int(samples*2) {
135+
t.Fatalf("opl3 buffer length changed: got %d", len(opl3Buf))
136+
}
137+
}

0 commit comments

Comments
 (0)