diff --git a/.gitignore b/.gitignore index e3b1e9b..f073e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ obj/ .DS_Store .directory thumbs.db + +Bass.Net.dll # provide it yourself diff --git a/ChuTools.Tests/BeReadWrite.cs b/ChuTools.Tests/BeReadWrite.cs new file mode 100644 index 0000000..50085e1 --- /dev/null +++ b/ChuTools.Tests/BeReadWrite.cs @@ -0,0 +1,27 @@ +using Be.IO; + +using BmsDerg.Utility; + +namespace ChuTools.Tests; + +[TestFixture] +public class BeReadWrite +{ + [Test] + [TestCase(10)] + [TestCase(200)] + [TestCase(20000)] + [TestCase(20000000)] + public static void TestVarInt(int value) + { + var ms = new MemoryStream(); + var writer = new BeBinaryWriter(ms); + writer.WriteVarInt(value); + + ms.Position = 0; + var reader = new BeBinaryReader(ms); + var readBack = reader.ReadVarInt32(); + + Assert.That(readBack, Is.EqualTo(value)); + } +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 7d0fd61..96bca08 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,9 +4,15 @@ true + - + + + + + + diff --git a/JAIMaker/.gitignore b/JAIMaker/.gitignore new file mode 100644 index 0000000..a53ce9e --- /dev/null +++ b/JAIMaker/.gitignore @@ -0,0 +1 @@ +Bass.Net.dll # Provide it yourself \ No newline at end of file diff --git a/JAIMaker/App.axaml b/JAIMaker/App.axaml new file mode 100644 index 0000000..104d823 --- /dev/null +++ b/JAIMaker/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/JAIMaker/App.axaml.cs b/JAIMaker/App.axaml.cs new file mode 100644 index 0000000..93c064c --- /dev/null +++ b/JAIMaker/App.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +using JaiSeqX.Player.BassBuff; + +namespace JaiMaker; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + Engine.Init(); // Start audio engine. + Keyboard.init(); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/JAIMaker/Assets/coollogo_com-29453469.png b/JAIMaker/Assets/coollogo_com-29453469.png new file mode 100644 index 0000000..676345d Binary files /dev/null and b/JAIMaker/Assets/coollogo_com-29453469.png differ diff --git a/JAIMaker/Assets/coollogo_com-3970742.png b/JAIMaker/Assets/coollogo_com-3970742.png new file mode 100644 index 0000000..959a0a5 Binary files /dev/null and b/JAIMaker/Assets/coollogo_com-3970742.png differ diff --git a/JAIMaker/BMSChannelManager.cs b/JAIMaker/BMSChannelManager.cs new file mode 100644 index 0000000..520adb6 --- /dev/null +++ b/JAIMaker/BMSChannelManager.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JaiSeqX.Player.BassBuff; + +namespace JaiSeqX.Player +{ + public class BMSChannel + { + SoundEffectInstance[] voices; + public SoundEffectInstance LastVoice; + public int ActiveVoices; + public BMSChannel() + { + voices = new SoundEffectInstance[256]; // Should only ever have 8 voices, but still. + } + + public void silence() + { + for (int i = 0; i < voices.Length; i++) + { + if (voices[i]!=null) + { + voices[i].Stop(); + } + } + } + + + public bool addVoice(int index, SoundEffectInstance voice) // to add a voice to a channel + { + if (index < voices.Length) // Make sure the index isn't stupid. + { + if (voices[index] != null) // Check if we already have a sound playing there. + { + stopVoice(index); // Stop it if we do. + } + voices[index] = voice; // Throw the voice into its index + LastVoice = voice; + ActiveVoices++; + return true; // success + } + return false; + } + + public bool stopVoice(int index) + { + if (index < voices.Length) + { // Make sure the index isnt stupid + if (voices[index] != null) // dont do any work if we dont have anything to do + { + var voi = voices[index]; // grab the voice + voi.Stop(); // Stop the voice + voi.Dispose(); // feed it to GC + voices[index] = null; // clear its index in the voice table + ActiveVoices--; + return true; // good + + } + } + return false; // we didnt do anything + } + + } + + public class BMSChannelManager + { + SoundEffect[] Cache; // Sound cache + string[] CacheStrings; // Maps the sound path to the cache index + public BMSChannel[] channels; // current channels + int cacheHigh; // Highest number in the cache we have + + bool[] bending; + + double[] bendtarget; + int[] bendtargetricks; + int[] bendticks; + + + float[] bendPitchBase; + + public BMSChannelManager() + { + + Cache = new SoundEffect[1024]; // I HOPE that the engine doesn't need more than 1024 sounds at once. + CacheStrings = new string[1024]; // ^ + + channels = new BMSChannel[32]; // Usually no more than 16 channels. Again, just to be safe + + bendtarget = new double[32]; + bending = new bool[32]; + bendPitchBase = new float[32]; + bendticks = new int[32]; + bendtargetricks = new int[32]; + + for (int i = 0; i < channels.Length; i++) + { + channels[i] = new BMSChannel(); // Preallocating the channels + } + + } + + public bool doPitchBend(byte channel, int bend, int duration, byte type) + { + var chn = channels[channel]; + + if (chn.LastVoice != null) + { + var voi = chn.LastVoice; + //Console.WriteLine("Add PitchBend: {0} {1} {2} ", channel, duration, bend); + bendPitchBase[channel] = voi.Pitch; // fuck + bending[channel] = true; // fuck + bendticks[channel] = 0; // fuck + bendtargetricks[channel] = duration; // fuck + + float target = 0; + if (type == 1) + { + target = (float)bend / 0xFF; + } + if (type == 2) + { + target = (float)bend / 0x7F; + } + if (type == 3) + { + target = (float)bend / 0x7FFF; + } + + //Console.WriteLine("Add Target {0} ", target); + + bendtarget[channel] = target; // fuck + + return true; + } + + return false; + } + + public bool onTick() + { + for (int chn = 0; chn < channels.Length; chn++) + { + // bend + + if (bending[chn]) + { + var bendChannel = channels[chn]; + bendticks[chn]++; + var ticks = bendticks[chn]; + var targetTicks = bendtargetricks[chn]; + if (ticks > targetTicks) + { + bending[chn] = false; + } + float bendPercent = ((float)ticks / targetTicks) < 1 ? ((float)ticks / targetTicks) : 1; + double semitones = bendtarget[chn] * bendPercent; + + if (bendChannel.LastVoice != null) + { + // Console.WriteLine("doing it "); + // var real_pitch = (float)Math.Pow(2, / 12f); + var voice = bendChannel.LastVoice; + var basepitch = bendPitchBase[chn]; + // var newpitch = (float)Math.Pow(2, / 12) ; + //double newpitch = Math.Pow(2,-semitones / 12 ); + + //Console.WriteLine("BEND DEGREE {0} {1}", newpitch,semitones); + //voice.Pitch = basepitch * (float)(newpitch); + } + + } + + } + + return true; + } + public SoundEffect loadSound(string file, bool lo, int ls, int le) + { + for (int i = 0; i < CacheStrings.Length; i++) + { + if (CacheStrings[i] == null || i > cacheHigh) // if we've hit null, we've hit the end of our array. + { + break; // So just stop the loop + } + else + { + if (CacheStrings[i] == file) // If we find it. + { + return Cache[i]; // Return the same index of the cache (will be our file) + } + } + } +#if DEBUG + var b = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("loadSound {0}", file); + Console.ForegroundColor = b; + + +#endif + CacheStrings[cacheHigh] = file; // otherwise, it's not loaded. so we need to store it in our cache + Cache[cacheHigh] = new SoundEffect(file, lo, ls, le); // Load the WAV for it. + var ret = Cache[cacheHigh]; // Then set our return value (we store it, because we increment cacheHigh below) + cacheHigh++; // Increment our next cache index. + + return ret; // Return our object. + } + + public void startVoice(SoundEffectInstance snd, byte channel, byte voice) + { + if (channels[channel] != null) // check if the channel exists + { + var chn = channels[channel]; // if it does, reference it + chn.addVoice(voice, snd); // store the voice + } + } + + public void silenceChannel(byte channel) + { + if (channels[channel] != null) // check if the channel exists + { + var chn = channels[channel]; // if it does, reference it + chn.silence(); + } + } + + public void stopVoice(byte channel, byte voice) + { + // see above, inverse. + if (channels[channel] != null) + { + var chn = channels[channel]; + chn.stopVoice(voice); + } + } + } +} diff --git a/JAIMaker/Bass.Net.dll b/JAIMaker/Bass.Net.dll new file mode 100644 index 0000000..df21e14 Binary files /dev/null and b/JAIMaker/Bass.Net.dll differ diff --git a/JAIMaker/BassBuff/Engine.CS b/JAIMaker/BassBuff/Engine.CS new file mode 100644 index 0000000..8397d8c --- /dev/null +++ b/JAIMaker/BassBuff/Engine.CS @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Un4seen.Bass; +using Un4seen.Bass.AddOn.Fx; + +namespace JaiSeqX.Player.BassBuff +{ + public static class Engine + { + public static SYNCPROC globalLoopProc; + public static SYNCPROC g_FadeFreeProc; + + public static void Init() + { + #region dumb obfuscation for email and registration key, just to prevent bots. + byte obfu = 0xDA; + byte[] eml = new byte[] + { + 0xBE, 0xBB, 0xB4,0xBF,0x9A,0xA2,0xBB,0xA3,0xA8,0xF4,0xBD,0xBB, + }; + + byte[] rkey = new byte[] + { + 0xE8,0x82,0xE3,0xE9,0xE8,0xE9,0xEB,0xE8,0xEE,0xE9,0xE9, + }; + for (int i=0; i < eml.Length;i++) + { + eml[i] ^=(obfu); + } + for (int i = 0; i < rkey.Length; i++) + { + rkey[i] ^= (obfu); + } + #endregion + + Un4seen.Bass.BassNet.Registration(Encoding.ASCII.GetString(eml), Encoding.ASCII.GetString(rkey)); // Registration code, feel free to email me. + // Note that because of this, JaiSeqX cannot be used for commercial purposes. + Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero); // Initialize audio engine + // BassFx.LoadMe(); // Load the effects library + + globalLoopProc = new SYNCPROC(DoLoop); // Create our loop proc to bind audio objects to, global and static so it doesn't get collected. + g_FadeFreeProc = new SYNCPROC(FadeCollect); + + + BASS_DEVICEINFO info = new BASS_DEVICEINFO(); // Print device info. + for (int n = 0; Bass.BASS_GetDeviceInfo(n, info); n++) + { + Console.WriteLine(info.ToString()); + } + } + + + private static void DoLoop(int syncHandle, int channel, int data, IntPtr user) + { + Bass.BASS_ChannelSetPosition(channel, user.ToInt64()); + } + + private static void FadeCollect(int syncHandle, int channel, int data, IntPtr user) + { + Bass.BASS_ChannelRemoveSync(channel, syncHandle); + Bass.BASS_StreamFree(channel); + //Console.Write("Dealloc {0}", channel); + + } + + } +} diff --git a/JAIMaker/BassBuff/SoundEffect.cs b/JAIMaker/BassBuff/SoundEffect.cs new file mode 100644 index 0000000..ace5797 --- /dev/null +++ b/JAIMaker/BassBuff/SoundEffect.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Un4seen.Bass; +using System.Runtime; +using System.Runtime.InteropServices; + +namespace JaiSeqX.Player.BassBuff +{ + + + public class SoundEffect : IDisposable + { + + public bool loop; // Do we loop + public int loopstart; // Where do we start + public int loopend; // Where do we end + public string dwave; // Wav PCM path + public SoundEffect(string wav) + { + dwave = wav; // store pcm path + } + + public SoundEffect(string wav, bool lo, int loops, int loope) + { + + // See comments above for info. + + loop = lo; + loopstart = loops; + loopend = loope; + dwave = wav; + + } + + public SoundEffectInstance CreateInstance() + { + var stream = Bass.BASS_StreamCreateFile(dwave, 0, 0, BASSFlag.BASS_DEFAULT); // Allocate the new stream + return new SoundEffectInstance(stream, loop, loopstart, loopend); // Pass our instance parameters to the sound. + + } + + + public void Dispose() + { + + } + } +} diff --git a/JAIMaker/BassBuff/SoundEffectInstance.cs b/JAIMaker/BassBuff/SoundEffectInstance.cs new file mode 100644 index 0000000..4d41e8e --- /dev/null +++ b/JAIMaker/BassBuff/SoundEffectInstance.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Un4seen.Bass; +using Un4seen.Bass.AddOn.Fx; + +namespace JaiSeqX.Player.BassBuff +{ + public class SoundEffectInstance : IDisposable + { + int handle = 0; // BASS Handle + + private float iPitch = 1; + private float iVolume = 1; + + private int syncHandle; + private float baseSRate = 0; + private bool looping; + + private bool fading; + + public bool ShouldFade = false; + public int FadeOutMS = 0; + + public SoundEffectInstance(int bassHandle, bool loop, int loopstart, int loopend) + { + handle = bassHandle; // Store the handle + Bass.BASS_ChannelGetAttribute(handle, BASSAttribute.BASS_ATTRIB_FREQ, ref baseSRate); // Store the original sample rate (for pitch bending) + var sz = Bass.BASS_ChannelGetLength(handle); + + if (loopend > sz | (loopend - loopstart) < 3000) // Hax until i figure out wtf is going on with looping. + { + loopend = (int)sz; + + } + //Console.WriteLine("{0} {1}", loopstart, loopend); + if (loop) // If we loop + { + syncHandle = Bass.BASS_ChannelSetSync(handle, BASSSync.BASS_SYNC_POS | BASSSync.BASS_SYNC_MIXTIME, loopend, Engine.globalLoopProc, new IntPtr(loopstart));// Set the global loop proc to take place at the loop end position, then return to the start. + } + looping = loop; // Loopyes + } + + public float Pitch + { + get + { + return iPitch; + } + set + {// + Bass.BASS_ChannelSetAttribute(handle, BASSAttribute.BASS_ATTRIB_FREQ, baseSRate * value); // Change the frequency of the sound + iPitch = value; + + } + } + + + + public float Volume + { + get + { + return iVolume; + } + set + { + Bass.BASS_ChannelSetAttribute(handle, BASSAttribute.BASS_ATTRIB_VOL, value); // Change the volume of the sound + iVolume = value; + } + } + public void Play() + { + Bass.BASS_ChannelSetAttribute(handle, BASSAttribute.BASS_ATTRIB_FREQ, baseSRate * iPitch); // For good measure, unsure if needed. + Bass.BASS_ChannelPlay(handle, true); // Tell it to play + + + } + + public void FadeStop(int miliseconds) + { + fading = true; + Bass.BASS_ChannelSlideAttribute(handle, BASSAttribute.BASS_ATTRIB_VOL, 0, miliseconds); + Bass.BASS_ChannelSetSync(handle, BASSSync.BASS_SYNC_SLIDE, 0, Engine.g_FadeFreeProc, new IntPtr(0)); + + } + + public void Stop() + { + //Bass.BASS_ChannelSlideAttribute(handle, BASSAttribute.BASS_ATTRIB_VOL, 0F, 1000); + if (ShouldFade) + { + FadeStop(FadeOutMS); + return; + } + Bass.BASS_ChannelStop(handle); // Tell it to stop + } + public void Dispose() // Let the object dispose. + { + if (!fading) + { + Stop(); // If it's being collected, stop it first. + + Bass.BASS_StreamFree(handle); // Then finally, we can free the stream, as the sound is no longer used in any way. + } + // Let the GC do its thing, i guess. + if (looping) // If it loops + { + // We need to deallocate the sync proc + Bass.BASS_ChannelRemoveSync(handle, syncHandle); + } + + } + + } +} + \ No newline at end of file diff --git a/JAIMaker/INAFile.cs b/JAIMaker/INAFile.cs new file mode 100644 index 0000000..fd88f48 --- /dev/null +++ b/JAIMaker/INAFile.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace JaiMaker +{ + + + public static class INAFile + { + public static Dictionary> parse(string file) + { + var RETL = new Dictionary>(); + var STARR = File.ReadAllLines(file); + + var currentBank = 0; + var BankDict = new Dictionary(); + RETL[currentBank] = BankDict; + + for (int line = 0; line < STARR.Length; line++) + { + var currentLine = STARR[line]; + if (currentLine.Length > 1) // Ignore blank lines. + { + if (currentLine[0] == ':') + { + var newBank = currentLine.Substring(1); + var newBankNumber = Convert.ToInt32(newBank); + BankDict = new Dictionary(); + currentBank = newBankNumber; + RETL[currentBank] = BankDict; + } else if (currentLine[0]=='/' || currentLine[0] == '\r' || currentLine[0] == '\n') { + // do nothing, comment. + } + else + { + if (currentLine.Contains("=")) + { + var args = currentLine.Split('='); + try + { + var indexNumber = Convert.ToInt32(args[0]); + var name = args[1]; + Console.WriteLine("BANK {0} {1} {2}", currentBank, indexNumber,name); + BankDict[indexNumber] = name; + } catch { + Console.WriteLine("Malformmed line in {0}, line {1}", file, line); + }; + + } else + { + Console.WriteLine("Malformmed line in {0}, line {1}", file, line); + } + } + } + + } + return RETL; + } + + } +} diff --git a/JAIMaker/JAI/AABase.cs b/JAIMaker/JAI/AABase.cs new file mode 100644 index 0000000..23b56aa --- /dev/null +++ b/JAIMaker/JAI/AABase.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JaiSeqX.JAI.Seq; +using JaiSeqX.JAI.Types; +using JaiSeqX.JAI.Types.WSYS; +using System.IO; + +namespace JaiSeqX.JAI +{ + public abstract class AABase + { + public WaveSystem[] WSYS; + public InstrumentBank[] IBNK; + } +} diff --git a/JAIMaker/JAI/AAFFile.cs b/JAIMaker/JAI/AAFFile.cs new file mode 100644 index 0000000..78e2089 --- /dev/null +++ b/JAIMaker/JAI/AAFFile.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JaiSeqX.JAI.Seq; +using JaiSeqX.JAI.Types; +using JaiSeqX.JAI.Types.WSYS; +using System.IO; +using Be.IO; + +namespace JaiSeqX.JAI +{ + + + public class AAFFile : AABase + { + + private string convertChunkName(uint id) + { + switch (id) + { + case 0: + return "End marker"; + case 4: + return "Sequence Map Pointer"; + case 2: + return "IBNK Pointer Table"; + case 3: + return "WaveSystem Pointer Table"; + default: + return "Unknown Chunk (Alignment kept)"; + } + } + + public void LoadAAFile(string filename) + { + WSYS = new WaveSystem[0xFF]; // None over 256 please :). + IBNK = new InstrumentBank[0xFF]; // These either. + var aafdata = File.ReadAllBytes(filename); // We're just going to load a whole copy into memory because we're lame -- having this buffer in memory makes it easy to pass as a ref to stream readers later. + var aafRead = new BeBinaryReader(new MemoryStream(aafdata)); + + bool done = false; + + while (!done) + { + var ChunkID = aafRead.ReadUInt32(); + long anchor; + var name = convertChunkName(ChunkID); + Console.WriteLine("[AAF] Found chunk: {0}", name); + + switch (ChunkID) + { + case 0: + done = true; // either we're misalligned or done, so just stopo here. + break; + case 1: // Don't know + case 5: + case 4: + case 6: + case 7: + aafRead.ReadUInt32(); + aafRead.ReadUInt32(); + aafRead.ReadUInt32(); + break; + case 2: // INST + case 3: // WSYS + { + while (true) + { + + var offset = aafRead.ReadUInt32(); + if (offset == 0) + { + break; // 0 means we reached the end. + } + var size = aafRead.ReadUInt32(); + var type = aafRead.ReadUInt32(); + + + + anchor = aafRead.BaseStream.Position; // Store our return position. + + aafRead.BaseStream.Position = offset; // Seek to the offset pos. + if (ChunkID==3) + { + + var b = new WaveSystem(); // Load the wavesystem + b.LoadWSYS(aafRead, Path.GetDirectoryName(filename)!, false); + WSYS[b.id] = b; // store it + + Console.WriteLine("\t WSYS at 0x{0:X}", offset); + } else if (ChunkID==2) + { + + var x = new InstrumentBank(); + x.LoadInstrumentBank(aafRead); + Console.WriteLine("\t IBNK at 0x{0:X}", offset); + IBNK[x.id] = x; // Store it + } + aafRead.BaseStream.Position = anchor; // Return back to our original pos after loading. + + } + break; + } + + + + + } + } + + + } + } +} diff --git a/JAIMaker/JAI/BAAFile.cs b/JAIMaker/JAI/BAAFile.cs new file mode 100644 index 0000000..3440a14 --- /dev/null +++ b/JAIMaker/JAI/BAAFile.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JaiSeqX.JAI.Seq; +using JaiSeqX.JAI.Types; +using JaiSeqX.JAI.Types.WSYS; +using Be.IO; +using System.IO; + +namespace JaiSeqX.JAI +{ + + public class BAAFile : AABase + { + + private string convertChunkName(uint id) + { + switch (id) + { + case 1094803260: + return "Start Marker"; + case 1651733536: + return "BST Section"; + case 1651733614: + return "BSTN Section"; + case 1651729184: + return "BSC Section"; + case 2004033568: + return "WSYS pointer"; + case 1651403552: + return "IBNK Pointer"; + case 1046430017: + return "End Marker"; + default: + return "Unknown Chunk (Alignment kept)"; + } + } + + public void LoadBAAFile(string filename) + { + WSYS = new WaveSystem[0xFF]; // None over 256 please :). + IBNK = new InstrumentBank[0xFF]; // These either. + var aafdata = File.ReadAllBytes(filename); // We're just going to load a whole copy into memory because we're lame -- having this buffer in memory makes it easy to pass as a ref to stream readers later. + var aafRead = new BeBinaryReader(new MemoryStream(aafdata)); + + bool done = false; + + while (!done) + { + var ChunkID = aafRead.ReadUInt32(); + long anchor; + var name = convertChunkName(ChunkID); + Console.WriteLine("[BAA] Found chunk: {0}", name); + + switch (ChunkID) + { + case 1046430017: // >_AA + done = true; // either we're misalligned or done, so just stopo here. + break; + case 1094803260: // AA_< + break; + case 1651733536: // BST + { + var offset_sta = aafRead.ReadUInt32(); + var offset_end = aafRead.ReadUInt32(); + break; + } + case 1651733614: // BSTN + { + var offset_sta = aafRead.ReadUInt32(); + var offset_end = aafRead.ReadUInt32(); + break; + } + case 1651729184: // BSC + { + var offset_sta = aafRead.ReadUInt32(); + var offset_end = aafRead.ReadUInt32(); + break; + } + + case 1651403552: // BNK + { + var id = aafRead.ReadUInt32(); + var offset = aafRead.ReadUInt32(); + anchor = aafRead.BaseStream.Position; // Store our return position. + aafRead.BaseStream.Position = offset; // Seek to the offset pos. + var b = new InstrumentBank(); + b.LoadInstrumentBank(aafRead); // Load it up + IBNK[b.id] = b; + aafRead.BaseStream.Position = anchor; // Return back to our original pos after loading. + + FixWsysId(b, id); + + break; + } + case 2004033568: // WSYS + { + var id = aafRead.ReadUInt32(); + var offset = aafRead.ReadUInt32(); + var flags = aafRead.ReadUInt32(); + + anchor = aafRead.BaseStream.Position; // Store our return position. + aafRead.BaseStream.Position = offset; // Seek to the offset pos. + var b = new WaveSystem(); + b.LoadWSYS(aafRead, Path.GetDirectoryName(filename)!, true); + WSYS[id] = b; + aafRead.BaseStream.Position = anchor; // Return back to our original pos after loading. + break; + } + + } + } + } + + private static void FixWsysId(InstrumentBank bank, uint id) + { + foreach (var instrument in bank.Instruments) + { + if (instrument == null) + continue; + + foreach (var key in instrument.Keys) + { + if (key == null) + continue; + + foreach (var velocity in key.keys) + { + velocity?.wsysid = id; + } + } + } + } + } + } + + diff --git a/JAIMaker/JAI/Helpers.cs b/JAIMaker/JAI/Helpers.cs new file mode 100644 index 0000000..b6a8664 --- /dev/null +++ b/JAIMaker/JAI/Helpers.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Be.IO; +using System.IO; + +namespace JaiSeqX.JAI +{ + public static class Helpers + { + public static uint ReadUInt24BE(BinaryReader reader) + { + try + { + var b1 = reader.ReadByte(); + var b2 = reader.ReadByte(); + var b3 = reader.ReadByte(); + return + (((uint)b1) << 16) | // fuuck. + (((uint)b2) << 8) | // FfFFFuuuuuuck. + ((uint)b3); // FFFFUUUUUUUUUUUCK. + } + catch + { + return 0u; + } + } + + public static int ReadVLQ(BinaryReader reader) + { + int fade = (int)reader.ReadByte(); + while ((fade & 0x80) > 0) + { + fade = ((fade & 0x7F) << 7); + fade += reader.ReadByte(); + + + } + return fade; + } + + public static string readArchiveName(BinaryReader aafRead) + { + var ofs = aafRead.BaseStream.Position; + byte nextbyte; + byte[] name = new byte[0x70]; + + int count = 0; + while ((nextbyte = aafRead.ReadByte()) != 0xFF & nextbyte != 0x00) + { + name[count] = nextbyte; + count++; + } + aafRead.BaseStream.Seek(ofs + 0x70, SeekOrigin.Begin); + return Encoding.ASCII.GetString(name, 0, count); + } + + + + + + + + + + /* I'll start by being honest. + * I have absolutely no clue how ADPCM works, nor do i have any interest in this..... garbage. + * This is basically a miniaturized version of WWDumpSound, used code from Arookas and Jasper (magcius) + * I just modified it to work with BMSXPX / JaiSeqX + */ + + static ushort[] afccoef = new ushort[16] + { + 0, + 0x0800, + 0, + 0x0400, + 0x1000, + 0x0e00, + 0x0c00, + 0x1200, + 0x1068, + 0x12c0, + 0x1400, + 0x0800, + 0x0400, + 0xfc00, + 0xfc00, + 0xf800, + //? Array error + }; + + static ushort[] afccoef2 = new ushort[16] + { + 0, + 0, + 0x0800, + 0x0400, + 0xf800, + 0xfa00, + 0xfc00, + 0xf600, + 0xf738, + 0xf704, + 0xf400, + 0xf800, + 0xfc00, + 0x0400, + 0, + 0, + }; + + + + public static string AFCtoPCM16(byte[] adpcm, double srate ,int vsize, ushort format,string pth_out) + { + + var fobj_reader = new BeBinaryReader(new MemoryStream(adpcm)); + var fobj_writer_stream = File.Open(pth_out, FileMode.OpenOrCreate, FileAccess.ReadWrite); + var fobj_writer = new BinaryWriter(fobj_writer_stream); + short[] data_out; + var total = 0; + + /* Below is SHAMELESSLY ripped from WWDumpSND */ + unchecked + { + + /******* DECODE AFC TO PCM *********/ + + int hi0 = 0; + int hi1 = 0; + + int framesz = 9; + int osz = (int)vsize / framesz * 16 * 2; + int oszt = osz + 8; + int size_rem; + short[] wavout; + byte[] wavin; + + /////****** WAV BUFFER ******///// + + byte[] wavhead = new byte[44] { + 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, 0x20, + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00 + }; + + for (int i = 0; i < wavhead.Length; i++) + { + fobj_writer.Write(wavhead[i]); + + } + + fobj_writer.BaseStream.Position = 4; + fobj_writer.Write(oszt); + fobj_writer.BaseStream.Position = 24; + fobj_writer.Write((int)srate); + fobj_writer.Write((int)srate); + fobj_writer.BaseStream.Position = 40; + fobj_writer.Write((int)osz); + fobj_reader.BaseStream.Position = 0; + + byte cbyte = 0; + sbyte[] nibbles; + + for (size_rem = (int)vsize; size_rem >= framesz; size_rem -= framesz) + { + wavin = fobj_reader.ReadBytes(framesz); + wavout = new short[16]; + var wavreader = new BeBinaryReader(new MemoryStream(wavin)); + /********* AFC Decoder buffer **********/ + cbyte = wavreader.ReadByte(); // READ BYTE 0, ADVANCE TO 1 + int scale = (1 << (cbyte >> 4)); + //Console.WriteLine("Delta {0} - {1} FSZ {2}", scale, (short)((cbyte) >> 4),framesz); + short index = (short)(cbyte & 0xF); + //Console.WriteLine("Index {0}", index); + nibbles = new sbyte[16]; + if (format == 0) + { + for (int i = 0; i < 16; i += 2) + { + cbyte = wavreader.ReadByte(); // src ++ + var bse = (sbyte)(cbyte); + nibbles[i + 0] = (sbyte)(bse >> 4); + nibbles[i + 1] = (sbyte)(cbyte & 15); + } + + for (int i = 0; i < 16; i++) + { + if (nibbles[i] >= 8) + { + nibbles[i] = (sbyte)(nibbles[i] - 16); + } + } + } + else + { + /* + for (int i = 0; i < 16; i += 4) + { + nibbles[i + 0] = (short)((cbyte >> 6) & 0x03); + nibbles[i + 1] = (short)((cbyte >> 4) & 0x03); + nibbles[i + 2] = (short)((cbyte >> 2) & 0x03); + nibbles[i + 3] = (short)((cbyte >> 0) & 0x03); + + cbyte = wavreader.ReadByte(); // src ++ + } + + for (int i = 0; i < 16; i++) + { + if (nibbles[i] >= 2) + { + nibbles[i] = (short)(nibbles[i] - 4); + nibbles[i] = (short)(nibbles[i] << 13); + } + } + } + + + */ + + } + + + for (int i = 0; i < 16; i++) + { + //Console.WriteLine(nibbles[i]); + + var superscale = ((scale * nibbles[i]) << 11); + var coef1_addi = (int)hi0 * (short)afccoef[index]; + var coef2_addi = (int)hi1 * (short)afccoef2[index]; + var final0 = superscale + coef1_addi + coef2_addi; + var final1 = final0 >> 11; + int sample = final1; + //Console.ReadLine(); + // CLAMP 16 BIT PCM + //Console.WriteLine("XATA scc {0} c1a {1} c2a {2} f0 {3} f1 {4}", superscale,coef1_addi,coef2_addi,final0,final1); + //Console.WriteLine("Data scl {0} nibi {1} hi0 {2} hi1 {3} c1 {4:X6} c2 {5:X6}", scale, nibbles[i], hi0,hi1,afccoef[index],afccoef2[index]); + //Console.WriteLine("Sample {0}", final1); + //Console.ReadLine(); + + if (sample > 32767) + { + sample = 32767; + } + if (sample < -32768) + { + sample = -32768; + } + wavout[i] = (short)(sample); + hi1 = hi0; + hi0 = sample; + } + for (int i = 0; i < 16; i++) + { + fobj_writer.Write(wavout[i]); + } + } + + // Console.WriteLine("osz {0} {1}", osz, fobj_writer.BaseStream.Length); + fobj_writer.Flush(); + fobj_writer.Close(); + } + + return pth_out; + } + + + + public static void printJaiSeqStack(JAI.Seq.Subroutine Seq) + { + var opstack = Seq.OpcodeHistory; + var postack = Seq.OpcodeAddressStack; + opstack = new Queue(opstack.Reverse()); + postack = new Queue(postack.Reverse()); + int finalCall = 0; + int finalCallAddr = 0; + + try + { + finalCall = opstack.Dequeue(); + finalCallAddr = postack.Dequeue(); + + } catch + { + Console.WriteLine("JaiSeqXHelpers: Couldn't print JaiSeq stack. There's probably nothing in the stack."); + return; + } + + var depth = 0; + Console.WriteLine("===== printJaiSeqStack"); + Console.WriteLine("(depth) addr: opcode"); + var b = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("({2:X}) 0x{1:X}: 0x{0:X}", finalCall, finalCallAddr, depth); + Console.ForegroundColor = b; + while (true) + { + depth++; + if (opstack.Count == 0) { break; } + finalCall = opstack.Dequeue(); + finalCallAddr = postack.Dequeue(); + Console.WriteLine("\t({2:X}) 0x{1:X}: 0x{0:X}", finalCall, finalCallAddr,depth); + } + } + + } +} diff --git a/JAIMaker/JAI/Seq/JSequenceState.cs b/JAIMaker/JAI/Seq/JSequenceState.cs new file mode 100644 index 0000000..1cadce5 --- /dev/null +++ b/JAIMaker/JAI/Seq/JSequenceState.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JaiSeqX.JAI.Seq +{ + + // Track state object, tells all of the parameters of the track at any given point. + public class JSequenceState + { + + public byte note; + public byte voice; + public byte vel; + + public int delay; + + public byte param; + public short param_value; + + public int perf; + public int perf_value; + public int perf_duration; + public byte perf_type; + public double perf_decimal; + + + public byte voice_bank; + public byte voice_program; + + public short ppqn; + public short bpm; + + public int jump_address; + public byte jump_mode; + + public int track_id; + public int track_address; + public int track_stack_depth; + + + public int current_address; + + public int[] registers; + + public string message; + + public JSequenceState() + { + registers = new int[80]; + } + + + } +} diff --git a/JAIMaker/JAI/Seq/JaiEventType.cs b/JAIMaker/JAI/Seq/JaiEventType.cs new file mode 100644 index 0000000..65374b6 --- /dev/null +++ b/JAIMaker/JAI/Seq/JaiEventType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JaiSeqX.JAI.Seq +{ + public enum JaiEventType + { + + NOTE_ON = 0x00, + NOTE_OFF = 0x01, + DELAY = 0x02, + TIME_BASE = 0x03, + NEW_TRACK = 0x04, + BANK_CHANGE = 0x05, + PROG_CHANGE = 0x06, + JUMP = 0x07, + PARAM = 0x08, + PERF = 0x09, + CALL = 0x10, + RET = 0x11, + + DEBUG = 0xFB, + HALT = 0xFC, + PAUSE = 0xFD, + + UNKNOWN_ALIGN_FAIL = 0xFE, + UNKNOWN = 0xFF, + } +} diff --git a/JAIMaker/JAI/Seq/JaiSeqOpcodeV2.cs b/JAIMaker/JAI/Seq/JaiSeqOpcodeV2.cs new file mode 100644 index 0000000..8fe1201 --- /dev/null +++ b/JAIMaker/JAI/Seq/JaiSeqOpcodeV2.cs @@ -0,0 +1,27 @@ +namespace JaiSeqX.JAI.Seq; + +public enum JaiSeqOpcodeV2 : byte +{ + OpenTrack = 0xC1, + Jmp = 0xC7, + JmpF = 0xC8, + RegLoad = 0xD8, + Reg = 0xD9, + Tempo = 0xE0, + Bank = 0xE2, + Prg = 0xE3, + Wait = 0xF0, + WaitByte = 0xF1, + Finish = 0xFF, +} + +public static class JaiSeqOpcodeV2Extensions +{ + extension(BinaryWriter writer) + { + public void Write(JaiSeqOpcodeV2 opcode) + { + writer.Write((byte)opcode); + } + } +} \ No newline at end of file diff --git a/JAIMaker/JAI/Seq/Subroutine.cs b/JAIMaker/JAI/Seq/Subroutine.cs new file mode 100644 index 0000000..6516222 --- /dev/null +++ b/JAIMaker/JAI/Seq/Subroutine.cs @@ -0,0 +1,512 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Be.IO; +using System.IO; + +namespace JaiSeqX.JAI.Seq +{ + + public enum JaiSeqEvent + { + + /* wait with u8 arg */ + WAIT_8 = 0x80, + /* wait with u16 arg */ + WAIT_16 = 0x88, + /* wait with variable-length arg */ + WAIT_VAR = 0xF0, + + /* perf / lerp */ + PERF_U8_NODUR = 0x94, + PERF_U8_DUR_U8 = 0x96, + PERF_U8_DUR_U16 = 0x97, + PERF_S8_NODUR = 0x98, + PERF_S8_DUR_U8 = 0x9A, + PERF_S8_DUR_U16 = 0x9B, + PERF_S16_NODUR = 0x9C, + PERF_S16_DUR_U8 = 0x9E, + PERF_S16_DUR_U16 = 0x9F, + + PARAM_SET = 0xA0, + ADDR = 0xA1, + MULR = 0xA2, + CMPR = 0xA3, + PARAM_SET_8 = 0xA4, + ADD8 = 0xA5, + MUL8 = 0xA6, + CMP8 = 0xA7, + LOADTBL = 0xAA, + SUB = 0xAB, + PARAM_SET_16 = 0xAC, + ADD16 = 0xAD, + MUL16 = 0xAE, + CMP16 = 0xAF, + LOAD_TABLE = 0xAA, + SUBTRACT = 0xAB, + BITWISE = 0xA9, + + + + OPEN_TRACK = 0xC1, + OPEN_TRACK_BROS = 0xC2, + CALL = 0xC3, + CALL_COND = 0xC4, + RET = 0xC5, + RET_COND = 0xC6, + JUMP = 0xC7, + JUMP_COND = 0xC8, + LOOP_COUNT = 0xC9, + PORTREAD = 0xCB, + PORTWRITE = 0xCC, + SPECIALWAIT = 0xCF, + + + NAMEBUS = 0xD0, + ADSR = 0xD8, + BUSCONNECT = 0xDD, + INTERRUPT = 0xDF, + INTERRUPT_TIMER = 0xE4, + SYNC_CPU = 0xE7, + PANSWSET = 0xEF, + OSCILLATORFULL = 0xF2, + PRINTF = 0xFB, + TIME_BASE = 0xFD, + TEMPO = 0xFE, + FIN = 0xFF, + + + /* "Improved" JaiSeq from TP / SMG / SMG2 seems to use this instead */ + J2_SET_PERF_8 = 0xB8, + J2_SET_PERF_16 = 0xB9, + /* Set "articulation"? Used for setting timebase. */ + J2_SET_ARTIC = 0xD8, + J2_TEMPO = 0xE0, + J2_SET_BANK = 0xE2, + J2_SET_PROG = 0xE3, + } + + + public class Subroutine + { + BeBinaryReader Sequence; + byte[] SeqData; + + public Stack AddrStack; + + public Queue OpcodeHistory; + public Queue OpcodeAddressStack; + + + private int baseAddress; + + public JSequenceState State; + + public byte last_opcode; + + public Subroutine(ref byte[] BMSData,int BaseAddr) + { + State = new JSequenceState(); + SeqData = BMSData; + + AddrStack = new Stack(64); // 64 unit stack? + OpcodeHistory = new Queue(16); + OpcodeAddressStack = new Queue(16); + + Sequence = new BeBinaryReader(new MemoryStream(BMSData)); + Sequence.BaseStream.Position = BaseAddr; + + State = new JSequenceState(); + + baseAddress = BaseAddr; + + + } + + + private void skip(int bytes) + { + Sequence.BaseStream.Seek(bytes, SeekOrigin.Current); + } + + private void reset() + { + Sequence.BaseStream.Position = baseAddress; + } + + public void jump(int pos) + { + Sequence.BaseStream.Position = pos; + } + + public int nextOpAddress() + { + return (int)Sequence.BaseStream.Position; + } + public JaiEventType loadNextOp() + { + if (OpcodeAddressStack.Count == 16) + { + OpcodeAddressStack.Dequeue(); + } + if (OpcodeHistory.Count == 16) + { + OpcodeHistory.Dequeue(); + } + + OpcodeAddressStack.Enqueue((int)Sequence.BaseStream.Position); // push address to FIFO stack. + + State.current_address = (int)Sequence.BaseStream.Position; + byte current_opcode = Sequence.ReadByte(); // Reads the current byte in front of the cursor. + last_opcode = current_opcode; + OpcodeHistory.Enqueue(current_opcode); // push opcode to FIFO stack + + + + if (current_opcode < 0x80) + { + State.note = current_opcode; // The note on event is laid out like a piano with 127 (0x7F1) keys. + // So this means that the first 0x80 bytes are just pressing the individual keys. + State.voice = Sequence.ReadByte(); // The next byte tells the voice, 0-8 + State.vel = Sequence.ReadByte(); // And finally, the next byte will tell the velocity + return JaiEventType.NOTE_ON; // Return the note on event. + + } else if (current_opcode==(byte)JaiSeqEvent.WAIT_8) // Contrast to above, the opcode between these two is WAIT_U8 + { + State.delay += Sequence.ReadByte(); // Add u8 ticks to the delay. + + return JaiEventType.DELAY; + } else if (current_opcode < 0x88) // We already check if it's 0x80, so anything between here will be 0x81 and 0x87 + { + // Only the first 7 bits are going to determine which voice we're stopping. + State.voice = (byte)(current_opcode & 0x7F); + return JaiEventType.NOTE_OFF; + } else // Finally, we can fall into our CASE statement. + { + switch (current_opcode) + { + /* Delays and waits */ + case (byte)JaiSeqEvent.WAIT_16: // Wait (UInt16) + State.delay = Sequence.ReadUInt16(); // Add to the state delay + + return JaiEventType.DELAY; + + case (byte)JaiSeqEvent.WAIT_VAR: // Wait (VLQ) see readVlq function. + State.delay += Helpers.ReadVLQ(Sequence); + return JaiEventType.DELAY; + + /* Logical jumps */ + + case (byte)JaiSeqEvent.JUMP: // Unconditional jump + State.jump_mode = 0; // Set jump mode to 0 + State.jump_address = Sequence.ReadInt32(); // Absolute address. + return JaiEventType.JUMP; + + case (byte)JaiSeqEvent.JUMP_COND: // Jump based on mode + State.jump_mode = Sequence.ReadByte(); + State.jump_address = (int)Helpers.ReadUInt24BE(Sequence); // pointer + return JaiEventType.JUMP; + + case (byte)JaiSeqEvent.RET_COND: + State.jump_mode = Sequence.ReadByte(); + + return JaiEventType.RET; + case (byte)JaiSeqEvent.CALL_COND: + State.jump_mode = Sequence.ReadByte(); + State.jump_address = (int)Helpers.ReadUInt24BE(Sequence); + return JaiEventType.CALL; + case (byte)JaiSeqEvent.RET: + return JaiEventType.RET; + + + + /* Tempo Control */ + + case (byte)JaiSeqEvent.J2_SET_ARTIC: // The very same. + { + var type = Sequence.ReadByte(); + var val = Sequence.ReadInt16(); + if (type == 0x62) + { + State.ppqn = val; + } + return JaiEventType.TIME_BASE; + } + case (byte)JaiSeqEvent.TIME_BASE: // Set ticks per quarter note. + State.ppqn = (short)(Sequence.ReadInt16() ); + //State.bpm = 100; + Console.WriteLine("Timebase ppqn set {0}", State.ppqn); + return JaiEventType.TIME_BASE; + + case (byte)JaiSeqEvent.J2_TEMPO: // Set BPM, Same format + case (byte)JaiSeqEvent.TEMPO: // Set BPM + State.bpm = (short)(Sequence.ReadInt16() ); + return JaiEventType.TIME_BASE; + + /* Track Control */ + + case (byte)JaiSeqEvent.OPEN_TRACK: + State.track_id = Sequence.ReadByte(); + State.track_address = (int)Helpers.ReadUInt24BE(Sequence); // Pointer to track inside of BMS file (Absolute) + return JaiEventType.NEW_TRACK; + case (byte)JaiSeqEvent.FIN: + return JaiEventType.HALT; + + case (byte)JaiSeqEvent.J2_SET_BANK: + State.voice_bank = Sequence.ReadByte(); + return JaiEventType.BANK_CHANGE; + + case (byte)JaiSeqEvent.J2_SET_PROG: + State.voice_program = Sequence.ReadByte(); + return JaiEventType.PROG_CHANGE; + + /* Parameter control */ + + + case (byte)JaiSeqEvent.J2_SET_PERF_8: + State.param = Sequence.ReadByte(); + State.param_value = Sequence.ReadByte(); + return JaiEventType.PARAM; + + case (byte)JaiSeqEvent.J2_SET_PERF_16: + State.param = Sequence.ReadByte(); + State.param_value = Sequence.ReadInt16(); + return JaiEventType.PARAM; + + case (byte)JaiSeqEvent.PARAM_SET_8: // Set track parameters (Usually used for instruments) + State.param = Sequence.ReadByte(); + State.param_value = Sequence.ReadByte(); + if (State.param==0x20) // 0x20 is bank change + { + State.voice_bank = (byte)State.param_value; + return JaiEventType.BANK_CHANGE; + } + if (State.param == 0x21) // 0x21 is program change + { + State.voice_program = (byte)State.param_value; + return JaiEventType.PROG_CHANGE; + } + return JaiEventType.PARAM; + + case (byte)JaiSeqEvent.PARAM_SET_16: // Set track parameters (Usually used for instruments) + State.param = Sequence.ReadByte(); + State.param_value = Sequence.ReadInt16(); + if (State.param == 0x20) // 0x20 is bank change + { + State.voice_bank = (byte)State.param_value; + return JaiEventType.BANK_CHANGE; + } + if (State.param == 0x21) // 0x21 is program change + { + State.voice_program = (byte)State.param_value; + return JaiEventType.PROG_CHANGE; + } + return JaiEventType.PARAM; + case (byte)JaiSeqEvent.PRINTF: + var lastread = -1; + string v = ""; + while (lastread!=0) + { + lastread = Sequence.ReadByte(); + v += (char)lastread; + } + // Sequence.ReadByte(); + Console.WriteLine(v); + + return JaiEventType.UNKNOWN; + + /* PERF Control*/ + /* Perf structure is as follows + * type + * val + * ( dur) + */ + + case (byte)JaiSeqEvent.PERF_U8_NODUR: + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadByte(); + + State.perf_duration = 0; + State.perf_type = 1; + State.perf_decimal = ((double)State.perf_value / 0xFF); + return JaiEventType.PERF; + + case (byte)JaiSeqEvent.PERF_U8_DUR_U8: + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadByte(); + State.perf_duration = Sequence.ReadByte(); + State.perf_type = 1; + State.perf_decimal = ((double)State.perf_value / 0xFF); + return JaiEventType.PERF; + + case (byte)JaiSeqEvent.PERF_U8_DUR_U16: + + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadByte(); + State.perf_duration = Sequence.ReadUInt16(); + State.perf_type = 1; + State.perf_decimal = ((double)State.perf_value / 0xFF); + return JaiEventType.PERF; + + case (byte)JaiSeqEvent.PERF_S8_NODUR: + { + State.perf = Sequence.ReadByte(); + var b = Sequence.ReadByte(); // Lazy byte signage, apparently C#'s SByte is broken. + State.perf_value = (b > 0x7F) ? b - 0xFF : b; + State.perf_duration = 0; + State.perf_type = 2; + State.perf_decimal = ((double)(State.perf_value) / 0x7F); + return JaiEventType.PERF; + } + case (byte)JaiSeqEvent.PERF_S8_DUR_U8: + { + State.perf = Sequence.ReadByte(); + var b = Sequence.ReadByte(); // Lazy byte signage, apparently C#'s SByte is broken. + State.perf_value = (b > 0x7F) ? b - 0xFF : b; + State.perf_duration = Sequence.ReadByte(); + State.perf_type = 2; + State.perf_decimal = ((double)(State.perf_value) / 0x7F); + return JaiEventType.PERF; + } + + case (byte)JaiSeqEvent.PERF_S8_DUR_U16: + { + State.perf = Sequence.ReadByte(); + var b = Sequence.ReadByte(); // Lazy byte signage, apparently C#'s SByte is broken. + State.perf_value = State.perf_value = (b > 0x7F) ? b - 0xFF : b; + State.perf_duration = Sequence.ReadUInt16(); + State.perf_type = 2; + State.perf_decimal = ((double)(State.perf_value) / 0x7F); + return JaiEventType.PERF; + } + + case (byte)JaiSeqEvent.PERF_S16_NODUR: + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadInt16(); + State.perf_duration = 0; + State.perf_type = 3; + State.perf_decimal = ((double)(State.perf_value) / 0x7FFF); + return JaiEventType.PERF; + + case (byte)JaiSeqEvent.PERF_S16_DUR_U8: + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadInt16(); + State.perf_duration = Sequence.ReadByte(); + State.perf_type = 3; + State.perf_decimal = ((double)(State.perf_value) / 0x7FFF); + return JaiEventType.PERF; + + case (byte)JaiSeqEvent.PERF_S16_DUR_U16: + State.perf = Sequence.ReadByte(); + State.perf_value = Sequence.ReadInt16(); + State.perf_duration = Sequence.ReadUInt16(); + State.perf_type = 3; + State.perf_decimal = ((double)(State.perf_value) / 0x7FFF); + return JaiEventType.PERF; + + + /* J2 Opcodes */ + + + /* Unsure as of yet, but we have to keep alignment */ + case 0xE7: + skip(2); + // Console.WriteLine(Sequence.ReadByte()); + //Console.WriteLine(Sequence.ReadByte()); + + return JaiEventType.DEBUG; + case 0xDD: + case 0xED: + skip(3); + return JaiEventType.UNKNOWN; + case 0xEF: + case 0xF9: + case 0xE6: + + skip(2); + return JaiEventType.UNKNOWN; + case 0xA0: + case (byte)JaiSeqEvent.ADDR: // + skip(2); + return JaiEventType.UNKNOWN; + case 0xA3: + skip(2); + return JaiEventType.UNKNOWN; + case 0xA5: + skip(2); + return JaiEventType.UNKNOWN; + case 0xA7: + skip(2); + return JaiEventType.UNKNOWN; + case 0xA9: + skip(4); + return JaiEventType.UNKNOWN; + case 0xAA: + skip(4); + return JaiEventType.UNKNOWN; + case 0xAD: + // State.delay += 0xFFFF; + // Add (byte) register. + (short) value + // + skip(3); + return JaiEventType.UNKNOWN; + case 0xAE: + return JaiEventType.UNKNOWN; + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + int flag = Sequence.ReadByte(); + if (flag == 0x40) { skip(2); } + if (flag == 0x80) { skip(4); } + return JaiEventType.UNKNOWN; + case 0xDB: + + case 0xDF: + + skip(4); + return JaiEventType.UNKNOWN; + case 0xCB: + case 0xBE: + skip(2); + return JaiEventType.UNKNOWN; + case 0xCC: + skip(2); + return JaiEventType.UNKNOWN; + case 0xCF: + skip(1); + return JaiEventType.UNKNOWN; + case 0xD0: + case 0xD1: + case 0xD2: + case 0xD5: + case 0xD9: + + case 0xDE: + case 0xDA: + + skip(1); + return JaiEventType.UNKNOWN; + case 0xF1: + case 0xF4: + + case 0xD6: + skip(1); + //Console.WriteLine(Sequence.ReadByte()); + return JaiEventType.DEBUG; + case 0xBC: + return JaiEventType.UNKNOWN; + } + } + return JaiEventType.UNKNOWN_ALIGN_FAIL; + } + + + + } +} diff --git a/JAIMaker/JAI/Types/Instrument.cs b/JAIMaker/JAI/Types/Instrument.cs new file mode 100644 index 0000000..a368d6d --- /dev/null +++ b/JAIMaker/JAI/Types/Instrument.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Be.IO; +namespace JaiSeqX.JAI.Types +{ + public class InstrumentKey + { + public float Volume = 1; + public float Pitch = 1; + public InstrumentKeyVelocity?[] keys; + + } + + + public class InstrumentKeyVelocity + { + public float Volume; + public float Pitch; + public uint wave; + public uint wsysid; + public uint velocity; + + } + + + public class Instrument + { + public int id; + public float Volume; + public float Pitch; + public short attack = 0; + public short decay = 0; + public short sustain = 0; + public short release = 0; + public int oscillator = 0; + public bool IsPercussion; + + public InstrumentKey?[] Keys; + + } +} diff --git a/JAIMaker/JAI/Types/InstrumentBank.cs b/JAIMaker/JAI/Types/InstrumentBank.cs new file mode 100644 index 0000000..ee4879f --- /dev/null +++ b/JAIMaker/JAI/Types/InstrumentBank.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Be.IO; +using System.IO; + +using ibnktool; + +namespace JaiSeqX.JAI.Types +{ + public class InstrumentBank + { + public int id; + + public Instrument?[] Instruments; + + private const uint INST = 0x494E5354; + private const uint PERC = 0x50455243; + private const uint PER2 = 0x50455232; + private const uint Inst = 0x496E7374; + + public void LoadInstrumentBank(BeBinaryReader instReader) + { + Instruments = new Instrument?[0xF0]; // for some reason, they will only ever have 0xF0 instruments in them + + var start = instReader.BaseStream.Position; + instReader.BaseStream.Seek(0xC, SeekOrigin.Current); + var version = instReader.ReadUInt32(); + instReader.BaseStream.Position = start; + + if (version == 0) + { + loadIBNKJaiV1(instReader); + } + else if (version == 1) + { + loadIBNKJaiV2(instReader); + } + } + + + private long ReadJARCSizePointer(BeBinaryReader br) + { + var sectSize = br.ReadUInt32(); + return sectSize + 8; // This is basically taking the section size pointer and adding 8 to it, because the size is always 8 bytes deep. + // Adding 8 to this makes it a pointer to the next section relative to the section base :v + } + + + private void loadIBNKJaiV2(BeBinaryReader instReader) + { + var ibnk = InstrumentBankv2.CreateFromStream(instReader); + + id = ibnk.id; + + for (var i = 0; i < ibnk.List.Length; i++) + { + var newInstr = new Instrument(); + newInstr.Keys = new InstrumentKey[0xF0]; + + switch (ibnk.List[i]) + { + case JStandardInstrumentv2 standard: + newInstr.Volume = standard.Volume; + newInstr.Pitch = standard.Pitch; + + newInstr.IsPercussion = false; + + int KeyHigh = 0; + int KeyLow = 0; + + foreach (var key in standard.Keys) + { + var NewKey = new InstrumentKey(); + NewKey.keys = new InstrumentKeyVelocity[0x81]; + KeyHigh = key.BaseKey; + + int VelLow = 0; + int VelHigh = 0; + foreach (var velocity in key.Velocities) + { + var NewVelR = new InstrumentKeyVelocity(); + VelHigh = velocity.Velocity; + NewVelR.Pitch = velocity.Pitch; + NewVelR.velocity = velocity.Velocity; + NewVelR.Volume = velocity.Volume; + NewVelR.wave = (uint)velocity.WAVEID; + + for (int idx = 0; idx < ( (1+ VelHigh) - VelLow); idx++) // See below for what this is doing + { + NewKey.keys[(VelLow + idx)] = NewVelR; + NewKey.keys[127] = NewVelR; + } + VelLow = VelHigh; + } + + for (int idx = 0; idx < (KeyHigh - KeyLow); idx++) // The keys are gappy. + { + newInstr.Keys[(KeyLow + idx)] = NewKey; // So we want to interpolate the previous keys across the empty ones, so that way it's a region + newInstr.Keys[127] = NewKey; + } + KeyLow = KeyHigh; + } + + break; + case JPercussionInstrumentv2 percussion: + newInstr.Volume = percussion.Volume; + newInstr.Pitch = percussion.Pitch; + + newInstr.IsPercussion = true; + + //percussion.Keys + + break; + case null: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + Instruments[i] = newInstr; + } + } + + private void loadIBNKJaiV1(BeBinaryReader instReader) + { + long anchor = 0; + var BaseAddress = instReader.BaseStream.Position; + var current_header = 0u; + current_header = instReader.ReadUInt32(); // read the first 4 byteas + if (current_header != 0x49424e4b) // Check to see if it equals IBNK + { + throw new InvalidDataException("Scanned header is not an IBNK."); + } + var size = instReader.ReadUInt32(); + id = instReader.ReadInt32(); // Global virtual ID + instReader.BaseStream.Seek(0x14, SeekOrigin.Current); // 0x14 bytes always blank + current_header = instReader.ReadUInt32(); // We should be reading "BANK" + for (int inst_id = 0; inst_id < 0xF0; inst_id++) + { + var inst_offset = instReader.ReadInt32(); // Read the relative pointer to the instrument + anchor = instReader.BaseStream.Position; // store the position to jump back into. + if (inst_offset > 0) // If we have a 0 offset, then the instrument is unassigned. + { + instReader.BaseStream.Position = BaseAddress + inst_offset; // Seek to the offset of the instrument. + current_header = instReader.ReadUInt32(); // Read the 4 byte identity of the instrument. + var NewINST = new Instrument(); + NewINST.Keys = new InstrumentKey[0xF0]; + switch (current_header) + { + case INST: + { + instReader.ReadUInt32(); // The first 4 bytes following an instrument is always 0, for some reason. Maybe reserved. + NewINST.Pitch = instReader.ReadSingle(); // 4 byte float pitch + NewINST.Volume = instReader.ReadSingle(); // 4 byte float volume + /* Lots of skipping, i havent added these yet, but i'll comment what they are. */ + var poscioffs = instReader.ReadUInt32(); // offset to first oscillator table + var poscicnt = instReader.ReadUInt32(); // Offset to second oscillator count + // Console.WriteLine("Oscillator at 0x{0:X}, length {1}", poscioffs, poscicnt); + //Console.ReadLine(); + instReader.ReadUInt32(); // Offset to first effect object + instReader.ReadUInt32(); // offset to second effect object + instReader.ReadUInt32(); // offset of first sensor object + instReader.ReadUInt32(); // offset of second sensor object + /*////////////////////////////////////////////////////////////////////////////*/ + var keyCounts = instReader.ReadInt32(); // How many key regions are in here. + int KeyHigh = 0; + int KeyLow = 0; + for (int k = 0; k < keyCounts; k++) + { + var NewKey = new InstrumentKey(); + NewKey.keys = new InstrumentKeyVelocity[0x81]; + var keyreg_offset = instReader.ReadInt32(); // This will be where the data for our key region is. + var keyptr_return = instReader.BaseStream.Position; // This is our position after reading the pointer, we'll need to return to it + instReader.BaseStream.Position = BaseAddress + keyreg_offset; // Seek to the key region + byte key = instReader.ReadByte(); // Read the key identifierr + KeyHigh = key; // Set the highest key to what we just read. + instReader.BaseStream.Seek(3, SeekOrigin.Current); // 3 bytes, unused. + var VelocityRegionCount = instReader.ReadInt32(); // read the number of entries in the velocity region array + for (int b = 0; b < VelocityRegionCount; b++) + { + var NewVelR = new InstrumentKeyVelocity(); + var velreg_offs = instReader.ReadInt32(); // read the offset of the velocity region + var velreg_retn = instReader.BaseStream.Position; // another one of these. Return pointer + instReader.BaseStream.Position = velreg_offs + BaseAddress; + int VelLow = 0; + int VelHigh = 0; + { + var velocity = instReader.ReadByte(); // The velocity of this key. + VelHigh = velocity; + instReader.BaseStream.Seek(3, SeekOrigin.Current); // Unused. + NewVelR.velocity = velocity; + NewVelR.wsysid = instReader.ReadUInt16(); // This will be the ID of the WAVESYSTEM that its in + NewVelR.wave = instReader.ReadUInt16(); // This will be the ID of the wave inside of that wavesystem + NewVelR.Volume = instReader.ReadSingle(); // Finetune, volume, float + NewVelR.Pitch = instReader.ReadSingle(); // finetune pitch, float. + for (int idx = 0; idx < ( (1+ VelHigh) - VelLow); idx++) // See below for what this is doing + { + NewKey.keys[(VelLow + idx)] = NewVelR; + NewKey.keys[127] = NewVelR; + } + VelLow = VelHigh; + } + instReader.BaseStream.Position = velreg_retn; // return to our pointer position [THIS IS BELOW] + } + for (int idx = 0; idx < (KeyHigh - KeyLow); idx++) // The keys are gappy. + { + NewINST.Keys[(KeyLow + idx)] = NewKey; // So we want to interpolate the previous keys across the empty ones, so that way it's a region + NewINST.Keys[127] = NewKey; + } + KeyLow = KeyHigh; // Set our new lowest key to the previous highest + instReader.BaseStream.Position = keyptr_return; // return to our last pointer position + } + + + break; + } + case PER2: + { + NewINST.IsPercussion = true; + instReader.BaseStream.Seek(0x84, SeekOrigin.Current); // 0x88 - 4 (PERC) + for (int per = 0; per < 100; per++) + { + var NewKey = new InstrumentKey(); + NewKey.keys = new InstrumentKeyVelocity[0x81]; + + var keyreg_offset = instReader.ReadInt32(); // This will be where the data for our key region is. + var keyptr_return = instReader.BaseStream.Position; // This is our position after reading the pointer, we'll need to return to it + if (keyreg_offset == 0) + { + continue; // Skip, its empty. + } + instReader.BaseStream.Position = BaseAddress + keyreg_offset; // seek to position. + NewINST.Pitch = instReader.ReadSingle(); + NewINST.Volume = instReader.ReadSingle(); + instReader.BaseStream.Seek(8, SeekOrigin.Current); + var VelocityRegionCount = instReader.ReadInt32(); // read the number of entries in the velocity region array + for (int b = 0; b < VelocityRegionCount; b++) + { + var NewVelR = new InstrumentKeyVelocity(); + var velreg_offs = instReader.ReadInt32(); // read the offset of the velocity region + var velreg_retn = instReader.BaseStream.Position; // another one of these. Return pointer + instReader.BaseStream.Position = velreg_offs + BaseAddress; + int VelLow = 0; + int VelHigh = 0; + { + var velocity = instReader.ReadByte(); // The velocity of this key. + VelHigh = velocity; + instReader.BaseStream.Seek(3, SeekOrigin.Current); // Unused. + NewVelR.velocity = velocity; + NewVelR.wsysid = instReader.ReadUInt16(); // This will be the ID of the WAVESYSTEM that its in + + NewVelR.wave = instReader.ReadUInt16(); // This will be the ID of the wave inside of that wavesystem + NewVelR.Volume = instReader.ReadSingle(); // Finetune, volume, float + NewVelR.Pitch = instReader.ReadSingle(); // finetune pitch, float. + for (int idx = 0; idx < (VelHigh - (VelLow )); idx++) // See below for what this is doing + { + NewKey.keys[(VelLow + (idx))] = NewVelR; + NewKey.keys[127] = NewVelR; + } + VelLow = VelHigh; + } + instReader.BaseStream.Position = velreg_retn; // return to our pointer position [THIS IS BELOW] + } + instReader.BaseStream.Position = keyptr_return; + NewINST.Keys[per] = NewKey; // oops, add to instrument data or else it doesnt load x.x + NewINST.Keys[127] = NewKey; + } + + break; + } + case PERC: + + break; + } + Instruments[inst_id] = NewINST; // Store it in the instruments bank + } + instReader.BaseStream.Position = anchor; // return back to our original pos to read the next pointer + } + + + } + + } +} diff --git a/JAIMaker/JAI/Types/WSYS/WSYSGroup.cs b/JAIMaker/JAI/Types/WSYS/WSYSGroup.cs new file mode 100644 index 0000000..093a4cd --- /dev/null +++ b/JAIMaker/JAI/Types/WSYS/WSYSGroup.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Be.IO; + +namespace JaiSeqX.JAI.Types.WSYS +{ + public class WSYSGroup + { + private static int SequentialID; + + public string path; // path to the .aw + + public BeBinaryReader handle; // file handle for the .aw + + public int global_id; + + public int unique_id; + + public int[] IDMap; + + public WSYSWave[] Waves; + + + public void UnpackSamples(ref WaveSystem root) + { + for (int i = 0; i < IDMap.Length; i++) + { + var index = IDMap[i]; + if (index > 0) { + + root.waves[index] = Waves[IDMap[index]]; + } + } + } + + + } +} diff --git a/JAIMaker/JAI/Types/WSYS/WSYSWave.cs b/JAIMaker/JAI/Types/WSYS/WSYSWave.cs new file mode 100644 index 0000000..9c6e46d --- /dev/null +++ b/JAIMaker/JAI/Types/WSYS/WSYSWave.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JaiSeqX.JAI.Types.WSYS +{ + public class WSYSWave + { + public int id; + public ushort format; + public ushort key; + public double sampleRate; + public int sampleCount; + + + public uint w_start; + public uint w_size; + + public bool loop; + public int loop_start; + public int loop_end; + + + public string pcmpath; + + } + +} diff --git a/JAIMaker/JAI/Types/WSYS/WaveSystem.cs b/JAIMaker/JAI/Types/WSYS/WaveSystem.cs new file mode 100644 index 0000000..ef3f152 --- /dev/null +++ b/JAIMaker/JAI/Types/WSYS/WaveSystem.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Be.IO; +using System.IO; + +namespace JaiSeqX.JAI.Types.WSYS +{ + /* + Structure of a WSYS + ENDIAN BIG; + int32 'WSYS' = 0x57535953; + int32 size + int32 id + int32 padding + int32 WaveInfo offsets + int32 WaveBaseControlTable offsets; + + Structure of WaveInfo + int32 'WINF' = ? + int32 padding + int32 count + int32xcount WaveGroupOffset + + Structure of WaveBaseControlTable + int32 'WBCT' = ? + int32 padding + int32 count + int32xcount WaveBaseControlTableOffset + + Structure of WaveGroup + string (0x70) AW filename \xff + int32 WaveInfocount + int32 * WaveInfoCount WaveSceneOffset + + // The two below are completely useless in terms of function. I think. + Structure of WaveScene + int32 'SCNE' = 0x53434E45; + + + + */ + public class WaveSystem + { + public int id; + private int size; + public WSYSWave[] waves; + public WSYSGroup[] groups; + + private const int WSYS = 0x57535953; + private const int SCNE = 0x53434E45; + private const int C_DF = 0x432D4446; + + private long BaseAddress; + + private int current_header = 0; // utility + private int back = 0; // utility + + public void LoadWSYS(BeBinaryReader WSYSReader, string baseDir, bool baa) + { + + BaseAddress = WSYSReader.BaseStream.Position; + waves = new WSYSWave[32768]; // TEMPORARY. Fix WSYS Loading! + current_header = WSYSReader.ReadInt32(); + if (current_header != WSYS) + { + Console.WriteLine("Error: Section base at {0} is not WSYS, is instead {1:X}", BaseAddress, current_header); + return; + } + + size = WSYSReader.ReadInt32(); + + id = WSYSReader.ReadInt32(); + + WSYSReader.ReadUInt32(); // 4 bytes, not used. .. I think + + // A little messy, but both of these need to be loaded before we can continue. + var winfo_offset = WSYSReader.ReadInt32(); // relative offset to wave info offset pointer table. + var wbct_offset = WSYSReader.ReadInt32(); // relative offset to wave info offset pointer table. + + int[] winfOffsets; + int[] wbctOffsets; + { + // Load WINF offsets. + WSYSReader.BaseStream.Position = BaseAddress + winfo_offset; + current_header = WSYSReader.ReadInt32(); // Should be WINF + winfOffsets = new int[WSYSReader.ReadInt32()]; // 4 bytes count + + for (int i = 0; i < winfOffsets.Length; i++) + { + winfOffsets[i] = WSYSReader.ReadInt32(); // Int32's following the length. + } + + // Load WBCT data + + WSYSReader.BaseStream.Position = BaseAddress + wbct_offset; + current_header = WSYSReader.ReadInt32(); // Should be WBCT + WSYSReader.ReadUInt32(); // 4 bytes unused? + wbctOffsets = new int[WSYSReader.ReadInt32()]; // 4 bytes count + + for (int i = 0; i < wbctOffsets.Length; i++) + { + wbctOffsets[i] = WSYSReader.ReadInt32(); // Int32's following the length. + } + } + + groups = new WSYSGroup[winfOffsets.Length]; + for (int i=0; i < winfOffsets.Length;i++) + { + + /* This is loading the data for a WINF */ + WSYSReader.BaseStream.Position = BaseAddress + winfOffsets[i]; + + var Group = new WSYSGroup(); + Group.path = Helpers.readArchiveName(WSYSReader); + + var path = Path.Combine(baseDir, baa ? "Waves" : "Banks", Group.path); + var fobj = File.OpenRead(path); // Open the .AW file (AW contains only ADPCM data, nothing else.) + var fobj_reader = new BeBinaryReader(fobj); // Create a reader for it. + + + int waveInfoCounts = WSYSReader.ReadInt32(); // 4 byte count + int[] info_offsets = new int[waveInfoCounts]; + for (int q = 0; q < waveInfoCounts; q++) + { + info_offsets[q] = WSYSReader.ReadInt32(); + } + Group.IDMap = new int[UInt16.MaxValue]; // We have to initialize the containers for the wave information + Group.Waves = new WSYSWave[waveInfoCounts]; + + + /* Since the count should be equal, we're loading the info for the WBCT in he re as well */ + WSYSReader.BaseStream.Position = BaseAddress + wbctOffsets[i]; + // The first several bytes of the WBCT are uselss, a WBCT points directly to a SCNE. + current_header = WSYSReader.ReadInt32(); // This should be SCNE. + WSYSReader.ReadUInt64(); // The next 8 bytes are useless. + var cdf_offset = WSYSReader.ReadInt32(); // However, the next 4 contain the pointer to c_DF relative to our base. + WSYSReader.BaseStream.Position = BaseAddress + cdf_offset; + + current_header = WSYSReader.ReadInt32(); // Should be C_DF. + int waveid_count = WSYSReader.ReadInt32(); // Count of our WAVE ID + int[] waveid_offsets = new int[waveid_count]; // Be ready to store them + for (int q=0; q < waveid_count; q++) + { + waveid_offsets[q] = WSYSReader.ReadInt32(); // Read each waveid + } + + + + // Finally, we're going to get our wave data. + + for (int wav=0;wav < waveInfoCounts; wav++) + { + var o_Wave = new WSYSWave(); + WSYSReader.BaseStream.Position = BaseAddress + waveid_offsets[wav]; + var aw_id = WSYSReader.ReadInt16(); // Strangely enough, it has an AW_ID here. This tells which file it sits in? I guess they're normally loaded separately. + o_Wave.id = WSYSReader.ReadInt16(); // This is the waveid for this wave, it's normally globally unique, but some games hot-load banks. + + + WSYSReader.BaseStream.Position = BaseAddress + info_offsets[wav]; // Seek to the offset of the actual wave parameters. + + WSYSReader.ReadByte(); // I have no clue what the first byte does. + o_Wave.format = WSYSReader.ReadByte(); // Tells what format it's in, usually type 5 AFC (ADPCM) + o_Wave.key = WSYSReader.ReadByte(); // Tells the base key for this wave (0-127 usually) + WSYSReader.ReadByte(); // I have no clue what this byte does. + //var srate = WSYSReader.ReadBytes(4); + + o_Wave.sampleRate = WSYSReader.ReadSingle(); + + /* + * oh. its a float. + * oops + if (o_Wave.format == 5) + { + o_Wave.sampleRate = 32000; /// ???? + } + + if (o_Wave.sampleRate == 5666) // What the actual fuck. Broken value, can't figure out why. + { + o_Wave.sampleRate = 44100; // I guess set the srate to 44100? + } + */ + + o_Wave.w_start = WSYSReader.ReadUInt32(); // 4 byte start in AW + o_Wave.w_size = WSYSReader.ReadUInt32(); // 4 byte size in AW + var b = WSYSReader.ReadUInt32(); + o_Wave.loop = b==UInt32.MaxValue ? true : false; // Weird looping flag? + o_Wave.loop_start = (int)Math.Floor(((WSYSReader.ReadInt32() / 8f) ) * 16f) ; + o_Wave.loop_end = (int)Math.Floor(((WSYSReader.ReadInt32()/8f) ) * 16f) ; + + o_Wave.sampleCount = WSYSReader.ReadInt32(); // 4 byte sample cont + + // Console.WriteLine("L {0:X} (0x{1:X}), LS {2:X} , LE {3:X}, SC {4:X} SZ {5:X}", o_Wave.loop, b, o_Wave.loop_start, o_Wave.loop_end,o_Wave.sampleCount,o_Wave.w_size); + //Console.ReadLine(); + var name = string.Format("0x{0:X}.wav", o_Wave.id); + var name2 = string.Format("0x{0:X}.par", o_Wave.id); + if (!Directory.Exists("./WSYS_CACHE")) + { + Directory.CreateDirectory("./WSYS_CACHE"); + + } + if (!Directory.Exists("./WSYS_CACHE/AW_" + Group.path)) + { + Directory.CreateDirectory("./WSYS_CACHE/AW_" + Group.path); + } + + o_Wave.pcmpath = "./WSYS_CACHE/AW_" + Group.path + "/" + name; + + Group.Waves[wav] = o_Wave; // We're done with just about everything except the PCM data now (ADPCM / AFC conversion) + Group.IDMap[o_Wave.id] = wav; + waves[o_Wave.id] = o_Wave; // TEMPORARY, FIX WSYS LOADING! + + fobj_reader.BaseStream.Position = o_Wave.w_start; + var adpcm = fobj_reader.ReadBytes((int)o_Wave.w_size); + + if (!File.Exists(o_Wave.pcmpath)) + { + Helpers.AFCtoPCM16(adpcm, o_Wave.sampleRate, (int)o_Wave.w_size,o_Wave.format, o_Wave.pcmpath); + } + + + } + + } + + + + + + + + + + } + } +} diff --git a/JAIMaker/JaiMaker.csproj b/JAIMaker/JaiMaker.csproj new file mode 100644 index 0000000..cb606d4 --- /dev/null +++ b/JAIMaker/JaiMaker.csproj @@ -0,0 +1,41 @@ + + + + WinExe + net10.0 + enable + enable + app.manifest + true + jcion.ico + + + + + + + + + None + All + + + + + + + Bass.Net.dll + + + + + + + + + + + + + + diff --git a/JAIMaker/Keyboard.cs b/JAIMaker/Keyboard.cs new file mode 100644 index 0000000..f826d3a --- /dev/null +++ b/JAIMaker/Keyboard.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Drawing; +using System.Runtime; +using System.Runtime.InteropServices; +using System.Diagnostics; +using JaiSeqX.Player; + +namespace JaiMaker +{ + public static class Keyboard + { + public static BMSChannelManager channelManager = new BMSChannelManager(); + static string keyOrderString = @"1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./"; + static int[] pitches; + public static void init() + { + var lastPitch = 0; + pitches = new int[1024]; + for (int i=0; i < keyOrderString.Length;i++) + { + var str = keyOrderString[i]; + pitches[str] = lastPitch++; + } + } + + + public static void stopSound(byte inkey) + { + channelManager.stopVoice(0, inkey); + } + public static void startSound(byte inkey) + { + + var prog = Root.currentProg; + if (prog!=null) + { + var note = pitches[inkey] + Root.keyOffset; + var vel = Root.currentVel; + + if (prog.Keys[note]!=null) + { + var notedata = prog.Keys[note]; + var key = notedata.keys[vel]; + + if (key!=null) + { + try + { + var wsysid = key.wsysid; + var waveid = key.wave; + var wsys = Root.allWSYS[wsysid]; + if (wsys != null) + { + + var wave = wsys.waves[waveid]; + var sound = channelManager.loadSound(wave.pcmpath, wave.loop, wave.loop_start, wave.loop_end).CreateInstance(); + var pmul = prog.Pitch * key.Pitch; + var vmul = prog.Volume * key.Volume; + var real_pitch = Math.Pow(2, ((note - wave.key) * pmul) / 12); + var true_volume = (Math.Pow(((float)vel + Root.keyOffset) / 127, 2) * vmul) * 0.5; + sound.Volume = (float)(true_volume * 0.6); + sound.ShouldFade = true; + sound.FadeOutMS = 30; + if (prog.IsPercussion) + { + real_pitch = (float)(key.Pitch * prog.Pitch); + + sound.ShouldFade = true; + sound.FadeOutMS = 200; // no instant stops + } + sound.Pitch = (float)(real_pitch); + + channelManager.startVoice(sound, 0, inkey); + + sound.Play(); + + + } + else + { + Console.WriteLine("Null WSYS??"); + } + } + catch (Exception E) + { + var b = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("fuuuuuck"); + Console.WriteLine(E.ToString()); + Console.ForegroundColor = b; + } + } else + { + Console.WriteLine("Null key :("); + } + + + } else + { + Console.WriteLine("ugh."); + } + } + + } + } +} diff --git a/JAIMaker/MainWindow.axaml b/JAIMaker/MainWindow.axaml new file mode 100644 index 0000000..ee58337 --- /dev/null +++ b/JAIMaker/MainWindow.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +