start on YM2612. DAC works. process port writes for FM channels. No FM channels output audio yet.

This commit is contained in:
beirich 2012-04-29 01:09:06 +00:00
parent 4e04facdd9
commit 7f1292d4f9
8 changed files with 324 additions and 19 deletions

View File

@ -291,6 +291,10 @@
<Compile Include="Properties\svnrev.cs" />
<Compile Include="QuickCollections.cs" />
<Compile Include="Sound\CDAudio.cs" />
<Compile Include="Sound\Utilities\Equalizer.cs" />
<Compile Include="Sound\YM2612.IO.cs" />
<Compile Include="Sound\YM2612.Channel.cs" />
<Compile Include="Sound\YM2612.Operator.cs" />
<Compile Include="Sound\Utilities\BufferedAsync.cs" />
<Compile Include="Sound\Utilities\Metaspu.cs" />
<Compile Include="Interfaces\IController.cs" />

View File

@ -63,8 +63,8 @@ namespace BizHawk.Emulation.Consoles.Sega
CoreOutputComm = new CoreOutputComm();
MainCPU = new MC68000();
SoundCPU = new Z80A();
YM2612 = new YM2612();
PSG = new SN76489();
YM2612 = new YM2612() { MaxVolume = 23405 };
PSG = new SN76489() { MaxVolume = 4681 };
VDP = new GenVDP();
VDP.DmaReadFrom68000 = ReadWord;
SoundMixer = new SoundMixer(YM2612, PSG);
@ -96,6 +96,7 @@ namespace BizHawk.Emulation.Consoles.Sega
Frame++;
PSG.BeginFrame(SoundCPU.TotalExecutedCycles);
YM2612.BeginFrame(SoundCPU.TotalExecutedCycles);
for (VDP.ScanLine = 0; VDP.ScanLine < 262; VDP.ScanLine++)
{
//Log.Error("VDP","FRAME {0}, SCANLINE {1}", Frame, VDP.ScanLine);
@ -123,6 +124,7 @@ namespace BizHawk.Emulation.Consoles.Sega
}
}
PSG.EndFrame(SoundCPU.TotalExecutedCycles);
YM2612.EndFrame(SoundCPU.TotalExecutedCycles);
Controller.UpdateControls(Frame++);
if (lagged)

View File

@ -139,7 +139,6 @@ namespace BizHawk.Emulation.Consoles.Sega
case 0x13:
case 0x15:
case 0x17:
Console.WriteLine("!+!+!+ PSG WRITE => {0:X2}",value);
PSG.WritePsgData((byte) value, SoundCPU.TotalExecutedCycles);
return;
}

View File

@ -16,7 +16,7 @@ namespace BizHawk.Emulation.Consoles.Sega
if (address >= 0x4000 && address < 0x6000)
{
//Console.WriteLine(" === Z80 READS FM STATUS ===");
return YM2612.ReadStatus(); // TODO: more than 1 read port probably?
return YM2612.ReadStatus(SoundCPU.TotalExecutedCycles); // TODO: more than 1 read port probably?
}
if (address >= 0x8000)
{
@ -40,7 +40,7 @@ namespace BizHawk.Emulation.Consoles.Sega
if (address >= 0x4000 && address < 0x6000)
{
//Console.WriteLine(" === Z80 WRITES YM2612 {0:X4}:{1:X2} ===",address, value);
YM2612.Write(address & 3, value);
YM2612.Write(address & 3, value, SoundCPU.TotalExecutedCycles);
return;
}
if (address < 0x6100)

View File

@ -0,0 +1,42 @@
using System;
namespace BizHawk.Emulation.Sound
{
public partial class YM2612
{
public sealed class Channel
{
public readonly Operator[] Operators;
public int Frequency;
public int Feedback;
public int Algorithm;
public bool SpecialMode; // Enables separate frequency for each operator, available on CH3 and CH6 only
// TODO. CSM. Pg6 details CSM mode.
public bool LeftOutput;
public bool RightOutput;
public int AMS_AmplitudeModulationSensitivity;
public int FMS_FrequencyModulationSensitivity;
public Channel()
{
Operators = new Operator[4];
Operators[0] = new Operator();
Operators[1] = new Operator();
Operators[2] = new Operator();
Operators[3] = new Operator();
}
//----------------------
//|Mode| Behaviour |
//|----|---------------|
//| 00 | Normal |
//| 01 | Special |
//| 10 | Special + CSM |
//| 11 | Special |
//----------------------
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
namespace BizHawk.Emulation.Sound
{
// We process TIMER writes, and also track BUSY status, immediately when writes come in.
// All other writes are queued up with a timestamp, so that we can sift through them when
// we're rendering audio for the frame.
public partial class YM2612
{
byte PartSelect;
byte RegisterSelect;
bool DacEnable;
byte DacValue;
Queue<QueuedCommand> commands = new Queue<QueuedCommand>();
/*
I might share a little quirk I discovered a few days ago, while I was working through a list of random tests unrelated to the envelope generator. I was running a test to check whether
parts 1 and 2 had a separate address register, IE, whether you could write one address to $A00000 and another to $A00002, then write to each data port, and have the two writes go to two
different register addresses in each part. What I found was that not only do parts 1 and 2 not have a separate address register, they don't even have a separate data port.
It turns out that writing to an address register stores both the written address, and the part number of the address register you wrote to. You can then write to either the data port at
$A00001, or the data port at $A00003, and the write will go to the register number you wrote, within the part of the address register you wrote to. This means you can, for example, write
an address to $A00000, then write the data to $A00003, and the data will in fact be written to the part 1 register block, not the part 2 register block.
The simpler implementation would seem to be only storing the 8-bit address data that was written, and use the data port that received the write to determine which part to write to, but
it isn't implemented this way. Writing to the address register stores 9 bits of data, indicating both the target register address, and the part number. I don't think any emulator does
this correctly right now.
*/
public byte ReadStatus(int clock)
{
// default status: not BUSY, both timers tripped
return 3;
}
public void Write(int addr, byte value, int clock)
{
if (addr == 0)
{
PartSelect = 1;
RegisterSelect = value;
return;
}
else if (addr == 2)
{
PartSelect = 2;
RegisterSelect = value;
return;
}
if (PartSelect == 1)
{
if (RegisterSelect == 0x24) { WriteTimerA_MSB_24(value, clock); return; }
if (RegisterSelect == 0x25) { WriteTimerA_LSB_25(value, clock); return; }
if (RegisterSelect == 0x26) { WriteTimerB_26(value, clock); return; }
if (RegisterSelect == 0x27) { WriteTimerControl_27(value, clock); } // don't return on this one; we process immediately AND enqueue command for port $27.
}
var cmd = new QueuedCommand { Part = PartSelect, Register = RegisterSelect, Data = value, Clock = clock-frameStartClock };
commands.Enqueue(cmd);
}
void WriteCommand(QueuedCommand cmd)
{
if (cmd.Part == 1)
Part1_WriteRegister(cmd.Register, cmd.Data, 0); // TODO remove clock 0 ?
else
Part2_WriteRegister(cmd.Register, cmd.Data);
}
void WriteTimerA_MSB_24(byte value, int clock)
{
Console.WriteLine("Timer A (msb) {0:X2}", value);
}
void WriteTimerA_LSB_25(byte value, int clock)
{
Console.WriteLine("Timer A (lsb) {0:X2}", value);
}
void WriteTimerB_26(byte value, int clock)
{
Console.WriteLine("Timer B {0:X2}", value);
}
void WriteTimerControl_27(byte value, int clock)
{
Console.WriteLine("Timer control {0:X2}", value);
}
// information on TIMER is on pg 6
void Part1_WriteRegister(byte register, byte value, int clock) // TODO remove clock?
{
switch (register)
{
case 0x22: Console.WriteLine("LFO Control {0:X2}", value); break;
case 0x24: break; // Timer A MSB, handled immediately
case 0x25: break; // Timer A LSB, handled immediately
case 0x26: break; // Timer B, handled immediately
case 0x27: Console.WriteLine("$27: Ch3 Mode / Timer Control {0:X2}", value); break; // determines if CH3 has 1 frequency or 4 frequencies.
case 0x28: Console.WriteLine("Operator Key On/Off Ctrl {0:X2}", value); break;
case 0x2A: DacValue = value; break;
case 0x2B: DacEnable = (value & 0x80) != 0; break;
case 0x2C: throw new Exception("something wrote to ym2612 port $2C!"); //http://forums.sonicretro.org/index.php?showtopic=28589
// TODO bleh the situation with op2/op3 confusing
// "In MAME OPN emulation code register pairs for multi-frequency mode are A6A2, ACA8, AEAA, ADA9".
//D7 - operator, which frequency defined by A6A2
//D6 - .. ACA8
//D5 - .. AEAA
//D4 - .. ADA9
//Where D7=op4, D6=op3, D5=op2, and D4=op1. That matches the YM2608 document. At least that's confirmed then.
// PG4 has some info on frquency calculations
default:
if (register >= 0x30 && register < 0xA0)
Console.WriteLine("P1 FM Channel data write");
else
Console.WriteLine("P1 REG {0:X2} WRITE {1:X2}", register, value); break;
}
}
void Part2_WriteRegister(byte register, byte value)
{
// NOTE. Only first bank has multi-frequency CSM/Special mode. This mode can't work on CH6.
if (register >= 0x30 && register < 0xA0)
Console.WriteLine("P2 FM Channel data write");
else
Console.WriteLine("P2 REG {0:X2} WRITE {1:X2}", register, value);
}
public class QueuedCommand
{
public byte Part;
public byte Register;
public byte Data;
public int Clock;
}
}
}

View File

@ -0,0 +1,43 @@
using System;
namespace BizHawk.Emulation.Sound
{
public partial class YM2612
{
public sealed class Operator
{
// External Settings
public int TL_TotalLevel;
public int AR_AttackRate;
public int RS_RateScaling;
public int D1R_FirstDecayRate;
public int D2R_SecondDecayRate;
public int D1L_FirstDecayLevel;
public int RR_ReleaseRate;
public int SSG_EG;
public int DT1_Detune;
public int MUL_Multiple;
public bool AM_AmplitudeModulation;
public int Frequency;
// Internal State
// ...
}
}
//TODO "the shape of the waves of the envelope changes in a exponential when attacking it, and it changes in the straight line at other rates."
// pg 8, read it
// pg 11, detailed overview of how operator works.
// pg 12, detailed description of phase generator.
//TL Total Level 7 bits
//SL Sustain Level 4 bits
//AR Attack Rate 5 bits
//DR Decay Rate 5 bits
//SR Sustain Rate 5 bits
//RR Release Rate 4 bits
//SSG-EG SSG-EG Mode 4 bits
}

View File

@ -1,24 +1,94 @@
namespace BizHawk.Emulation.Sound
{
public sealed class YM2612 : ISoundProvider
{
public byte ReadStatus()
{
// default status: not BUSY, both timers tripped
return 3;
}
using System;
using System.Diagnostics;
public void Write(int addr, byte value)
namespace BizHawk.Emulation.Sound
{
//System.Console.WriteLine("YM2612: {0:X2} -> {1:X2}", addr, value);
public sealed partial class YM2612 : ISoundProvider
{
public readonly Channel[] Channels;
int frameStartClock;
int frameEndClock;
public YM2612()
{
Channels = new Channel[6];
Channels[0] = new Channel();
Channels[1] = new Channel();
Channels[2] = new Channel();
Channels[3] = new Channel();
Channels[4] = new Channel();
Channels[5] = new Channel();
}
public void Reset()
{
}
public void BeginFrame(int clock)
{
frameStartClock = clock;
}
public void EndFrame(int clock)
{
frameEndClock = clock;
}
public void DiscardSamples() { }
public void GetSamples(short[] samples) {}
public int MaxVolume { get; set; }
public void GetSamples(short[] samples)
{
int elapsedCycles = frameEndClock - frameStartClock;
int start = 0;
while (commands.Count > 0)
{
var cmd = commands.Dequeue();
int pos = ((cmd.Clock * samples.Length) / elapsedCycles) & ~1;
GetSamplesImmediate(samples, start, pos - start);
start = pos;
WriteCommand(cmd);
}
GetSamplesImmediate(samples, start, samples.Length - start);
}
void GetSamplesImmediate(short[] samples, int pos, int length)
{
int channelVolume = MaxVolume / 6;
for (int i=0; i<length/2; i++)
{
// TODO: channels 1-5
// TODO, non-DAC
if (DacEnable)
{
short dacValue = (short)(((DacValue-80) * channelVolume) / 80);
samples[pos] += dacValue;
samples[pos + 1] += dacValue;
}
pos += 2;
}
}
// Pg 8 major post w/ info on clock rates, envelope generator, loudness
// Note: part about EG clock is wrong, see pg 12. :|
// pg 27 contains further sets of corrections to previous pages. Jesus. Including data on SSG-EG.
// pg 32 contains some details about the LFO
// pg 33 paul jensens contains frequency conversion. gmaniac corrected.
// pg 33 blargg comments on DAC size - depends on if I WANT to emulate the inaccuracies of the DAC or a hypotheticaly perfect YM2612. more on DAC on page 37 - contradicting blargg.
/*
Chilly Willy wrote:
Hmm - does the FM channel still update even if it is set to PCM?
Yes, it does. FM Channel 6 and Reg $2A are independent of each other. "DAC mode" (Reg $2B) only chose output of one of them. If it is in "FM Mode" it outputs Channel 6 output. If it is in "DAC Mode", it outputs value stored in Reg $2A and normalized to scale of DAC (-512 to + 512). Normalization formula is
(v - $80) * 4
where v = ($2A).
I tested in on HW.
*/
}
}