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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/JAIMaker/MainWindow.axaml.cs b/JAIMaker/MainWindow.axaml.cs
new file mode 100644
index 0000000..94e0d7d
--- /dev/null
+++ b/JAIMaker/MainWindow.axaml.cs
@@ -0,0 +1,566 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using JaiSeqX.JAI.Seq;
+using JaiSeqX.JAI.Types;
+using JaiSeqX.JAI;
+using MidiSharp;
+using System.IO;
+using System.Diagnostics;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+
+using MidiSharp.Events.Meta;
+
+using MsBox.Avalonia;
+using MsBox.Avalonia.Enums;
+
+namespace JaiMaker
+{
+ public partial class MainWindow : Window
+ {
+ public static readonly DirectProperty FunctionsEnabledProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(FunctionsEnabled),
+ o => o.FunctionsEnabled,
+ (o, v) => o.FunctionsEnabled = v);
+
+ public static readonly DirectProperty MidiFunctionsEnabledProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(MidiFunctionsEnabled),
+ o => o.MidiFunctionsEnabled,
+ (o, v) => o.MidiFunctionsEnabled = v);
+
+ public int[] bankMap = new int[1024];
+ public int[] progMap = new int[1024];
+ InstrumentBank? currentIBNK;
+ Instrument? currentInst;
+ MidiSequence? currentSequence;
+ // KeysConverter kk;
+ public BitArray keysPressed = new(1024);
+ string JaiFile = "";
+ private bool newBms = false;
+
+ private readonly List<(NumericUpDown Bank, NumericUpDown Program, Button Insert)> _trackControls = [];
+
+ private Dictionary