diff --git a/VG Music Studio/Core/Engine.cs b/VG Music Studio/Core/Engine.cs index 57200db2..3c11e8f2 100644 --- a/VG Music Studio/Core/Engine.cs +++ b/VG Music Studio/Core/Engine.cs @@ -10,7 +10,8 @@ public enum EngineType : byte GBA_AlphaDream, GBA_MP2K, NDS_DSE, - NDS_SDAT + NDS_SDAT, + PSX_PSF } public static Engine Instance { get; private set; } @@ -72,6 +73,16 @@ public Engine(EngineType type, object playerArg) Player = new NDS.SDAT.Player(mixer, config); break; } + case EngineType.PSX_PSF: + { + string bgmPath = (string)playerArg; + var config = new PSX.PSF.Config(bgmPath); + Config = config; + var mixer = new PSX.PSF.Mixer(); + Mixer = mixer; + Player = new PSX.PSF.Player(mixer, config); + break; + } default: throw new ArgumentOutOfRangeException(nameof(type)); } Type = type; diff --git a/VG Music Studio/Core/PSX/PSF/Channel.cs b/VG Music Studio/Core/PSX/PSF/Channel.cs new file mode 100644 index 00000000..621890ab --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -0,0 +1,136 @@ +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Channel + { + public readonly byte Index; + + public Track Owner; + public ushort BaseTimer = NDS.Utils.ARM7_CLOCK / 44100; + public ushort Timer; + public byte NoteVelocity; + public byte Volume = 0x7F; + public sbyte Pan = 0x40; + public byte BaseKey, Key; + public byte PitchTune; + + private int _pos; + private short _prevLeft; + private short _prevRight; + + private long _dataOffset; + private long _loopOffset; + private short[] _decompressedSample; + + public Channel(byte i) + { + Index = i; + } + + private static readonly float[][] _idk = new float[5][] + { + new float[2] { 0f, 0f }, + new float[2] { 60f / 64f, 0f }, + new float[2] { 115f / 64f, 52f / 64f }, + new float[2] { 98f / 64f, 55f / 64f }, + new float[2] { 122f / 64f, 60f / 64f } + }; + public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) + { + Stop(); + //State = EnvelopeState.Attack; + //Velocity = -92544; + _pos = 0; + _prevLeft = _prevRight = 0; + _loopOffset = 0; + _dataOffset = 0; + float prev1 = 0, prev2 = 0; + _decompressedSample = new short[0x50000]; + for (long i = 0; i < sampleSize; i += 16) + { + byte b0 = exeBuffer[sampleOffset + i]; + byte b1 = exeBuffer[sampleOffset + i + 1]; + int range = b0 & 0xF; + int filter = (b0 & 0xF0) >> 4; + bool end = (b1 & 0x1) != 0; + bool looping = (b1 & 0x2) != 0; + bool loop = (b1 & 0x4) != 0; + + // Decomp + long pi = i * 28 / 16; + int shift = range + 16; + for (int j = 0; j < 14; j++) + { + sbyte bj = (sbyte)exeBuffer[sampleOffset + i + 2 + j]; + _decompressedSample[pi + (j * 2)] = (short)((bj << 28) >> shift); + _decompressedSample[pi + (j * 2) + 1] = (short)(((bj & 0xF0) << 24) >> shift); + } + if (filter == 0) + { + prev1 = _decompressedSample[pi + 27]; + prev2 = _decompressedSample[pi + 26]; + } + else + { + float f1 = _idk[filter][0]; + float f2 = _idk[filter][1]; + float p1 = prev1; + float p2 = prev2; + for (int j = 0; j < 28; j++) + { + float t = _decompressedSample[pi + j] + (p1 * f1) - (p2 * f2); + _decompressedSample[pi + j] = (short)t; + p2 = p1; + p1 = t; + } + prev1 = p1; + prev2 = p2; + } + } + } + + public void Stop() + { + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = null; + Volume = 0; + } + + public void Process(out short left, out short right) + { + if (Timer != 0) + { + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + // prevLeft and prevRight are stored because numSamples can be 0. + for (int i = 0; i < numSamples; i++) + { + short samp; + // If hit end + if (_dataOffset >= _decompressedSample.Length) + { + if (true) + //if (swav.DoesLoop) + { + _dataOffset = _loopOffset; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = _decompressedSample[_dataOffset++]; + samp = (short)(samp * Volume / 0x7F); + _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); + _prevRight = (short)(samp * (Pan + 0x40) / 0x80); + } + } + left = _prevLeft; + right = _prevRight; + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/Commands.cs b/VG Music Studio/Core/PSX/PSF/Commands.cs new file mode 100644 index 00000000..92be90fb --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Commands.cs @@ -0,0 +1,54 @@ +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class ControllerCommand : ICommand + { + public Color Color => Color.MediumVioletRed; + public string Label => "Controller"; + public string Arguments => $"{Controller}, {Value}"; + + public byte Controller { get; set; } + public byte Value { get; set; } + } + internal class FinishCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; + } + internal class NoteCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => $"Note {(Velocity == 0 ? "Off" : "On")}"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)}{(Velocity == 0 ? string.Empty : $", {Velocity}")}"; + + public byte Key { get; set; } + public byte Velocity { get; set; } + } + internal class PitchBendCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => $"0x{Bend1:X} 0x{Bend2:X}"; + + public byte Bend1 { get; set; } + public byte Bend2 { get; set; } + } + internal class TempoCommand : ICommand + { + public Color Color => Color.DeepSkyBlue; + public string Label => "Tempo"; + public string Arguments => Tempo.ToString(); + + public uint Tempo { get; set; } + } + internal class VoiceCommand : ICommand + { + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/Config.cs b/VG Music Studio/Core/PSX/PSF/Config.cs new file mode 100644 index 00000000..e8860996 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Config.cs @@ -0,0 +1,37 @@ +using Kermalis.VGMusicStudio.Properties; +using System; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Config : Core.Config + { + public readonly string BGMPath; + public readonly string[] BGMFiles; + + public Config(string bgmPath) + { + BGMPath = bgmPath; + BGMFiles = Directory.EnumerateFiles(bgmPath).Where(f => f.EndsWith(".minipsf", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".psf", StringComparison.OrdinalIgnoreCase)).ToArray(); + if (BGMFiles.Length == 0) + { + throw new Exception(Strings.ErrorDSENoSequences); + } + var songs = new Song[BGMFiles.Length]; + for (int i = 0; i < BGMFiles.Length; i++) + { + // TODO: Read title from tag + songs[i] = new Song(i, Path.GetFileNameWithoutExtension(BGMFiles[i])); + } + Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + } + + public override string GetSongName(long index) + { + return index < 0 || index >= BGMFiles.Length + ? index.ToString() + : '\"' + BGMFiles[index] + '\"'; + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/Mixer.cs b/VG Music Studio/Core/PSX/PSF/Mixer.cs new file mode 100644 index 00000000..f339dc1d --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Mixer.cs @@ -0,0 +1,211 @@ +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Mixer : Core.Mixer + { + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public Channel[] Channels; + private readonly BufferedWaveProvider _buffer; + + public Mixer() + { + const int sampleRate = 65456; // TODO + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + Channels = new Channel[0x10]; + for (byte i = 0; i < 0x10; i++) + { + Channels[i] = new Channel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64 + }; + Init(_buffer); + } + public override void Dispose() + { + base.Dispose(); + CloseWaveWriter(); + } + + public Channel AllocateChannel() + { + int GetScore(Channel c) + { + return c.Owner == null ? -2 : false ? -1 : 0; + //return c.Owner == null ? -2 : c.State == EnvelopeState.Release ? -1 : 0; + } + Channel nChan = null; + for (int i = 0; i < 0x10; i++) + { + Channel c = Channels[i]; + if (nChan != null) + { + int nScore = GetScore(nChan); + int cScore = GetScore(c); + if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) + { + nChan = c; + } + } + else + { + nChan = c; + } + } + return nChan != null && 0 >= GetScore(nChan) ? nChan : null; + } + + public void ChannelTick() + { + for (int i = 0; i < 0x10; i++) + { + Channel chan = Channels[i]; + if (chan.Owner != null) + { + //chan.StepEnvelope(); + int vol = NDS.SDAT.Utils.SustainTable[chan.NoteVelocity] + 0; + int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.PitchTune; // "<< 6" is "* 0x40" + if (/*chan.State == EnvelopeState.Release && */vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = NDS.SDAT.Utils.GetChannelVolume(vol); + chan.Timer = NDS.SDAT.Utils.GetChannelTimer(chan.BaseTimer, pitch); + chan.Pan = 0; + } + } + } + } + + public void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + public void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; + } + public bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private WaveFileWriter _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter?.Dispose(); + } + public void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + byte[] b = new byte[4]; + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < 0x10; j++) + { + Channel chan = Channels[j]; + if (chan.Owner != null) + { + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + b[0] = (byte)left; + b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + b[2] = (byte)right; + b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(b, 0, 4); + } + if (recording) + { + _waveWriter.Write(b, 0, 4); + } + } + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/PSF.cs b/VG Music Studio/Core/PSX/PSF/PSF.cs new file mode 100644 index 00000000..a7951a95 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/PSF.cs @@ -0,0 +1,113 @@ +using Ionic.Crc; +using Ionic.Zlib; +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class PSF + { + private const int _exeBufferSize = 0x200000; + + public string FilePath; + public byte[] FileBytes; + public byte[] DecompressedEXE; + public string Tag; + + public static void Open(string fileName, out byte[] exeBuffer, out PSF psf) + { + exeBuffer = new byte[_exeBufferSize]; + psf = new PSF(fileName); + LoadLib1(psf, exeBuffer); + PlaceEXE(psf, exeBuffer); + LoadLib2(psf, exeBuffer); + } + + private PSF(string fileName) + { + FilePath = fileName; + FileBytes = File.ReadAllBytes(fileName); + using (var reader = new EndianBinaryReader(new MemoryStream(FileBytes))) + { + if (reader.ReadString(3) != "PSF") + { + throw new InvalidDataException(); + } + if (reader.ReadByte() != 1) + { + throw new InvalidDataException(); + } + uint reservedSize = reader.ReadUInt32(); + uint exeSize = reader.ReadUInt32(); + int checksum = reader.ReadInt32(); + byte[] exe = new byte[exeSize]; + Array.Copy(FileBytes, 0x10 + reservedSize, exe, 0, exeSize); + var crc32 = new CRC32(); + if (crc32.GetCrc32(new MemoryStream(exe)) != checksum) + { + throw new InvalidDataException(); + } + DecompressedEXE = ZlibStream.UncompressBuffer(exe); + uint tagOffset = 0x10 + reservedSize + exeSize; + Tag = reader.ReadString((int)(FileBytes.Length - tagOffset), tagOffset); + } + } + private static void LoadLib1(PSF psf, byte[] exeBuffer) + { + foreach (KeyValuePair kvp in GetTags(psf.Tag)) + { + if (kvp.Key == "_lib") + { + var lib = new PSF(Path.Combine(Path.GetDirectoryName(psf.FilePath), kvp.Value)); + LoadLib1(lib, exeBuffer); + LoadLib2(lib, exeBuffer); + PlaceEXE(lib, exeBuffer); + break; + } + } + } + private static void LoadLib2(PSF psf, byte[] exeBuffer) + { + bool cont = true; + for (int i = 2; cont; i++) + { + cont = false; + foreach (KeyValuePair kvp in GetTags(psf.Tag)) + { + if (kvp.Key == $"_lib{i}") + { + var lib = new PSF(Path.Combine(Path.GetDirectoryName(psf.FilePath), kvp.Value)); + LoadLib1(lib, exeBuffer); + LoadLib2(lib, exeBuffer); + PlaceEXE(lib, exeBuffer); + cont = true; + break; + } + } + } + } + private static void PlaceEXE(PSF psf, byte[] exeBuffer) + { + using (var reader = new EndianBinaryReader(new MemoryStream(psf.DecompressedEXE))) + { + uint textSectionStart = reader.ReadUInt32(0x18) & 0x3FFFFF; + uint textSectionSize = reader.ReadUInt32(); + Array.Copy(psf.DecompressedEXE, 0x800, exeBuffer, textSectionStart, textSectionSize); + } + } + private static Dictionary GetTags(string tag) + { + string[] tags = tag.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + var mapped = new Dictionary(); + for (int i = 0; i < tags.Length - 1; i++) + { + string str = tags[i]; + int index = str.IndexOf('='); + mapped.Add(str.Substring(0, index), str.Substring(index + 1, str.Length - index - 1)); + } + return mapped; + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs new file mode 100644 index 00000000..2da19cd4 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -0,0 +1,604 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Player : IPlayer + { + private const long SongOffset = 0x120000; // Crash Bandicoot 2 + private const long SamplesOffset = 0x140000; // Crash Bandicoot 2 + private const int RefreshRate = 192; // TODO: A PSF can determine refresh rate regardless of region + private readonly Track[] _tracks = new Track[0x10]; + private readonly Mixer _mixer; + private readonly Config _config; + private readonly TimeBarrier _time; + private Thread _thread; + private byte[] _exeBuffer; + private VAB _vab; + private long _dataOffset; + private long _startOffset; + private long _loopOffset; + private byte _runningStatus; + private ushort _ticksPerQuarterNote; + private uint _microsecondsPerBeat; + private uint _microsecondsPerTick; + private long _tickStack; + private long _ticksPerUpdate; + private ushort _tempo; + private long _deltaTicks; + private long _elapsedLoops; + + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; private set; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + + public PlayerState State { get; private set; } + public event SongEndedEvent SongEnded; + + public Player(Mixer mixer, Config config) + { + for (byte i = 0; i < 0x10; i++) + { + _tracks[i] = new Track(i); + } + _mixer = mixer; + _config = config; + + _time = new TimeBarrier(RefreshRate); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "PSF Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void TEMPORARY_UpdateTimeVars() + { + _tempo = (ushort)(60000000 / _microsecondsPerBeat); + _microsecondsPerTick = _microsecondsPerBeat / _ticksPerQuarterNote; + _ticksPerUpdate = 1000000 / RefreshRate; + } + private void InitEmulation() + { + _dataOffset = SongOffset; + _dataOffset += 4; // "pQES" + _dataOffset += 4; // Version + _ticksPerQuarterNote = (ushort)((_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); + _microsecondsPerBeat = (uint)((_exeBuffer[_dataOffset++] << 16) | (_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); + TEMPORARY_UpdateTimeVars(); + //dataOffset += 2; // Time Signature + byte ts1 = _exeBuffer[_dataOffset++]; + double ts2 = Math.Pow(2, _exeBuffer[_dataOffset++]); + _dataOffset += 4; // Unknown + + _startOffset = _dataOffset; + _loopOffset = _tickStack = _elapsedLoops = ElapsedTicks = _deltaTicks = _runningStatus = 0; + _mixer.ResetFade(); + for (int i = 0; i < _tracks.Length; i++) + { + _tracks[i].Init(); + } + } + public void LoadSong(long index) + { + PSF.Open(_config.BGMFiles[index], out _exeBuffer, out _); + using (var reader = new EndianBinaryReader(new MemoryStream(_exeBuffer))) + { + uint ReadVarLen() + { + uint value; + byte c; + if (((value = reader.ReadByte()) & 0x80) != 0) + { + value &= 0x7F; + do + { + value = (uint)((value << 7) + ((c = reader.ReadByte()) & 0x7F)); + } while ((c & 0x80) != 0); + } + return value; + } + + _vab = new VAB(reader); + reader.BaseStream.Position = SongOffset; + reader.Endianness = Endianness.BigEndian; + reader.ReadString(4); // "pQES" + reader.ReadUInt32(); // Version + reader.ReadUInt16(); // Ticks per Quarter Note + reader.ReadBytes(3); // Microseconds per Beat + reader.ReadBytes(2); // Time signature + reader.ReadBytes(4); // Unknown + Events = new List[0x10]; + for (int i = 0; i < 0x10; i++) + { + Events[i] = new List(); + } + _runningStatus = 0; + byte curTrack = 0; + MaxTicks = 0; + + bool EventExists(long offset) + { + return Events[curTrack].Any(e => e.Offset == offset); + } + bool cont = true; + while (cont) + { + long offset = reader.BaseStream.Position; + void AddEvent(ICommand command) + { + var ev = new SongEvent(offset, command); + ev.Ticks.Add(MaxTicks); + Events[curTrack].Add(ev); + } + MaxTicks += ReadVarLen(); + byte cmd = reader.ReadByte(); + void Invalid() + { + throw new Exception(string.Format("TODO", curTrack, offset, cmd)); + } + + if (cmd <= 0x7F) + { + cmd = _runningStatus; + reader.BaseStream.Position--; + } + else + { + _runningStatus = cmd; + } + curTrack = (byte)(cmd & 0xF); + switch (cmd & 0xF0) + { + case 0x90: + { + byte key = reader.ReadByte(); + byte velocity = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new NoteCommand { Key = key, Velocity = velocity }); + } + break; + } + case 0xB0: + { + byte controller = reader.ReadByte(); + byte value = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new ControllerCommand { Controller = controller, Value = value }); + } + switch (controller) + { + case 0x63: + { + switch (value) + { + case 0x1E: + { + cont = false; + break; + } + } + break; + } + } + break; + } + case 0xC0: + { + byte voice = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = voice }); + } + break; + } + case 0xE0: + { + byte bend1 = reader.ReadByte(); + byte bend2 = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend1 = bend1, Bend2 = bend2 }); + } + break; + } + case 0xF0: + { + byte meta = reader.ReadByte(); + switch (meta) + { + case 0x2F: + { + if (!EventExists(offset)) + { + AddEvent(new FinishCommand()); + } + cont = false; + break; + } + case 0x51: + { + uint tempo = (uint)((reader.ReadUInt16() << 8) | reader.ReadByte()); + if (!EventExists(offset)) + { + AddEvent(new TempoCommand { Tempo = tempo }); + } + break; + } + default: Invalid(); break; // TODO: Include this invalid portion + } + break; + } + default: Invalid(); break; + } + } + } + } + public void SetCurrentPosition(long ticks) + { + /*if (tracks == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + else + { + while (tempoStack >= 240) + { + tempoStack -= 240; + for (int i = 0; i < tracks.Length; i++) + { + Track track = tracks[i]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(i); + } + } + } + ElapsedTicks++; + if (ElapsedTicks == ticks) + { + goto finish; + } + } + tempoStack += tempo; + } + } + finish: + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].StopAllChannels(); + } + Pause(); + }*/ + } + public void Play() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State == PlayerState.Playing || State == PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + public void GetSongState(UI.SongInfoControl.SongInfo info) + { + info.Tempo = _tempo; + for (int i = 0; i < 0x10; i++) + { + Track track = _tracks[i]; + UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; + tin.Position = _dataOffset; + tin.Rest = _deltaTicks; + tin.Voice = track.Voice; + tin.Type = "PCM"; + //tin.Volume = track.Volume; + tin.PitchBend = track.PitchBend; + //tin.Extra = track.Octave; + //tin.Panpot = track.Panpot; + + Channel[] channels = track.Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + Channel c = channels[j]; + tin.Keys[numKeys++] = c.Key; + float a = (float)(0 + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > left) + { + left = a; + } + a = (float)(0 + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > right) + { + right = a; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + } + } + } + + private void ExecuteNext() + { + uint ReadVarLen() + { + uint value; + byte c; + if (((value = _exeBuffer[_dataOffset++]) & 0x80) != 0) + { + value &= 0x7F; + do + { + value = (uint)((value << 7) + ((c = _exeBuffer[_dataOffset++]) & 0x7F)); + } while ((c & 0x80) != 0); + } + return value; + } + + _deltaTicks = ReadVarLen(); + byte cmd = _exeBuffer[_dataOffset++]; + if (cmd <= 0x7F) + { + cmd = _runningStatus; + _dataOffset--; + } + else + { + _runningStatus = cmd; + } + Track track = _tracks[cmd & 0xF]; + switch (cmd & 0xF0) + { + case 0x90: + { + byte key = _exeBuffer[_dataOffset++]; + byte velocity = _exeBuffer[_dataOffset++]; + if (velocity == 0) + { + Channel[] chans = track.Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + Channel c = chans[i]; + if (c.Key == key) + { + c.Stop(); + break; + } + } + } + else + { + VAB.Program p = _vab.Programs[track.Voice]; + VAB.Instrument ins = _vab.Instruments[track.Voice]; + byte num = p.NumTones; + for (int i = 0; i < num; i++) + { + VAB.Tone t = ins.Tones[i]; + if (t.LowKey <= key && t.HighKey >= key) + { + (long sampleOffset, long sampleSize) = _vab.VAGs[t.SampleId - 1]; + Channel c = _mixer.AllocateChannel(); + if (c != null) + { + c.Start(sampleOffset + SamplesOffset, sampleSize, _exeBuffer); + c.Key = key; + c.BaseKey = t.BaseKey; + c.PitchTune = t.PitchTune; + c.NoteVelocity = velocity; + c.Owner = track; + track.Channels.Add(c); + } + break; + } + } + } + break; + } + case 0xB0: + { + byte controller = _exeBuffer[_dataOffset++]; + byte value = _exeBuffer[_dataOffset++]; + switch (controller) + { + case 0x63: + { + switch (value) + { + case 0x14: + { + _loopOffset = _dataOffset; + break; + } + case 0x1E: + { + _dataOffset = _loopOffset; + break; + } + } + break; + } + } + break; + } + case 0xC0: + { + byte voice = _exeBuffer[_dataOffset++]; + track.Voice = voice; + break; + } + case 0xE0: + { + ushort pitchBend = (ushort)((_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); + track.PitchBend = pitchBend; + break; + } + case 0xF0: + { + byte meta = _exeBuffer[_dataOffset++]; + switch (meta) + { + case 0x2F: + { + _dataOffset = _startOffset; + break; + } + case 0x51: + { + _microsecondsPerBeat = (uint)((_exeBuffer[_dataOffset++] << 16) | (_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); + TEMPORARY_UpdateTimeVars(); + break; + } + } + break; + } + } + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + goto stop; + } + + while (_tickStack > _microsecondsPerTick) + { + _tickStack -= _microsecondsPerTick; + if (_deltaTicks > 0) + { + _deltaTicks--; + } + while (_deltaTicks == 0) + { + ExecuteNext(); + } + if (ElapsedTicks == MaxTicks) + { + for (int i = 0; i < 0x10; i++) + { + List t = Events[i]; + for (int j = 0; j < t.Count; j++) + { + SongEvent e = t[j]; + if (e.Offset == _dataOffset) + { + ElapsedTicks = e.Ticks[0]; + goto doneSearch; + } + } + } + throw new Exception(); + doneSearch: + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + else + { + ElapsedTicks++; + } + } + _tickStack += _ticksPerUpdate; + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + if (playing) + { + _time.Wait(); + } + } + stop: + _time.Stop(); + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/Track.cs b/VG Music Studio/Core/PSX/PSF/Track.cs new file mode 100644 index 00000000..263cc28e --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Track.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Track + { + public readonly byte Index; + + public byte Voice; + public ushort PitchBend; + + public readonly List Channels = new List(0x10); + + public Track(byte i) + { + Index = i; + } + public void Init() + { + Voice = 0; + PitchBend = 0; + StopAllChannels(); + } + + public void StopAllChannels() + { + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + } +} diff --git a/VG Music Studio/Core/PSX/PSF/VAB.cs b/VG Music Studio/Core/PSX/PSF/VAB.cs new file mode 100644 index 00000000..a567923f --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/VAB.cs @@ -0,0 +1,105 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class VAB + { + public class Program + { + public byte NumTones { get; set; } + public byte Volume { get; set; } // Out of 127 + public byte Priority { get; set; } + public byte Mode { get; set; } + public byte Panpot { get; set; } // 0x40 is middle + [BinaryArrayFixedLength(11)] + public byte[] Unknown { get; set; } + } + + public class Tone + { + public byte Priority { get; set; } + public byte Mode { get; set; } + public byte Volume { get; set; } // Out of 127 + public byte Panpot { get; set; } // 0x40 is middle + public byte BaseKey { get; set; } + public byte PitchTune { get; set; } + public byte LowKey { get; set; } + public byte HighKey { get; set; } + public byte VibratoWidth { get; set; } + public byte VibratoTime { get; set; } + public byte PortamentoWidth { get; set; } + public byte PortamentoTime { get; set; } + public byte PitchBendMin { get; set; } + public byte PitchBendMax { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Release { get; set; } + public ushort ParentProgram { get; set; } + public ushort SampleId { get; set; } + [BinaryArrayFixedLength(8)] + public byte[] Unknown2 { get; set; } + } + + public class Instrument + { + [BinaryArrayFixedLength(0x10)] + public Tone[] Tones { get; set; } + } + + private const long _instrumentsOffset = 0x130000; // Crash Bandicoot 2 + + public ushort NumPrograms { get; } + public ushort NumTones { get; } + public ushort NumVAGs { get; } + public Program[] Programs { get; } + public Instrument[] Instruments { get; } + public (long Offset, long Size)[] VAGs { get; } + + public VAB(EndianBinaryReader reader) + { + // Header + reader.BaseStream.Position = _instrumentsOffset; + reader.Endianness = Endianness.LittleEndian; + reader.ReadString(4); // "pBAV" + reader.ReadUInt32(); // Version + reader.ReadUInt32(); // VAB ID + reader.ReadUInt32(); // Size + reader.ReadBytes(2); // Unknown + NumPrograms = reader.ReadUInt16(); + NumTones = reader.ReadUInt16(); + NumVAGs = reader.ReadUInt16(); + reader.ReadByte(); // MasterVolume (out of 100?) + reader.ReadByte(); // MasterPanpot (0x40 is middle) + reader.ReadByte(); // BankAttributes1 + reader.ReadByte(); // BankAttributes2 + reader.ReadBytes(4); // Padding + + // Programs + Programs = new Program[0x80]; + for (int i = 0; i < 0x80; i++) + { + Programs[i] = reader.ReadObject(); + } + + // Instruments + Instruments = new Instrument[NumPrograms]; + for (int i = 0; i < NumPrograms; i++) + { + Instruments[i] = reader.ReadObject(); + } + + // VAG Pointers + VAGs = new (long Offset, long Size)[0xFF]; + long offset = reader.ReadUInt16() * 8; + for (int i = 0; i < 0xFF; i++) + { + long size = reader.ReadUInt16() * 8; + VAGs[i] = (offset, size); + offset += size; + } + } + } +} diff --git a/VG Music Studio/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs index f66445c8..8d020229 100644 --- a/VG Music Studio/UI/MainForm.cs +++ b/VG Music Studio/UI/MainForm.cs @@ -34,7 +34,7 @@ internal class MainForm : ThemedForm #region Controls private readonly MenuStrip _mainMenu; - private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openPSFItem, _openSDATItem, _dataItem, _trackViewerItem, _exportMIDIItem, _exportWAVItem, _playlistItem, _endPlaylistItem; private readonly Timer _timer; @@ -71,10 +71,12 @@ private MainForm() _openAlphaDreamItem.Click += OpenAlphaDream; _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; _openMP2KItem.Click += OpenMP2K; + _openPSFItem = new ToolStripMenuItem { Text = "TODO" }; + _openPSFItem.Click += OpenPSF; _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; _openSDATItem.Click += OpenSDAT; _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; - _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); + _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openPSFItem, _openSDATItem }); // Data Menu _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; @@ -394,6 +396,34 @@ private void OpenMP2K(object sender, EventArgs e) } } } + private void OpenPSF(object sender, EventArgs e) + { + var d = new CommonOpenFileDialog + { + Title = "TODO", + IsFolderPicker = true + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.PSX_PSF, d.FileName); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex.Message, "TODO"); + success = false; + } + if (success) + { + var config = (Core.PSX.PSF.Config)Engine.Instance.Config; + FinishLoading(false, config.BGMFiles.Length); + } + } + } private void OpenSDAT(object sender, EventArgs e) { var d = new CommonOpenFileDialog diff --git a/VG Music Studio/Util/TimeBarrier.cs b/VG Music Studio/Util/TimeBarrier.cs index 41bf22fa..c253c0b5 100644 --- a/VG Music Studio/Util/TimeBarrier.cs +++ b/VG Music Studio/Util/TimeBarrier.cs @@ -29,11 +29,10 @@ public void Wait() double totalElapsed = _sw.ElapsedTicks * _timerInterval; double desiredTimeStamp = _lastTimeStamp + _waitInterval; double timeToWait = desiredTimeStamp - totalElapsed; - if (timeToWait < 0) + if (timeToWait > 0) { - timeToWait = 0; + Thread.Sleep((int)(timeToWait * 1000)); } - Thread.Sleep((int)(timeToWait * 1000)); _lastTimeStamp = desiredTimeStamp; } diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index 252eb1a8..6914a5e3 100644 --- a/VG Music Studio/VG Music Studio.csproj +++ b/VG Music Studio/VG Music Studio.csproj @@ -99,6 +99,9 @@ ..\packages\YamlDotNet.5.2.1\lib\net45\YamlDotNet.dll + + ..\packages\Zlib.Portable.Signed.1.11.0\lib\portable-net4+sl5+wp8+win8+wpa81+MonoTouch+MonoAndroid\Zlib.Portable.dll + @@ -150,6 +153,14 @@ + + + + + + + + diff --git a/VG Music Studio/packages.config b/VG Music Studio/packages.config index 45a2f874..370fabaa 100644 --- a/VG Music Studio/packages.config +++ b/VG Music Studio/packages.config @@ -5,4 +5,5 @@ + \ No newline at end of file