661 lines
19 KiB
C#
661 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
|
|
using BizHawk.Common.BufferExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
using BizHawk.Common;
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
|
|
{
|
|
/// <summary>
|
|
/// a gameboy/gameboy color emulator wrapped around native C++ libgambatte
|
|
/// </summary>
|
|
[CoreAttributes(
|
|
"Gambatte",
|
|
"",
|
|
isPorted: true,
|
|
isReleased: true,
|
|
portedVersion: "SVN 344",
|
|
portedUrl: "http://gambatte.sourceforge.net/"
|
|
)]
|
|
[ServiceNotApplicable(typeof(IDriveLight), typeof(IDriveLight))]
|
|
public partial class Gameboy : IEmulator, IVideoProvider, ISyncSoundProvider, ISaveRam, IStatable, IInputPollable,
|
|
IDebuggable, ISettable<Gameboy.GambatteSettings, Gameboy.GambatteSyncSettings>
|
|
{
|
|
/// <summary>
|
|
/// the nominal length of one frame
|
|
/// </summary>
|
|
private const uint TICKSINFRAME = 35112;
|
|
|
|
/// <summary>
|
|
/// number of ticks per second
|
|
/// </summary>
|
|
private const uint TICKSPERSECOND = 2097152;
|
|
|
|
/// <summary>
|
|
/// keep a copy of the input callback delegate so it doesn't get GCed
|
|
/// </summary>
|
|
LibGambatte.InputGetter InputCallback;
|
|
|
|
/// <summary>
|
|
/// whatever keys are currently depressed
|
|
/// </summary>
|
|
LibGambatte.Buttons CurrentButtons = 0;
|
|
|
|
#region RTC
|
|
|
|
/// <summary>
|
|
/// RTC time when emulation begins.
|
|
/// </summary>
|
|
uint zerotime = 0;
|
|
|
|
/// <summary>
|
|
/// if true, RTC will run off of real elapsed time
|
|
/// </summary>
|
|
bool real_rtc_time = false;
|
|
|
|
LibGambatte.RTCCallback TimeCallback;
|
|
|
|
static long GetUnixNow()
|
|
{
|
|
// because internally the RTC works off of relative time, we don't need to base
|
|
// this off of any particular canonical epoch.
|
|
return DateTime.UtcNow.Ticks / 10000000L - 60000000000L;
|
|
}
|
|
|
|
uint GetCurrentTime()
|
|
{
|
|
if (real_rtc_time)
|
|
{
|
|
return (uint)GetUnixNow();
|
|
}
|
|
else
|
|
{
|
|
ulong fn = (ulong)Frame;
|
|
// as we're exactly tracking cpu cycles, this can be pretty accurate
|
|
fn *= 4389;
|
|
fn /= 262144;
|
|
fn += zerotime;
|
|
return (uint)fn;
|
|
}
|
|
}
|
|
|
|
uint GetInitialTime()
|
|
{
|
|
if (real_rtc_time)
|
|
return (uint)GetUnixNow();
|
|
else
|
|
// setting the initial boot time to 0 will cause our zerotime
|
|
// to function as an initial offset, which is what we want
|
|
return 0;
|
|
}
|
|
|
|
#endregion
|
|
|
|
[CoreConstructor("GB", "GBC")]
|
|
public Gameboy(CoreComm comm, GameInfo game, byte[] file, object Settings, object SyncSettings, bool deterministic)
|
|
{
|
|
var ser = new BasicServiceProvider(this);
|
|
ser.Register<IDisassemblable>(new GBDisassembler());
|
|
ServiceProvider = ser;
|
|
Tracer = new TraceBuffer();
|
|
ser.Register<ITraceable>(Tracer);
|
|
InitMemoryCallbacks();
|
|
CoreComm = comm;
|
|
|
|
comm.VsyncNum = 262144;
|
|
comm.VsyncDen = 4389;
|
|
comm.RomStatusAnnotation = null;
|
|
comm.RomStatusDetails = null;
|
|
comm.NominalWidth = 160;
|
|
comm.NominalHeight = 144;
|
|
|
|
ThrowExceptionForBadRom(file);
|
|
BoardName = MapperName(file);
|
|
|
|
DeterministicEmulation = deterministic;
|
|
|
|
GambatteState = LibGambatte.gambatte_create();
|
|
|
|
if (GambatteState == IntPtr.Zero)
|
|
throw new InvalidOperationException("gambatte_create() returned null???");
|
|
|
|
try
|
|
{
|
|
this._syncSettings = (GambatteSyncSettings)SyncSettings ?? new GambatteSyncSettings();
|
|
// copy over non-loadflag syncsettings now; they won't take effect if changed later
|
|
zerotime = (uint)this._syncSettings.RTCInitialTime;
|
|
real_rtc_time = DeterministicEmulation ? false : this._syncSettings.RealTimeRTC;
|
|
|
|
LibGambatte.LoadFlags flags = 0;
|
|
|
|
if (this._syncSettings.ForceDMG)
|
|
flags |= LibGambatte.LoadFlags.FORCE_DMG;
|
|
if (this._syncSettings.GBACGB)
|
|
flags |= LibGambatte.LoadFlags.GBA_CGB;
|
|
if (this._syncSettings.MulticartCompat)
|
|
flags |= LibGambatte.LoadFlags.MULTICART_COMPAT;
|
|
|
|
if (LibGambatte.gambatte_load(GambatteState, file, (uint)file.Length, GetCurrentTime(), flags) != 0)
|
|
throw new InvalidOperationException("gambatte_load() returned non-zero (is this not a gb or gbc rom?)");
|
|
|
|
// set real default colors (before anyone mucks with them at all)
|
|
PutSettings((GambatteSettings)Settings ?? new GambatteSettings());
|
|
|
|
InitSound();
|
|
|
|
Frame = 0;
|
|
LagCount = 0;
|
|
IsLagFrame = false;
|
|
|
|
InputCallback = new LibGambatte.InputGetter(ControllerCallback);
|
|
|
|
LibGambatte.gambatte_setinputgetter(GambatteState, InputCallback);
|
|
|
|
InitMemoryDomains();
|
|
|
|
CoreComm.RomStatusDetails = string.Format("{0}\r\nSHA1:{1}\r\nMD5:{2}\r\n",
|
|
game.Name,
|
|
file.HashSHA1(),
|
|
file.HashMD5());
|
|
|
|
{
|
|
byte[] buff = new byte[32];
|
|
LibGambatte.gambatte_romtitle(GambatteState, buff);
|
|
string romname = System.Text.Encoding.ASCII.GetString(buff);
|
|
Console.WriteLine("Core reported rom name: {0}", romname);
|
|
}
|
|
|
|
TimeCallback = new LibGambatte.RTCCallback(GetCurrentTime);
|
|
LibGambatte.gambatte_setrtccallback(GambatteState, TimeCallback);
|
|
|
|
NewSaveCoreSetBuff();
|
|
}
|
|
catch
|
|
{
|
|
Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public IEmulatorServiceProvider ServiceProvider { get; private set; }
|
|
|
|
#region ALL SAVESTATEABLE STATE GOES HERE
|
|
|
|
/// <summary>
|
|
/// internal gambatte state
|
|
/// </summary>
|
|
internal IntPtr GambatteState = IntPtr.Zero;
|
|
|
|
public int Frame { get; set; }
|
|
public int LagCount { get; set; }
|
|
public bool IsLagFrame { get; private set; }
|
|
|
|
// all cycle counts are relative to a 2*1024*1024 mhz refclock
|
|
|
|
/// <summary>
|
|
/// total cycles actually executed
|
|
/// </summary>
|
|
private ulong _cycleCount = 0;
|
|
|
|
/// <summary>
|
|
/// number of extra cycles we overran in the last frame
|
|
/// </summary>
|
|
private uint frameOverflow = 0;
|
|
public ulong CycleCount { get { return _cycleCount; } }
|
|
|
|
#endregion
|
|
|
|
#region controller
|
|
|
|
public static readonly ControllerDefinition GbController = new ControllerDefinition
|
|
{
|
|
Name = "Gameboy Controller",
|
|
BoolButtons =
|
|
{
|
|
"Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power"
|
|
}
|
|
};
|
|
|
|
public ControllerDefinition ControllerDefinition
|
|
{
|
|
get { return GbController; }
|
|
}
|
|
|
|
public IController Controller { get; set; }
|
|
|
|
LibGambatte.Buttons ControllerCallback()
|
|
{
|
|
InputCallbacks.Call();
|
|
IsLagFrame = false;
|
|
return CurrentButtons;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// true if the emulator is currently emulating CGB
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool IsCGBMode()
|
|
{
|
|
return (LibGambatte.gambatte_iscgb(GambatteState));
|
|
}
|
|
|
|
private InputCallbackSystem _inputCallbacks = new InputCallbackSystem();
|
|
// low priority TODO: due to certain aspects of the core implementation,
|
|
// we don't smartly use the ActiveChanged event here.
|
|
public IInputCallbackSystem InputCallbacks { get { return _inputCallbacks; } }
|
|
|
|
/// <summary>
|
|
/// for use in dual core
|
|
/// </summary>
|
|
/// <param name="ics"></param>
|
|
public void ConnectInputCallbackSystem(InputCallbackSystem ics)
|
|
{
|
|
_inputCallbacks = ics;
|
|
}
|
|
|
|
internal void FrameAdvancePrep()
|
|
{
|
|
Frame++;
|
|
|
|
// update our local copy of the controller data
|
|
CurrentButtons = 0;
|
|
|
|
if (Controller["Up"])
|
|
CurrentButtons |= LibGambatte.Buttons.UP;
|
|
if (Controller["Down"])
|
|
CurrentButtons |= LibGambatte.Buttons.DOWN;
|
|
if (Controller["Left"])
|
|
CurrentButtons |= LibGambatte.Buttons.LEFT;
|
|
if (Controller["Right"])
|
|
CurrentButtons |= LibGambatte.Buttons.RIGHT;
|
|
if (Controller["A"])
|
|
CurrentButtons |= LibGambatte.Buttons.A;
|
|
if (Controller["B"])
|
|
CurrentButtons |= LibGambatte.Buttons.B;
|
|
if (Controller["Select"])
|
|
CurrentButtons |= LibGambatte.Buttons.SELECT;
|
|
if (Controller["Start"])
|
|
CurrentButtons |= LibGambatte.Buttons.START;
|
|
|
|
// the controller callback will set this to false if it actually gets called during the frame
|
|
IsLagFrame = true;
|
|
|
|
if (Controller["Power"])
|
|
LibGambatte.gambatte_reset(GambatteState, GetCurrentTime());
|
|
|
|
if (Tracer.Enabled)
|
|
tracecb = MakeTrace;
|
|
else
|
|
tracecb = null;
|
|
LibGambatte.gambatte_settracecallback(GambatteState, tracecb);
|
|
}
|
|
|
|
internal void FrameAdvancePost()
|
|
{
|
|
if (IsLagFrame)
|
|
LagCount++;
|
|
|
|
if (endofframecallback != null)
|
|
endofframecallback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
|
|
}
|
|
|
|
public void FrameAdvance(bool render, bool rendersound)
|
|
{
|
|
FrameAdvancePrep();
|
|
if (_syncSettings.EqualLengthFrames)
|
|
{
|
|
while (true)
|
|
{
|
|
// target number of samples to emit: length of 1 frame minus whatever overflow
|
|
uint samplesEmitted = TICKSINFRAME - frameOverflow;
|
|
System.Diagnostics.Debug.Assert(samplesEmitted * 2 <= soundbuff.Length);
|
|
if (LibGambatte.gambatte_runfor(GambatteState, soundbuff, ref samplesEmitted) > 0)
|
|
LibGambatte.gambatte_blitto(GambatteState, VideoBuffer, 160);
|
|
|
|
// account for actual number of samples emitted
|
|
_cycleCount += (ulong)samplesEmitted;
|
|
frameOverflow += samplesEmitted;
|
|
|
|
if (rendersound && !Muted)
|
|
{
|
|
ProcessSound((int)samplesEmitted);
|
|
}
|
|
|
|
if (frameOverflow >= TICKSINFRAME)
|
|
{
|
|
frameOverflow -= TICKSINFRAME;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// target number of samples to emit: always 59.7fps
|
|
// runfor() always ends after creating a video frame, so sync-up is guaranteed
|
|
// when the display has been off, some frames can be markedly shorter than expected
|
|
uint samplesEmitted = TICKSINFRAME;
|
|
if (LibGambatte.gambatte_runfor(GambatteState, soundbuff, ref samplesEmitted) > 0)
|
|
LibGambatte.gambatte_blitto(GambatteState, VideoBuffer, 160);
|
|
|
|
_cycleCount += (ulong)samplesEmitted;
|
|
frameOverflow = 0;
|
|
if (rendersound && !Muted)
|
|
{
|
|
ProcessSound((int)samplesEmitted);
|
|
}
|
|
}
|
|
|
|
if (rendersound && !Muted)
|
|
ProcessSoundEnd();
|
|
|
|
FrameAdvancePost();
|
|
}
|
|
|
|
static string MapperName(byte[] romdata)
|
|
{
|
|
switch (romdata[0x147])
|
|
{
|
|
case 0x00: return "Plain ROM"; // = PLAIN; break;
|
|
case 0x01: return "MBC1 ROM"; // = MBC1; break;
|
|
case 0x02: return "MBC1 ROM+RAM"; // = MBC1; break;
|
|
case 0x03: return "MBC1 ROM+RAM+BATTERY"; // = MBC1; break;
|
|
case 0x05: return "MBC2 ROM"; // = MBC2; break;
|
|
case 0x06: return "MBC2 ROM+BATTERY"; // = MBC2; break;
|
|
case 0x08: return "Plain ROM+RAM"; // = PLAIN; break;
|
|
case 0x09: return "Plain ROM+RAM+BATTERY"; // = PLAIN; break;
|
|
case 0x0F: return "MBC3 ROM+TIMER+BATTERY"; // = MBC3; break;
|
|
case 0x10: return "MBC3 ROM+TIMER+RAM+BATTERY"; // = MBC3; break;
|
|
case 0x11: return "MBC3 ROM"; // = MBC3; break;
|
|
case 0x12: return "MBC3 ROM+RAM"; // = MBC3; break;
|
|
case 0x13: return "MBC3 ROM+RAM+BATTERY"; // = MBC3; break;
|
|
case 0x19: return "MBC5 ROM"; // = MBC5; break;
|
|
case 0x1A: return "MBC5 ROM+RAM"; // = MBC5; break;
|
|
case 0x1B: return "MBC5 ROM+RAM+BATTERY"; // = MBC5; break;
|
|
case 0x1C: return "MBC5 ROM+RUMBLE"; // = MBC5; break;
|
|
case 0x1D: return "MBC5 ROM+RUMBLE+RAM"; // = MBC5; break;
|
|
case 0x1E: return "MBC5 ROM+RUMBLE+RAM+BATTERY"; // = MBC5; break;
|
|
case 0xFF: return "HuC1 ROM+RAM+BATTERY"; // = HUC1; break;
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// throw exception with intelligible message on some kinds of bad rom
|
|
/// </summary>
|
|
/// <param name="romdata"></param>
|
|
static void ThrowExceptionForBadRom(byte[] romdata)
|
|
{
|
|
if (romdata.Length < 0x148)
|
|
throw new ArgumentException("ROM is far too small to be a valid GB\\GBC rom!");
|
|
|
|
switch (romdata[0x147])
|
|
{
|
|
case 0x00: break;
|
|
case 0x01: break;
|
|
case 0x02: break;
|
|
case 0x03: break;
|
|
case 0x05: break;
|
|
case 0x06: break;
|
|
case 0x08: break;
|
|
case 0x09: break;
|
|
|
|
case 0x0b: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
|
|
case 0x0c: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
|
|
case 0x0d: throw new UnsupportedGameException("\"MM01\" Mapper not supported!");
|
|
|
|
case 0x0f: break;
|
|
case 0x10: break;
|
|
case 0x11: break;
|
|
case 0x12: break;
|
|
case 0x13: break;
|
|
|
|
case 0x15: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
|
|
case 0x16: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
|
|
case 0x17: throw new UnsupportedGameException("\"MBC4\" Mapper not supported!");
|
|
|
|
case 0x19: break;
|
|
case 0x1a: break;
|
|
case 0x1b: break;
|
|
case 0x1c: break; // rumble
|
|
case 0x1d: break; // rumble
|
|
case 0x1e: break; // rumble
|
|
|
|
case 0x20: throw new UnsupportedGameException("\"MBC6\" Mapper not supported!");
|
|
case 0x22: throw new UnsupportedGameException("\"MBC7\" Mapper not supported!");
|
|
|
|
case 0xfc: throw new UnsupportedGameException("\"Pocket Camera\" Mapper not supported!");
|
|
case 0xfd: throw new UnsupportedGameException("\"Bandai TAMA5\" Mapper not supported!");
|
|
case 0xfe: throw new UnsupportedGameException("\"HuC3\" Mapper not supported!");
|
|
case 0xff: break;
|
|
default: throw new UnsupportedGameException(string.Format("Unknown mapper: {0:x2}", romdata[0x147]));
|
|
}
|
|
return;
|
|
}
|
|
|
|
public string SystemId { get { return "GB"; } }
|
|
|
|
public string BoardName { get; private set; }
|
|
|
|
public bool DeterministicEmulation { get; private set; }
|
|
|
|
public void ResetCounters()
|
|
{
|
|
Frame = 0;
|
|
LagCount = 0;
|
|
IsLagFrame = false;
|
|
// reset frame counters is meant to "re-zero" emulation time wherever it was
|
|
// so these should be reset as well
|
|
_cycleCount = 0;
|
|
frameOverflow = 0;
|
|
}
|
|
|
|
public CoreComm CoreComm { get; set; }
|
|
|
|
#region ppudebug
|
|
|
|
public bool GetGPUMemoryAreas(out IntPtr vram, out IntPtr bgpal, out IntPtr sppal, out IntPtr oam)
|
|
{
|
|
IntPtr _vram = IntPtr.Zero;
|
|
IntPtr _bgpal = IntPtr.Zero;
|
|
IntPtr _sppal = IntPtr.Zero;
|
|
IntPtr _oam = IntPtr.Zero;
|
|
int unused = 0;
|
|
if (!LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.vram, ref _vram, ref unused)
|
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.bgpal, ref _bgpal, ref unused)
|
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.sppal, ref _sppal, ref unused)
|
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.oam, ref _oam, ref unused))
|
|
{
|
|
vram = IntPtr.Zero;
|
|
bgpal = IntPtr.Zero;
|
|
sppal = IntPtr.Zero;
|
|
oam = IntPtr.Zero;
|
|
return false;
|
|
}
|
|
vram = _vram;
|
|
bgpal = _bgpal;
|
|
sppal = _sppal;
|
|
oam = _oam;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="lcdc">current value of register $ff40 (LCDC)</param>
|
|
public delegate void ScanlineCallback(int lcdc);
|
|
|
|
/// <summary>
|
|
/// set up callback
|
|
/// </summary>
|
|
/// <param name="callback"></param>
|
|
/// <param name="line">scanline. -1 = end of frame, -2 = RIGHT NOW</param>
|
|
public void SetScanlineCallback(ScanlineCallback callback, int line)
|
|
{
|
|
if (GambatteState == IntPtr.Zero)
|
|
// not sure how this is being reached. tried the debugger...
|
|
return;
|
|
endofframecallback = null;
|
|
if (callback == null || line == -1 || line == -2)
|
|
{
|
|
scanlinecb = null;
|
|
LibGambatte.gambatte_setscanlinecallback(GambatteState, null, 0);
|
|
if (line == -1)
|
|
endofframecallback = callback;
|
|
else if (line == -2)
|
|
callback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
|
|
}
|
|
else if (line >= 0 && line <= 153)
|
|
{
|
|
scanlinecb = delegate()
|
|
{
|
|
callback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
|
|
};
|
|
LibGambatte.gambatte_setscanlinecallback(GambatteState, scanlinecb, line);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException("line", "line must be in [0, 153]");
|
|
}
|
|
}
|
|
|
|
LibGambatte.ScanlineCallback scanlinecb;
|
|
ScanlineCallback endofframecallback;
|
|
|
|
#endregion
|
|
|
|
public void Dispose()
|
|
{
|
|
if (GambatteState != IntPtr.Zero)
|
|
{
|
|
LibGambatte.gambatte_destroy(GambatteState);
|
|
GambatteState = IntPtr.Zero;
|
|
}
|
|
DisposeSound();
|
|
}
|
|
|
|
#region palette
|
|
|
|
/// <summary>
|
|
/// update gambatte core's internal colors
|
|
/// </summary>
|
|
public void ChangeDMGColors(int[] colors)
|
|
{
|
|
for (int i = 0; i < 12; i++)
|
|
LibGambatte.gambatte_setdmgpalettecolor(GambatteState, (LibGambatte.PalType)(i / 4), (uint)i % 4, (uint)colors[i]);
|
|
}
|
|
|
|
public void SetCGBColors(GBColors.ColorType type)
|
|
{
|
|
int[] lut = GBColors.GetLut(type);
|
|
LibGambatte.gambatte_setcgbpalette(GambatteState, lut);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ISoundProvider
|
|
|
|
public ISoundProvider SoundProvider { get { return null; } }
|
|
public ISyncSoundProvider SyncSoundProvider { get { return this; } }
|
|
public bool StartAsyncSound() { return false; }
|
|
public void EndAsyncSound() { }
|
|
|
|
/// <summary>
|
|
/// sample pairs before resampling
|
|
/// </summary>
|
|
short[] soundbuff = new short[(35112 + 2064) * 2];
|
|
|
|
int soundoutbuffcontains = 0;
|
|
|
|
short[] soundoutbuff = new short[2048];
|
|
|
|
int latchL = 0;
|
|
int latchR = 0;
|
|
|
|
BlipBuffer blipL, blipR;
|
|
uint blipAccumulate;
|
|
|
|
private void ProcessSound(int nsamp)
|
|
{
|
|
for (uint i = 0; i < nsamp; i++)
|
|
{
|
|
int curr = soundbuff[i * 2];
|
|
|
|
if (curr != latchL)
|
|
{
|
|
int diff = latchL - curr;
|
|
latchL = curr;
|
|
blipL.AddDelta(blipAccumulate, diff);
|
|
}
|
|
curr = soundbuff[i * 2 + 1];
|
|
|
|
if (curr != latchR)
|
|
{
|
|
int diff = latchR - curr;
|
|
latchR = curr;
|
|
blipR.AddDelta(blipAccumulate, diff);
|
|
}
|
|
|
|
blipAccumulate++;
|
|
}
|
|
}
|
|
|
|
private void ProcessSoundEnd()
|
|
{
|
|
blipL.EndFrame(blipAccumulate);
|
|
blipR.EndFrame(blipAccumulate);
|
|
blipAccumulate = 0;
|
|
|
|
soundoutbuffcontains = blipL.SamplesAvailable();
|
|
if (soundoutbuffcontains != blipR.SamplesAvailable())
|
|
throw new InvalidOperationException("Audio processing error");
|
|
|
|
blipL.ReadSamplesLeft(soundoutbuff, soundoutbuffcontains);
|
|
blipR.ReadSamplesRight(soundoutbuff, soundoutbuffcontains);
|
|
}
|
|
|
|
void InitSound()
|
|
{
|
|
blipL = new BlipBuffer(1024);
|
|
blipL.SetRates(TICKSPERSECOND, 44100);
|
|
blipR = new BlipBuffer(1024);
|
|
blipR.SetRates(TICKSPERSECOND, 44100);
|
|
}
|
|
|
|
void DisposeSound()
|
|
{
|
|
if (blipL != null)
|
|
{
|
|
blipL.Dispose();
|
|
blipL = null;
|
|
}
|
|
if (blipR != null)
|
|
{
|
|
blipR.Dispose();
|
|
blipR = null;
|
|
}
|
|
}
|
|
|
|
public void DiscardSamples()
|
|
{
|
|
soundoutbuffcontains = 0;
|
|
}
|
|
|
|
public void GetSamples(out short[] samples, out int nsamp)
|
|
{
|
|
samples = soundoutbuff;
|
|
nsamp = soundoutbuffcontains;
|
|
}
|
|
|
|
public bool Muted { get { return _settings.Muted; } }
|
|
|
|
#endregion
|
|
}
|
|
}
|