From 676123053ff7568e4fa8b6b907fc9d5dcb264da7 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Wed, 7 Aug 2019 15:40:56 -0400 Subject: [PATCH 01/14] Initial 8-bit implementation of PSF --- VG Music Studio/Core/Engine.cs | 13 +- VG Music Studio/Core/PSX/PSF/Channel.cs | 78 ++++ VG Music Studio/Core/PSX/PSF/Commands.cs | 53 +++ VG Music Studio/Core/PSX/PSF/Config.cs | 29 ++ VG Music Studio/Core/PSX/PSF/Mixer.cs | 196 ++++++++++ VG Music Studio/Core/PSX/PSF/PSF.cs | 106 +++++ VG Music Studio/Core/PSX/PSF/Player.cs | 479 +++++++++++++++++++++++ VG Music Studio/Core/PSX/PSF/Track.cs | 38 ++ VG Music Studio/UI/MainForm.cs | 34 +- VG Music Studio/VG Music Studio.csproj | 10 + VG Music Studio/packages.config | 1 + 11 files changed, 1034 insertions(+), 3 deletions(-) create mode 100644 VG Music Studio/Core/PSX/PSF/Channel.cs create mode 100644 VG Music Studio/Core/PSX/PSF/Commands.cs create mode 100644 VG Music Studio/Core/PSX/PSF/Config.cs create mode 100644 VG Music Studio/Core/PSX/PSF/Mixer.cs create mode 100644 VG Music Studio/Core/PSX/PSF/PSF.cs create mode 100644 VG Music Studio/Core/PSX/PSF/Player.cs create mode 100644 VG Music Studio/Core/PSX/PSF/Track.cs diff --git a/VG Music Studio/Core/Engine.cs b/VG Music Studio/Core/Engine.cs index 6b44e346..60c3f577 100644 --- a/VG Music Studio/Core/Engine.cs +++ b/VG Music Studio/Core/Engine.cs @@ -10,7 +10,8 @@ public enum EngineType : byte GBA_MLSS, 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..f2aac4d3 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -0,0 +1,78 @@ +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Channel + { + public readonly byte Index; + + public Track Owner; + public ushort BaseTimer, Timer; + public int NoteDuration; + public byte NoteVelocity; + public byte Volume = 0x7F; + public sbyte Pan = 0x40; + public byte Key; + + private int pos; + private short prevLeft, prevRight; + + // PSG + private byte psgDuty; + private int psgCounter; + + public Channel(byte i) + { + Index = i; + } + + public void StartPSG(byte duty, int noteDuration) + { + psgCounter = 0; + psgDuty = duty; + BaseTimer = 8006; + Start(noteDuration); + } + + private void Start(int noteDuration) + { + //State = EnvelopeState.Attack; + //Velocity = -92544; + pos = 0; + prevLeft = prevRight = 0; + NoteDuration = noteDuration; + } + + 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 = psgCounter <= psgDuty ? short.MinValue : short.MaxValue; + psgCounter++; + if (psgCounter >= 8) + { + psgCounter = 0; + } + 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..759dcd1f --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Commands.cs @@ -0,0 +1,53 @@ +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 => Bend.ToString(); + + public ushort Bend { 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..2d4cad59 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Config.cs @@ -0,0 +1,29 @@ +using Kermalis.VGMusicStudio.Properties; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Config : Core.Config + { + public string BGMPath; + public string[] BGMFiles; + + public Config(string bgmPath) + { + BGMPath = bgmPath; + BGMFiles = Directory.GetFiles(bgmPath, "*.psf", SearchOption.TopDirectoryOnly); + 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)); + } + } +} 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..ce16110b --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Mixer.cs @@ -0,0 +1,196 @@ +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 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(); + if (chan.NoteDuration == 0) + { + //chan.State = EnvelopeState.Release; + } + int vol = NDS.SDAT.Utils.SustainTable[chan.NoteVelocity] + 0; + int pitch = (chan.Key - 60) << 6; // "<< 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; + } + public void BeginFadeOut() + { + fadePos = 1f; + fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + fadeStepPerMicroframe = -1f / fadeMicroFramesLeft; + } + public bool IsFadeDone() + { + return fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + 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 fromMaster = 1f, 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--; + } + float masterStep = (toMaster - fromMaster) * samplesReciprocal; + float 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..95d21603 --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/PSF.cs @@ -0,0 +1,106 @@ +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))) + { + reader.ReadString(3); // PSF + reader.ReadByte(); // Version + 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); + } + } + } + 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..64d8ba1f --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -0,0 +1,479 @@ +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; + private readonly Track[] tracks = new Track[0x10]; + private readonly Mixer mixer; + private readonly Config config; + private readonly TimeBarrier time; + private Thread thread; + private ushort tempo; + private int tempoStack; + private long elapsedLoops; + private bool fadeOutBegan; + + 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); + } + this.mixer = mixer; + this.config = config; + + time = new TimeBarrier(192); + //time = new TimeBarrier(60); // TODO: A PSF can determine refresh rate regardless of region; does this affect what we should put here? + } + 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 uint ReadVarLen(EndianBinaryReader reader) + { + 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; + } + + private void InitEmulation() + { + tempo = 158; + tempoStack = 0; + elapsedLoops = ElapsedTicks = 0; + fadeOutBegan = false; + for (int i = 0; i < tracks.Length; i++) + { + tracks[i].Init(); + } + } + public void LoadSong(long index) + { + PSF.Open(config.BGMFiles[index], out byte[] exeBuffer, out _); + using (var reader = new EndianBinaryReader(new MemoryStream(exeBuffer), Endianness.BigEndian)) + { + reader.BaseStream.Position = SongOffset; + reader.ReadString(4); // "pQES" + reader.ReadUInt32(); // Version + reader.ReadUInt16(); // PPQN + reader.ReadBytes(3); // Tempo + reader.ReadBytes(2); // Time signature + reader.ReadBytes(4); // Unknown + Events = new List[0x10]; + for (int i = 0; i < 0x10; i++) + { + Events[i] = new List(); + } + byte 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(reader); + 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: + { + ushort bend = reader.ReadUInt16(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = bend }); + } + 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 = track.CurEvent >= Events[i].Count ? 0 : Events[i][track.CurEvent].Offset; + //tin.Rest = track.Rest; + 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(int trackIndex) + { + bool increment = true; + List ev = Events[trackIndex]; + Track track = tracks[trackIndex]; + switch (ev[track.CurEvent].Command) + { + case FinishCommand _: + { + track.CurEvent = 0; // TODO: loops + break; + } + case NoteCommand note: + { + if (note.Velocity == 0) + { + Channel[] chans = track.Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + Channel c = chans[i]; + if (c.Key == note.Key) + { + c.Stop(); + break; + } + } + } + else + { + Channel channel = mixer.AllocateChannel(); + channel.Stop(); + channel.StartPSG(4, -1); + channel.Key = note.Key; + channel.NoteVelocity = note.Velocity; + channel.Owner = track; + track.Channels.Add(channel); + } + break; + } + case PitchBendCommand bend: track.PitchBend = bend.Bend; break; + case TempoCommand tem: tempo = (ushort)(60000000 / tem.Tempo); break; + case VoiceCommand voice: track.Voice = voice.Voice; break; + } + if (increment) + { + track.CurEvent++; + } + } + + private void Tick() + { + time.Start(); + while (State == PlayerState.Playing || State == PlayerState.Recording) + { + while (tempoStack >= 24) + { + tempoStack -= 24; + bool allDone = true; + for (int i = 0; i < tracks.Length; i++) + { + Track track = tracks[i]; + while (track.CurEvent < Events[i].Count && Events[i][track.CurEvent].Ticks.Contains(ElapsedTicks)) + { + ExecuteNext(i); + } + if (!track.Stopped || track.Channels.Count != 0) + { + allDone = false; + } + } + if (ElapsedTicks == MaxTicks) + { + ElapsedTicks = 0; + elapsedLoops++; + if (ShouldFadeOut && !fadeOutBegan && elapsedLoops > NumLoops) + { + fadeOutBegan = true; + mixer.BeginFadeOut(); + } + } + else + { + ElapsedTicks++; + } + if (fadeOutBegan && mixer.IsFadeDone()) + { + allDone = true; + } + if (allDone) + { + State = PlayerState.Stopped; + SongEnded?.Invoke(); + } + } + tempoStack += tempo; + mixer.ChannelTick(); + mixer.Process(State == PlayerState.Playing, State == PlayerState.Recording); + if (State == PlayerState.Playing) + { + time.Wait(); + } + } + 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..9c4a9d1c --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/Track.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.PSX.PSF +{ + internal class Track + { + public readonly byte Index; + + public byte Voice; + public bool Stopped; + public ushort PitchBend; + public int CurEvent; + + public readonly List Channels = new List(0x10); + + public Track(byte i) + { + Index = i; + } + public void Init() + { + Voice = 0; + CurEvent = 0; + PitchBend = 0; + Stopped = false; + 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/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs index 96b1a957..dbb6d871 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, openMLSSItem, openMP2KItem, openSDATItem, + private readonly ToolStripMenuItem fileItem, openDSEItem, openMLSSItem, openMP2KItem, openPSFItem, openSDATItem, dataItem, trackViewerItem, exportMIDIItem, exportWAVItem, playlistItem, endPlaylistItem; private readonly Timer timer; @@ -71,10 +71,12 @@ private MainForm() openMLSSItem.Click += OpenMLSS; 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, openMLSSItem, openMP2KItem, openSDATItem }); + fileItem.DropDownItems.AddRange(new ToolStripItem[] { openDSEItem, openMLSSItem, 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/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index d1eb081d..7ca1d8c7 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,13 @@ + + + + + + + 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 From d5e14b57fa964d837189615615321898fc9e9bc5 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Wed, 7 Aug 2019 17:55:22 -0400 Subject: [PATCH 02/14] [PSX] Use initial tempo --- VG Music Studio/Core/PSX/PSF/Player.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index 64d8ba1f..39427e87 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -16,6 +16,7 @@ internal class Player : IPlayer private readonly Config config; private readonly TimeBarrier time; private Thread thread; + private ushort initialTempo; private ushort tempo; private int tempoStack; private long elapsedLoops; @@ -72,7 +73,7 @@ private uint ReadVarLen(EndianBinaryReader reader) private void InitEmulation() { - tempo = 158; + tempo = initialTempo; tempoStack = 0; elapsedLoops = ElapsedTicks = 0; fadeOutBegan = false; @@ -90,7 +91,7 @@ public void LoadSong(long index) reader.ReadString(4); // "pQES" reader.ReadUInt32(); // Version reader.ReadUInt16(); // PPQN - reader.ReadBytes(3); // Tempo + initialTempo = (ushort)(60000000 / (uint)((reader.ReadUInt16() << 8) | (reader.ReadByte()))); reader.ReadBytes(2); // Time signature reader.ReadBytes(4); // Unknown Events = new List[0x10]; From 661d03fbd0a75eb615bb25856a94a03bb3f9cbde Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Wed, 7 Aug 2019 17:58:03 -0400 Subject: [PATCH 03/14] [PSX] VAB object --- VG Music Studio/Core/PSX/PSF/Player.cs | 5 +- VG Music Studio/Core/PSX/PSF/VAB.cs | 71 ++++++++++++++++++++++++++ VG Music Studio/VG Music Studio.csproj | 1 + 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 VG Music Studio/Core/PSX/PSF/VAB.cs diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index 39427e87..def40caa 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -16,6 +16,7 @@ internal class Player : IPlayer private readonly Config config; private readonly TimeBarrier time; private Thread thread; + private VAB vab; private ushort initialTempo; private ushort tempo; private int tempoStack; @@ -85,9 +86,11 @@ private void InitEmulation() public void LoadSong(long index) { PSF.Open(config.BGMFiles[index], out byte[] exeBuffer, out _); - using (var reader = new EndianBinaryReader(new MemoryStream(exeBuffer), Endianness.BigEndian)) + using (var reader = new EndianBinaryReader(new MemoryStream(exeBuffer))) { + vab = new VAB(reader); reader.BaseStream.Position = SongOffset; + reader.Endianness = Endianness.BigEndian; reader.ReadString(4); // "pQES" reader.ReadUInt32(); // Version reader.ReadUInt16(); // PPQN 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..850ae47a --- /dev/null +++ b/VG Music Studio/Core/PSX/PSF/VAB.cs @@ -0,0 +1,71 @@ +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; } + } + + 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 byte[][] Tones { 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 < Programs.Length; i++) + { + Programs[i] = reader.ReadObject(); + } + + // Tones (Unknown structure) + Tones = new byte[0x10 * NumPrograms][]; + for (int i = 0; i < Tones.Length; i++) + { + Tones[i] = reader.ReadBytes(0x20); + } + + // VAG Pointers + VAGs = new (long Offset, long Size)[0xFF]; + long offset = reader.ReadUInt16() * 8; + for (int i = 0; i < VAGs.Length; i++) + { + long size = reader.ReadUInt16() * 8; + VAGs[i] = (offset, size); + offset += size; + } + } + } +} diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index 7ca1d8c7..d7288b06 100644 --- a/VG Music Studio/VG Music Studio.csproj +++ b/VG Music Studio/VG Music Studio.csproj @@ -160,6 +160,7 @@ + From 5fb3bac39389e55000d0232702aa17360f44bbe1 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 9 Aug 2019 19:51:26 -0400 Subject: [PATCH 04/14] [PSF] Play song without events --- VG Music Studio/Core/PSX/PSF/Player.cs | 178 +++++++++++++++++-------- VG Music Studio/Core/PSX/PSF/Track.cs | 2 - 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index def40caa..b79d9c0d 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -16,10 +16,14 @@ internal class Player : IPlayer private readonly Config config; private readonly TimeBarrier time; private Thread thread; + private byte[] exeBuffer; private VAB vab; - private ushort initialTempo; + private long dataOffset; + private byte runningStatus; + private uint tempoMicroseconds; private ushort tempo; private int tempoStack; + private long deltaTicks; private long elapsedLoops; private bool fadeOutBegan; @@ -57,26 +61,18 @@ private void WaitThread() } } - private uint ReadVarLen(EndianBinaryReader reader) - { - 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; - } - private void InitEmulation() { - tempo = initialTempo; - tempoStack = 0; - elapsedLoops = ElapsedTicks = 0; + dataOffset = SongOffset; + dataOffset += 4; // "pQES" + dataOffset += 4; // Version + dataOffset += 2; // PPQN + tempoMicroseconds = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); + tempo = (ushort)(60000000 / tempoMicroseconds); + dataOffset += 2; // Time Signature + dataOffset += 4; // Unknown + + elapsedLoops = ElapsedTicks = deltaTicks = tempoStack = runningStatus = 0; fadeOutBegan = false; for (int i = 0; i < tracks.Length; i++) { @@ -85,16 +81,31 @@ private void InitEmulation() } public void LoadSong(long index) { - PSF.Open(config.BGMFiles[index], out byte[] exeBuffer, out _); + 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(); // PPQN - initialTempo = (ushort)(60000000 / (uint)((reader.ReadUInt16() << 8) | (reader.ReadByte()))); + reader.ReadBytes(3); // Initial Tempo reader.ReadBytes(2); // Time signature reader.ReadBytes(4); // Unknown Events = new List[0x10]; @@ -102,7 +113,7 @@ public void LoadSong(long index) { Events[i] = new List(); } - byte runningStatus = 0; + runningStatus = 0; byte curTrack = 0; MaxTicks = 0; @@ -120,7 +131,7 @@ void AddEvent(ICommand command) ev.Ticks.Add(MaxTicks); Events[curTrack].Add(ev); } - MaxTicks += ReadVarLen(reader); + MaxTicks += ReadVarLen(); byte cmd = reader.ReadByte(); void Invalid() { @@ -208,7 +219,7 @@ void Invalid() } case 0x51: { - uint tempo = (uint)((reader.ReadUInt16() << 8) | (reader.ReadByte())); + uint tempo = (uint)((reader.ReadUInt16() << 8) | reader.ReadByte()); if (!EventExists(offset)) { AddEvent(new TempoCommand { Tempo = tempo }); @@ -332,8 +343,8 @@ public void GetSongState(UI.SongInfoControl.SongInfo info) { Track track = tracks[i]; UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = track.CurEvent >= Events[i].Count ? 0 : Events[i][track.CurEvent].Offset; - //tin.Rest = track.Rest; + tin.Position = dataOffset; + tin.Rest = deltaTicks; tin.Voice = track.Voice; tin.Type = "PCM"; //tin.Volume = track.Volume; @@ -375,27 +386,48 @@ public void GetSongState(UI.SongInfoControl.SongInfo info) } } - private void ExecuteNext(int trackIndex) + private void ExecuteNext() { - bool increment = true; - List ev = Events[trackIndex]; - Track track = tracks[trackIndex]; - switch (ev[track.CurEvent].Command) + uint ReadVarLen() { - case FinishCommand _: + uint value; + byte c; + if (((value = exeBuffer[dataOffset++]) & 0x80) != 0) { - track.CurEvent = 0; // TODO: loops - break; + value &= 0x7F; + do + { + value = (uint)((value << 7) + ((c = exeBuffer[dataOffset++]) & 0x7F)); + } while ((c & 0x80) != 0); } - case NoteCommand note: + 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: { - if (note.Velocity == 0) + 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 == note.Key) + if (c.Key == key) { c.Stop(); break; @@ -404,23 +436,53 @@ private void ExecuteNext(int trackIndex) } else { - Channel channel = mixer.AllocateChannel(); - channel.Stop(); - channel.StartPSG(4, -1); - channel.Key = note.Key; - channel.NoteVelocity = note.Velocity; - channel.Owner = track; - track.Channels.Add(channel); + Channel c = mixer.AllocateChannel(); + c.Stop(); + c.StartPSG(4, -1); + c.Key = key; + c.NoteVelocity = velocity; + c.Owner = track; + track.Channels.Add(c); + } + break; + } + case 0xB0: + { + byte controller = exeBuffer[dataOffset++]; + byte value = exeBuffer[dataOffset++]; + 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: + { + track.Stopped = true; + break; + } + case 0x51: + { + tempoMicroseconds = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); + tempo = (ushort)(60000000 / tempoMicroseconds); + break; + } } break; } - case PitchBendCommand bend: track.PitchBend = bend.Bend; break; - case TempoCommand tem: tempo = (ushort)(60000000 / tem.Tempo); break; - case VoiceCommand voice: track.Voice = voice.Voice; break; - } - if (increment) - { - track.CurEvent++; } } @@ -432,14 +494,18 @@ private void Tick() while (tempoStack >= 24) { tempoStack -= 24; + if (deltaTicks > 0) + { + deltaTicks--; + } + while (deltaTicks == 0) + { + ExecuteNext(); + } bool allDone = true; - for (int i = 0; i < tracks.Length; i++) + for (int i = 0; i < 0x10; i++) { Track track = tracks[i]; - while (track.CurEvent < Events[i].Count && Events[i][track.CurEvent].Ticks.Contains(ElapsedTicks)) - { - ExecuteNext(i); - } if (!track.Stopped || track.Channels.Count != 0) { allDone = false; diff --git a/VG Music Studio/Core/PSX/PSF/Track.cs b/VG Music Studio/Core/PSX/PSF/Track.cs index 9c4a9d1c..ff1b9be6 100644 --- a/VG Music Studio/Core/PSX/PSF/Track.cs +++ b/VG Music Studio/Core/PSX/PSF/Track.cs @@ -9,7 +9,6 @@ internal class Track public byte Voice; public bool Stopped; public ushort PitchBend; - public int CurEvent; public readonly List Channels = new List(0x10); @@ -20,7 +19,6 @@ public Track(byte i) public void Init() { Voice = 0; - CurEvent = 0; PitchBend = 0; Stopped = false; StopAllChannels(); From 533be0687b21ecc9861503b389546f9e9fbb77f6 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 9 Aug 2019 20:30:25 -0400 Subject: [PATCH 05/14] Slight TimeBarrier optimization --- VG Music Studio/Util/TimeBarrier.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/VG Music Studio/Util/TimeBarrier.cs b/VG Music Studio/Util/TimeBarrier.cs index 0802c80a..c2ae3c0a 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; } From 82eb679189ae88da24732e21fd4059b46a67485c Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 9 Aug 2019 22:26:10 -0400 Subject: [PATCH 06/14] [PSF] Timing work --- VG Music Studio/Core/PSX/PSF/Channel.cs | 2 +- VG Music Studio/Core/PSX/PSF/Player.cs | 43 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Channel.cs b/VG Music Studio/Core/PSX/PSF/Channel.cs index f2aac4d3..aad9306c 100644 --- a/VG Music Studio/Core/PSX/PSF/Channel.cs +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -28,7 +28,7 @@ public void StartPSG(byte duty, int noteDuration) { psgCounter = 0; psgDuty = duty; - BaseTimer = 8006; + BaseTimer = 2000; Start(noteDuration); } diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index b79d9c0d..6c9f6ef1 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -11,6 +11,7 @@ namespace Kermalis.VGMusicStudio.Core.PSX.PSF internal class Player : IPlayer { private const long SongOffset = 0x120000; + private const int RefreshRate = 60; // 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; @@ -20,9 +21,12 @@ internal class Player : IPlayer private VAB vab; private long dataOffset; private byte runningStatus; - private uint tempoMicroseconds; + private ushort ticksPerQuarterNote; + private uint microsecondsPerBeat; + private uint microsecondsPerTick; + private long tickStack; + private long ticksPerUpdate; private ushort tempo; - private int tempoStack; private long deltaTicks; private long elapsedLoops; private bool fadeOutBegan; @@ -45,8 +49,7 @@ public Player(Mixer mixer, Config config) this.mixer = mixer; this.config = config; - time = new TimeBarrier(192); - //time = new TimeBarrier(60); // TODO: A PSF can determine refresh rate regardless of region; does this affect what we should put here? + time = new TimeBarrier(RefreshRate); } private void CreateThread() { @@ -61,18 +64,26 @@ private void WaitThread() } } + 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 - dataOffset += 2; // PPQN - tempoMicroseconds = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); - tempo = (ushort)(60000000 / tempoMicroseconds); - dataOffset += 2; // Time Signature + 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 - elapsedLoops = ElapsedTicks = deltaTicks = tempoStack = runningStatus = 0; + tickStack = elapsedLoops = ElapsedTicks = deltaTicks = runningStatus = 0; fadeOutBegan = false; for (int i = 0; i < tracks.Length; i++) { @@ -104,8 +115,8 @@ uint ReadVarLen() reader.Endianness = Endianness.BigEndian; reader.ReadString(4); // "pQES" reader.ReadUInt32(); // Version - reader.ReadUInt16(); // PPQN - reader.ReadBytes(3); // Initial Tempo + 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]; @@ -476,8 +487,8 @@ uint ReadVarLen() } case 0x51: { - tempoMicroseconds = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); - tempo = (ushort)(60000000 / tempoMicroseconds); + microsecondsPerBeat = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); + TEMPORARY_UpdateTimeVars(); break; } } @@ -491,9 +502,9 @@ private void Tick() time.Start(); while (State == PlayerState.Playing || State == PlayerState.Recording) { - while (tempoStack >= 24) + while (tickStack > microsecondsPerTick) { - tempoStack -= 24; + tickStack -= microsecondsPerTick; if (deltaTicks > 0) { deltaTicks--; @@ -535,7 +546,7 @@ private void Tick() SongEnded?.Invoke(); } } - tempoStack += tempo; + tickStack += ticksPerUpdate; mixer.ChannelTick(); mixer.Process(State == PlayerState.Playing, State == PlayerState.Recording); if (State == PlayerState.Playing) From bb4f4a4da4b78591176216fdf99f9d0f67962b90 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 12 Aug 2019 16:59:35 -0400 Subject: [PATCH 07/14] [PSF] Add tones structure --- VG Music Studio/Core/PSX/PSF/Player.cs | 3 +- VG Music Studio/Core/PSX/PSF/VAB.cs | 48 ++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index 6c9f6ef1..044ba5a1 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -10,7 +10,8 @@ namespace Kermalis.VGMusicStudio.Core.PSX.PSF { internal class Player : IPlayer { - private const long SongOffset = 0x120000; + private const long SongOffset = 0x120000; // Crash Bandicoot 2 + private const long SamplesOffset = 0x140000; // Crash Bandicoot 2 private const int RefreshRate = 60; // TODO: A PSF can determine refresh rate regardless of region private readonly Track[] tracks = new Track[0x10]; private readonly Mixer mixer; diff --git a/VG Music Studio/Core/PSX/PSF/VAB.cs b/VG Music Studio/Core/PSX/PSF/VAB.cs index 850ae47a..302b1d8e 100644 --- a/VG Music Studio/Core/PSX/PSF/VAB.cs +++ b/VG Music Studio/Core/PSX/PSF/VAB.cs @@ -15,13 +15,47 @@ public class Program 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 byte[][] Tones { get; } + public Instrument[] Instruments { get; } public (long Offset, long Size)[] VAGs { get; } public VAB(EndianBinaryReader reader) @@ -45,22 +79,22 @@ public VAB(EndianBinaryReader reader) // Programs Programs = new Program[0x80]; - for (int i = 0; i < Programs.Length; i++) + for (int i = 0; i < 0x80; i++) { Programs[i] = reader.ReadObject(); } - // Tones (Unknown structure) - Tones = new byte[0x10 * NumPrograms][]; - for (int i = 0; i < Tones.Length; i++) + // Instruments + Instruments = new Instrument[NumPrograms]; + for (int i = 0; i < NumPrograms; i++) { - Tones[i] = reader.ReadBytes(0x20); + Instruments[i] = reader.ReadObject(); } // VAG Pointers VAGs = new (long Offset, long Size)[0xFF]; long offset = reader.ReadUInt16() * 8; - for (int i = 0; i < VAGs.Length; i++) + for (int i = 0; i < 0xFF; i++) { long size = reader.ReadUInt16() * 8; VAGs[i] = (offset, size); From 086bb8c714b08d31f7107b69386c13902e27206c Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 12 Aug 2019 17:12:51 -0400 Subject: [PATCH 08/14] [PSF] Also scan for .minipsf --- VG Music Studio/Core/PSX/PSF/Config.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VG Music Studio/Core/PSX/PSF/Config.cs b/VG Music Studio/Core/PSX/PSF/Config.cs index 2d4cad59..05ccd69a 100644 --- a/VG Music Studio/Core/PSX/PSF/Config.cs +++ b/VG Music Studio/Core/PSX/PSF/Config.cs @@ -1,6 +1,7 @@ using Kermalis.VGMusicStudio.Properties; using System; using System.IO; +using System.Linq; namespace Kermalis.VGMusicStudio.Core.PSX.PSF { @@ -12,7 +13,7 @@ internal class Config : Core.Config public Config(string bgmPath) { BGMPath = bgmPath; - BGMFiles = Directory.GetFiles(bgmPath, "*.psf", SearchOption.TopDirectoryOnly); + 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); From d4ff9b6207cc025aed20ea35b61b962c6c7f1395 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 12 Aug 2019 21:24:23 -0400 Subject: [PATCH 09/14] [PSF] Instruments (no adsr) --- VG Music Studio/Core/PSX/PSF/Channel.cs | 102 +++++++++++++++++++----- VG Music Studio/Core/PSX/PSF/Mixer.cs | 6 +- VG Music Studio/Core/PSX/PSF/Player.cs | 32 ++++++-- 3 files changed, 108 insertions(+), 32 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Channel.cs b/VG Music Studio/Core/PSX/PSF/Channel.cs index aad9306c..e286bb50 100644 --- a/VG Music Studio/Core/PSX/PSF/Channel.cs +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -5,40 +5,93 @@ internal class Channel public readonly byte Index; public Track Owner; - public ushort BaseTimer, Timer; - public int NoteDuration; + public ushort BaseTimer = NDS.Utils.ARM7_CLOCK / 44100, Timer; public byte NoteVelocity; public byte Volume = 0x7F; public sbyte Pan = 0x40; - public byte Key; + public byte BaseKey, Key; + public byte PitchTune; private int pos; private short prevLeft, prevRight; - // PSG - private byte psgDuty; - private int psgCounter; + private long dataOffset; + private long loopOffset; + private short[] decompressedSample; public Channel(byte i) { Index = i; } - public void StartPSG(byte duty, int noteDuration) + private static readonly float[][] idk = new float[5][] { - psgCounter = 0; - psgDuty = duty; - BaseTimer = 2000; - Start(noteDuration); - } - - private void Start(int noteDuration) + 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 } + }; + private static bool beeee = true; + public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) { + Stop(); //State = EnvelopeState.Attack; //Velocity = -92544; pos = 0; prevLeft = prevRight = 0; - NoteDuration = noteDuration; + 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; + } + } + if (beeee) + { + beeee = false; + byte[] result = new byte[decompressedSample.Length * sizeof(short)]; + System.Buffer.BlockCopy(decompressedSample, 0, result, 0, result.Length); + System.IO.File.WriteAllBytes(sampleOffset.ToString("X") + ".bin", result); + } } public void Stop() @@ -60,12 +113,23 @@ public void Process(out short left, out short right) // prevLeft and prevRight are stored because numSamples can be 0. for (int i = 0; i < numSamples; i++) { - short samp = psgCounter <= psgDuty ? short.MinValue : short.MaxValue; - psgCounter++; - if (psgCounter >= 8) + short samp; + // If hit end + if (dataOffset >= decompressedSample.Length) { - psgCounter = 0; + 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); diff --git a/VG Music Studio/Core/PSX/PSF/Mixer.cs b/VG Music Studio/Core/PSX/PSF/Mixer.cs index ce16110b..62b3b57d 100644 --- a/VG Music Studio/Core/PSX/PSF/Mixer.cs +++ b/VG Music Studio/Core/PSX/PSF/Mixer.cs @@ -75,12 +75,8 @@ public void ChannelTick() if (chan.Owner != null) { //chan.StepEnvelope(); - if (chan.NoteDuration == 0) - { - //chan.State = EnvelopeState.Release; - } int vol = NDS.SDAT.Utils.SustainTable[chan.NoteVelocity] + 0; - int pitch = (chan.Key - 60) << 6; // "<< 6" is "* 0x40" + int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.PitchTune; // "<< 6" is "* 0x40" if (/*chan.State == EnvelopeState.Release && */vol <= -92544) { chan.Stop(); diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index 044ba5a1..a492fbd7 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -12,7 +12,7 @@ internal class Player : IPlayer { private const long SongOffset = 0x120000; // Crash Bandicoot 2 private const long SamplesOffset = 0x140000; // Crash Bandicoot 2 - private const int RefreshRate = 60; // TODO: A PSF can determine refresh rate regardless of region + 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; @@ -448,13 +448,29 @@ uint ReadVarLen() } else { - Channel c = mixer.AllocateChannel(); - c.Stop(); - c.StartPSG(4, -1); - c.Key = key; - c.NoteVelocity = velocity; - c.Owner = track; - track.Channels.Add(c); + 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; } From 8c34816c771d334b60332dc774ee8588ed785c1c Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Thu, 15 Aug 2019 14:30:15 -0400 Subject: [PATCH 10/14] Some cleanup --- VG Music Studio/Core/PSX/PSF/Channel.cs | 8 -------- VG Music Studio/Core/PSX/PSF/PSF.cs | 11 +++++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Channel.cs b/VG Music Studio/Core/PSX/PSF/Channel.cs index e286bb50..ecfb38f1 100644 --- a/VG Music Studio/Core/PSX/PSF/Channel.cs +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -32,7 +32,6 @@ public Channel(byte i) new float[2] { 98f / 64f, 55f / 64f }, new float[2] { 122f / 64f, 60f / 64f } }; - private static bool beeee = true; public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) { Stop(); @@ -85,13 +84,6 @@ public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) prev2 = p2; } } - if (beeee) - { - beeee = false; - byte[] result = new byte[decompressedSample.Length * sizeof(short)]; - System.Buffer.BlockCopy(decompressedSample, 0, result, 0, result.Length); - System.IO.File.WriteAllBytes(sampleOffset.ToString("X") + ".bin", result); - } } public void Stop() diff --git a/VG Music Studio/Core/PSX/PSF/PSF.cs b/VG Music Studio/Core/PSX/PSF/PSF.cs index 95d21603..56c617c4 100644 --- a/VG Music Studio/Core/PSX/PSF/PSF.cs +++ b/VG Music Studio/Core/PSX/PSF/PSF.cs @@ -31,8 +31,14 @@ private PSF(string fileName) FileBytes = File.ReadAllBytes(fileName); using (var reader = new EndianBinaryReader(new MemoryStream(FileBytes))) { - reader.ReadString(3); // PSF - reader.ReadByte(); // Version + 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(); @@ -58,6 +64,7 @@ private static void LoadLib1(PSF psf, byte[] exeBuffer) LoadLib1(lib, exeBuffer); LoadLib2(lib, exeBuffer); PlaceEXE(lib, exeBuffer); + break; } } } From 365aa0aa5171a12814736a6f58491418ec290624 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Tue, 20 Aug 2019 15:59:12 -0400 Subject: [PATCH 11/14] [PSF] Looping --- VG Music Studio/Core/PSX/PSF/Player.cs | 59 +++++++++++++++++--------- VG Music Studio/Core/PSX/PSF/Track.cs | 2 - 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index a492fbd7..b9f226ef 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -20,7 +20,7 @@ internal class Player : IPlayer private Thread thread; private byte[] exeBuffer; private VAB vab; - private long dataOffset; + private long dataOffset, startOffset, loopOffset; private byte runningStatus; private ushort ticksPerQuarterNote; private uint microsecondsPerBeat; @@ -84,7 +84,8 @@ private void InitEmulation() double ts2 = Math.Pow(2, exeBuffer[dataOffset++]); dataOffset += 4; // Unknown - tickStack = elapsedLoops = ElapsedTicks = deltaTicks = runningStatus = 0; + startOffset = dataOffset; + loopOffset = tickStack = elapsedLoops = ElapsedTicks = deltaTicks = runningStatus = 0; fadeOutBegan = false; for (int i = 0; i < tracks.Length; i++) { @@ -478,6 +479,26 @@ uint ReadVarLen() { 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: @@ -499,7 +520,7 @@ uint ReadVarLen() { case 0x2F: { - track.Stopped = true; + dataOffset = startOffset; break; } case 0x51: @@ -530,18 +551,23 @@ private void Tick() { ExecuteNext(); } - bool allDone = true; - for (int i = 0; i < 0x10; i++) + if (ElapsedTicks == MaxTicks) { - Track track = tracks[i]; - if (!track.Stopped || track.Channels.Count != 0) + for (int i = 0; i < 0x10; i++) { - allDone = false; + 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; + } + } } - } - if (ElapsedTicks == MaxTicks) - { - ElapsedTicks = 0; + throw new Exception(); + doneSearch: elapsedLoops++; if (ShouldFadeOut && !fadeOutBegan && elapsedLoops > NumLoops) { @@ -553,15 +579,6 @@ private void Tick() { ElapsedTicks++; } - if (fadeOutBegan && mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } } tickStack += ticksPerUpdate; mixer.ChannelTick(); diff --git a/VG Music Studio/Core/PSX/PSF/Track.cs b/VG Music Studio/Core/PSX/PSF/Track.cs index ff1b9be6..263cc28e 100644 --- a/VG Music Studio/Core/PSX/PSF/Track.cs +++ b/VG Music Studio/Core/PSX/PSF/Track.cs @@ -7,7 +7,6 @@ internal class Track public readonly byte Index; public byte Voice; - public bool Stopped; public ushort PitchBend; public readonly List Channels = new List(0x10); @@ -20,7 +19,6 @@ public void Init() { Voice = 0; PitchBend = 0; - Stopped = false; StopAllChannels(); } From e35e941f47daf9d17e71bac6647fb739a98e4695 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Fri, 11 Oct 2019 10:02:08 -0400 Subject: [PATCH 12/14] [PSF] Split bend command into two arguments --- VG Music Studio/Core/PSX/PSF/Commands.cs | 5 +++-- VG Music Studio/Core/PSX/PSF/Player.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Commands.cs b/VG Music Studio/Core/PSX/PSF/Commands.cs index 759dcd1f..92be90fb 100644 --- a/VG Music Studio/Core/PSX/PSF/Commands.cs +++ b/VG Music Studio/Core/PSX/PSF/Commands.cs @@ -30,9 +30,10 @@ internal class PitchBendCommand : ICommand { public Color Color => Color.MediumPurple; public string Label => "Pitch Bend"; - public string Arguments => Bend.ToString(); + public string Arguments => $"0x{Bend1:X} 0x{Bend2:X}"; - public ushort Bend { get; set; } + public byte Bend1 { get; set; } + public byte Bend2 { get; set; } } internal class TempoCommand : ICommand { diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index b9f226ef..15e98de3 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -209,10 +209,11 @@ void Invalid() } case 0xE0: { - ushort bend = reader.ReadUInt16(); + byte bend1 = reader.ReadByte(); + byte bend2 = reader.ReadByte(); if (!EventExists(offset)) { - AddEvent(new PitchBendCommand { Bend = bend }); + AddEvent(new PitchBendCommand { Bend1 = bend1, Bend2 = bend2 }); } break; } From 0e6e1df75a65a6ec9993f1d751f35b7d0f77c969 Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Sat, 18 Jan 2020 16:36:26 -0500 Subject: [PATCH 13/14] Update Config.cs --- VG Music Studio/Core/PSX/PSF/Config.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/VG Music Studio/Core/PSX/PSF/Config.cs b/VG Music Studio/Core/PSX/PSF/Config.cs index 05ccd69a..0cd7ea75 100644 --- a/VG Music Studio/Core/PSX/PSF/Config.cs +++ b/VG Music Studio/Core/PSX/PSF/Config.cs @@ -26,5 +26,12 @@ public Config(string bgmPath) } Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); } + + public override string GetSongName(long index) + { + return index < 0 || index >= BGMFiles.Length + ? index.ToString() + : '\"' + BGMFiles[index] + '\"'; + } } } From 044e1bb126994fe137d09501a2efeb2a8fafed1f Mon Sep 17 00:00:00 2001 From: Kermalis <29823718+Kermalis@users.noreply.github.com> Date: Mon, 17 Feb 2020 18:23:36 -0500 Subject: [PATCH 14/14] [PSF] Formatting + process last frame before firing SongEnded event --- VG Music Studio/Core/PSX/PSF/Channel.cs | 62 ++++---- VG Music Studio/Core/PSX/PSF/Config.cs | 4 +- VG Music Studio/Core/PSX/PSF/Mixer.cs | 87 ++++++---- VG Music Studio/Core/PSX/PSF/PSF.cs | 4 +- VG Music Studio/Core/PSX/PSF/Player.cs | 201 +++++++++++++----------- VG Music Studio/Core/PSX/PSF/VAB.cs | 4 +- 6 files changed, 196 insertions(+), 166 deletions(-) diff --git a/VG Music Studio/Core/PSX/PSF/Channel.cs b/VG Music Studio/Core/PSX/PSF/Channel.cs index ecfb38f1..621890ab 100644 --- a/VG Music Studio/Core/PSX/PSF/Channel.cs +++ b/VG Music Studio/Core/PSX/PSF/Channel.cs @@ -5,26 +5,28 @@ internal class Channel public readonly byte Index; public Track Owner; - public ushort BaseTimer = NDS.Utils.ARM7_CLOCK / 44100, Timer; + 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, prevRight; + private int _pos; + private short _prevLeft; + private short _prevRight; - private long dataOffset; - private long loopOffset; - private short[] decompressedSample; + private long _dataOffset; + private long _loopOffset; + private short[] _decompressedSample; public Channel(byte i) { Index = i; } - private static readonly float[][] idk = new float[5][] + private static readonly float[][] _idk = new float[5][] { new float[2] { 0f, 0f }, new float[2] { 60f / 64f, 0f }, @@ -37,12 +39,12 @@ public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) Stop(); //State = EnvelopeState.Attack; //Velocity = -92544; - pos = 0; - prevLeft = prevRight = 0; - loopOffset = 0; - dataOffset = 0; + _pos = 0; + _prevLeft = _prevRight = 0; + _loopOffset = 0; + _dataOffset = 0; float prev1 = 0, prev2 = 0; - decompressedSample = new short[0x50000]; + _decompressedSample = new short[0x50000]; for (long i = 0; i < sampleSize; i += 16) { byte b0 = exeBuffer[sampleOffset + i]; @@ -59,24 +61,24 @@ public void Start(long sampleOffset, long sampleSize, byte[] exeBuffer) 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); + _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]; + prev1 = _decompressedSample[pi + 27]; + prev2 = _decompressedSample[pi + 26]; } else { - float f1 = idk[filter][0]; - float f2 = idk[filter][1]; + 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; + float t = _decompressedSample[pi + j] + (p1 * f1) - (p2 * f2); + _decompressedSample[pi + j] = (short)t; p2 = p1; p1 = t; } @@ -100,35 +102,35 @@ public void Process(out short left, out short right) { if (Timer != 0) { - int numSamples = (pos + 0x100) / Timer; - pos = (pos + 0x100) % Timer; + 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 (_dataOffset >= _decompressedSample.Length) { if (true) //if (swav.DoesLoop) { - dataOffset = loopOffset; + _dataOffset = _loopOffset; } else { - left = right = prevLeft = prevRight = 0; + left = right = _prevLeft = _prevRight = 0; Stop(); return; } } - samp = decompressedSample[dataOffset++]; + samp = _decompressedSample[_dataOffset++]; samp = (short)(samp * Volume / 0x7F); - prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); - prevRight = (short)(samp * (Pan + 0x40) / 0x80); + _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); + _prevRight = (short)(samp * (Pan + 0x40) / 0x80); } } - left = prevLeft; - right = prevRight; + left = _prevLeft; + right = _prevRight; } } } diff --git a/VG Music Studio/Core/PSX/PSF/Config.cs b/VG Music Studio/Core/PSX/PSF/Config.cs index 0cd7ea75..e8860996 100644 --- a/VG Music Studio/Core/PSX/PSF/Config.cs +++ b/VG Music Studio/Core/PSX/PSF/Config.cs @@ -7,8 +7,8 @@ namespace Kermalis.VGMusicStudio.Core.PSX.PSF { internal class Config : Core.Config { - public string BGMPath; - public string[] BGMFiles; + public readonly string BGMPath; + public readonly string[] BGMFiles; public Config(string bgmPath) { diff --git a/VG Music Studio/Core/PSX/PSF/Mixer.cs b/VG Music Studio/Core/PSX/PSF/Mixer.cs index 62b3b57d..f339dc1d 100644 --- a/VG Music Studio/Core/PSX/PSF/Mixer.cs +++ b/VG Music Studio/Core/PSX/PSF/Mixer.cs @@ -5,20 +5,21 @@ namespace Kermalis.VGMusicStudio.Core.PSX.PSF { internal class Mixer : Core.Mixer { - private readonly float samplesReciprocal; - private readonly int samplesPerBuffer; - private long fadeMicroFramesLeft; - private float fadePos; - private float fadeStepPerMicroframe; + 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; + private readonly BufferedWaveProvider _buffer; public Mixer() { const int sampleRate = 65456; // TODO - samplesPerBuffer = 341; // TODO - samplesReciprocal = 1f / samplesPerBuffer; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; Channels = new Channel[0x10]; for (byte i = 0; i < 0x10; i++) @@ -26,12 +27,12 @@ public Mixer() Channels[i] = new Channel(i); } - buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) { DiscardOnBufferOverflow = true, - BufferLength = samplesPerBuffer * 64 + BufferLength = _samplesPerBuffer * 64 }; - Init(buffer); + Init(_buffer); } public override void Dispose() { @@ -93,49 +94,67 @@ public void ChannelTick() public void BeginFadeIn() { - fadePos = 0f; - fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - fadeStepPerMicroframe = 1f / fadeMicroFramesLeft; + _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; + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; } public bool IsFadeDone() { - return fadeMicroFramesLeft == 0; + return _isFading && _fadeMicroFramesLeft == 0; } public void ResetFade() { - fadeMicroFramesLeft = 0; + _isFading = false; + _fadeMicroFramesLeft = 0; } - private WaveFileWriter waveWriter; + private WaveFileWriter _waveWriter; public void CreateWaveWriter(string fileName) { - waveWriter = new WaveFileWriter(fileName, buffer.WaveFormat); + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); } public void CloseWaveWriter() { - waveWriter?.Dispose(); + _waveWriter?.Dispose(); } public void Process(bool output, bool recording) { - float fromMaster = 1f, toMaster = 1f; - if (fadeMicroFramesLeft > 0) + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else { - 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--; + 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; } - float masterStep = (toMaster - fromMaster) * samplesReciprocal; - float masterLevel = fromMaster; byte[] b = new byte[4]; - for (int i = 0; i < samplesPerBuffer; i++) + for (int i = 0; i < _samplesPerBuffer; i++) { int left = 0, right = 0; @@ -180,11 +199,11 @@ public void Process(bool output, bool recording) masterLevel += masterStep; if (output) { - buffer.AddSamples(b, 0, 4); + _buffer.AddSamples(b, 0, 4); } if (recording) { - waveWriter.Write(b, 0, 4); + _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 index 56c617c4..a7951a95 100644 --- a/VG Music Studio/Core/PSX/PSF/PSF.cs +++ b/VG Music Studio/Core/PSX/PSF/PSF.cs @@ -9,7 +9,7 @@ namespace Kermalis.VGMusicStudio.Core.PSX.PSF { internal class PSF { - private const int ExeBufferSize = 0x200000; + private const int _exeBufferSize = 0x200000; public string FilePath; public byte[] FileBytes; @@ -18,7 +18,7 @@ internal class PSF public static void Open(string fileName, out byte[] exeBuffer, out PSF psf) { - exeBuffer = new byte[ExeBufferSize]; + exeBuffer = new byte[_exeBufferSize]; psf = new PSF(fileName); LoadLib1(psf, exeBuffer); PlaceEXE(psf, exeBuffer); diff --git a/VG Music Studio/Core/PSX/PSF/Player.cs b/VG Music Studio/Core/PSX/PSF/Player.cs index 15e98de3..2da19cd4 100644 --- a/VG Music Studio/Core/PSX/PSF/Player.cs +++ b/VG Music Studio/Core/PSX/PSF/Player.cs @@ -13,24 +13,25 @@ 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, startOffset, 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; - private bool fadeOutBegan; + 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; } @@ -45,57 +46,57 @@ public Player(Mixer mixer, Config config) { for (byte i = 0; i < 0x10; i++) { - tracks[i] = new Track(i); + _tracks[i] = new Track(i); } - this.mixer = mixer; - this.config = config; + _mixer = mixer; + _config = config; - time = new TimeBarrier(RefreshRate); + _time = new TimeBarrier(RefreshRate); } private void CreateThread() { - thread = new Thread(Tick) { Name = "PSF Player Tick" }; - thread.Start(); + _thread = new Thread(Tick) { Name = "PSF Player Tick" }; + _thread.Start(); } private void WaitThread() { - if (thread != null && (thread.ThreadState == ThreadState.Running || thread.ThreadState == ThreadState.WaitSleepJoin)) + if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) { - thread.Join(); + _thread.Join(); } } private void TEMPORARY_UpdateTimeVars() { - tempo = (ushort)(60000000 / microsecondsPerBeat); - microsecondsPerTick = microsecondsPerBeat / ticksPerQuarterNote; - ticksPerUpdate = 1000000 / RefreshRate; + _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++]); + _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 + byte ts1 = _exeBuffer[_dataOffset++]; + double ts2 = Math.Pow(2, _exeBuffer[_dataOffset++]); + _dataOffset += 4; // Unknown - startOffset = dataOffset; - loopOffset = tickStack = elapsedLoops = ElapsedTicks = deltaTicks = runningStatus = 0; - fadeOutBegan = false; - for (int i = 0; i < tracks.Length; i++) + _startOffset = _dataOffset; + _loopOffset = _tickStack = _elapsedLoops = ElapsedTicks = _deltaTicks = _runningStatus = 0; + _mixer.ResetFade(); + for (int i = 0; i < _tracks.Length; i++) { - tracks[i].Init(); + _tracks[i].Init(); } } public void LoadSong(long index) { - PSF.Open(config.BGMFiles[index], out exeBuffer, out _); - using (var reader = new EndianBinaryReader(new MemoryStream(exeBuffer))) + PSF.Open(_config.BGMFiles[index], out _exeBuffer, out _); + using (var reader = new EndianBinaryReader(new MemoryStream(_exeBuffer))) { uint ReadVarLen() { @@ -112,7 +113,7 @@ uint ReadVarLen() return value; } - vab = new VAB(reader); + _vab = new VAB(reader); reader.BaseStream.Position = SongOffset; reader.Endianness = Endianness.BigEndian; reader.ReadString(4); // "pQES" @@ -126,7 +127,7 @@ uint ReadVarLen() { Events[i] = new List(); } - runningStatus = 0; + _runningStatus = 0; byte curTrack = 0; MaxTicks = 0; @@ -153,12 +154,12 @@ void Invalid() if (cmd <= 0x7F) { - cmd = runningStatus; + cmd = _runningStatus; reader.BaseStream.Position--; } else { - runningStatus = cmd; + _runningStatus = cmd; } curTrack = (byte)(cmd & 0xF); switch (cmd & 0xF0) @@ -335,12 +336,12 @@ public void Stop() } public void Record(string fileName) { - mixer.CreateWaveWriter(fileName); + _mixer.CreateWaveWriter(fileName); InitEmulation(); State = PlayerState.Recording; CreateThread(); WaitThread(); - mixer.CloseWaveWriter(); + _mixer.CloseWaveWriter(); } public void Dispose() { @@ -352,13 +353,13 @@ public void Dispose() } public void GetSongState(UI.SongInfoControl.SongInfo info) { - info.Tempo = tempo; + info.Tempo = _tempo; for (int i = 0; i < 0x10; i++) { - Track track = tracks[i]; + Track track = _tracks[i]; UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = dataOffset; - tin.Rest = deltaTicks; + tin.Position = _dataOffset; + tin.Rest = _deltaTicks; tin.Voice = track.Voice; tin.Type = "PCM"; //tin.Volume = track.Volume; @@ -406,35 +407,35 @@ uint ReadVarLen() { uint value; byte c; - if (((value = exeBuffer[dataOffset++]) & 0x80) != 0) + if (((value = _exeBuffer[_dataOffset++]) & 0x80) != 0) { value &= 0x7F; do { - value = (uint)((value << 7) + ((c = exeBuffer[dataOffset++]) & 0x7F)); + value = (uint)((value << 7) + ((c = _exeBuffer[_dataOffset++]) & 0x7F)); } while ((c & 0x80) != 0); } return value; } - deltaTicks = ReadVarLen(); - byte cmd = exeBuffer[dataOffset++]; + _deltaTicks = ReadVarLen(); + byte cmd = _exeBuffer[_dataOffset++]; if (cmd <= 0x7F) { - cmd = runningStatus; - dataOffset--; + cmd = _runningStatus; + _dataOffset--; } else { - runningStatus = cmd; + _runningStatus = cmd; } - Track track = tracks[cmd & 0xF]; + Track track = _tracks[cmd & 0xF]; switch (cmd & 0xF0) { case 0x90: { - byte key = exeBuffer[dataOffset++]; - byte velocity = exeBuffer[dataOffset++]; + byte key = _exeBuffer[_dataOffset++]; + byte velocity = _exeBuffer[_dataOffset++]; if (velocity == 0) { Channel[] chans = track.Channels.ToArray(); @@ -450,19 +451,19 @@ uint ReadVarLen() } else { - VAB.Program p = vab.Programs[track.Voice]; - VAB.Instrument ins = vab.Instruments[track.Voice]; + 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(); + (long sampleOffset, long sampleSize) = _vab.VAGs[t.SampleId - 1]; + Channel c = _mixer.AllocateChannel(); if (c != null) { - c.Start(sampleOffset + SamplesOffset, sampleSize, exeBuffer); + c.Start(sampleOffset + SamplesOffset, sampleSize, _exeBuffer); c.Key = key; c.BaseKey = t.BaseKey; c.PitchTune = t.PitchTune; @@ -478,8 +479,8 @@ uint ReadVarLen() } case 0xB0: { - byte controller = exeBuffer[dataOffset++]; - byte value = exeBuffer[dataOffset++]; + byte controller = _exeBuffer[_dataOffset++]; + byte value = _exeBuffer[_dataOffset++]; switch (controller) { case 0x63: @@ -488,12 +489,12 @@ uint ReadVarLen() { case 0x14: { - loopOffset = dataOffset; + _loopOffset = _dataOffset; break; } case 0x1E: { - dataOffset = loopOffset; + _dataOffset = _loopOffset; break; } } @@ -504,29 +505,29 @@ uint ReadVarLen() } case 0xC0: { - byte voice = exeBuffer[dataOffset++]; + byte voice = _exeBuffer[_dataOffset++]; track.Voice = voice; break; } case 0xE0: { - ushort pitchBend = (ushort)((exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); + ushort pitchBend = (ushort)((_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); track.PitchBend = pitchBend; break; } case 0xF0: { - byte meta = exeBuffer[dataOffset++]; + byte meta = _exeBuffer[_dataOffset++]; switch (meta) { case 0x2F: { - dataOffset = startOffset; + _dataOffset = _startOffset; break; } case 0x51: { - microsecondsPerBeat = (uint)((exeBuffer[dataOffset++] << 16) | (exeBuffer[dataOffset++] << 8) | exeBuffer[dataOffset++]); + _microsecondsPerBeat = (uint)((_exeBuffer[_dataOffset++] << 16) | (_exeBuffer[_dataOffset++] << 8) | _exeBuffer[_dataOffset++]); TEMPORARY_UpdateTimeVars(); break; } @@ -538,17 +539,25 @@ uint ReadVarLen() private void Tick() { - time.Start(); - while (State == PlayerState.Playing || State == PlayerState.Recording) + _time.Start(); + while (true) { - while (tickStack > microsecondsPerTick) + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) { - tickStack -= microsecondsPerTick; - if (deltaTicks > 0) + goto stop; + } + + while (_tickStack > _microsecondsPerTick) + { + _tickStack -= _microsecondsPerTick; + if (_deltaTicks > 0) { - deltaTicks--; + _deltaTicks--; } - while (deltaTicks == 0) + while (_deltaTicks == 0) { ExecuteNext(); } @@ -560,7 +569,7 @@ private void Tick() for (int j = 0; j < t.Count; j++) { SongEvent e = t[j]; - if (e.Offset == dataOffset) + if (e.Offset == _dataOffset) { ElapsedTicks = e.Ticks[0]; goto doneSearch; @@ -569,11 +578,10 @@ private void Tick() } throw new Exception(); doneSearch: - elapsedLoops++; - if (ShouldFadeOut && !fadeOutBegan && elapsedLoops > NumLoops) + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) { - fadeOutBegan = true; - mixer.BeginFadeOut(); + _mixer.BeginFadeOut(); } } else @@ -581,15 +589,16 @@ private void Tick() ElapsedTicks++; } } - tickStack += ticksPerUpdate; - mixer.ChannelTick(); - mixer.Process(State == PlayerState.Playing, State == PlayerState.Recording); - if (State == PlayerState.Playing) + _tickStack += _ticksPerUpdate; + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + if (playing) { - time.Wait(); + _time.Wait(); } } - time.Stop(); + stop: + _time.Stop(); } } } diff --git a/VG Music Studio/Core/PSX/PSF/VAB.cs b/VG Music Studio/Core/PSX/PSF/VAB.cs index 302b1d8e..a567923f 100644 --- a/VG Music Studio/Core/PSX/PSF/VAB.cs +++ b/VG Music Studio/Core/PSX/PSF/VAB.cs @@ -49,7 +49,7 @@ public class Instrument public Tone[] Tones { get; set; } } - private const long InstrumentsOffset = 0x130000; // Crash Bandicoot 2 + private const long _instrumentsOffset = 0x130000; // Crash Bandicoot 2 public ushort NumPrograms { get; } public ushort NumTones { get; } @@ -61,7 +61,7 @@ public class Instrument public VAB(EndianBinaryReader reader) { // Header - reader.BaseStream.Position = InstrumentsOffset; + reader.BaseStream.Position = _instrumentsOffset; reader.Endianness = Endianness.LittleEndian; reader.ReadString(4); // "pBAV" reader.ReadUInt32(); // Version