diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b06e864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml diff --git a/src/App.config b/src/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/src/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/App.xaml b/src/App.xaml new file mode 100644 index 0000000..edf0794 --- /dev/null +++ b/src/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/App.xaml.cs b/src/App.xaml.cs new file mode 100644 index 0000000..7557369 --- /dev/null +++ b/src/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Degausser +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/src/BBPRecord.cs b/src/BBPRecord.cs new file mode 100644 index 0000000..bd52622 --- /dev/null +++ b/src/BBPRecord.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using static Degausser.Utils.GZip; + +namespace Degausser +{ + class BBPRecord + { + // Quick information + public string FullPath { get; } + public string Filename => Path.GetFileName(FullPath); + public string Folder => Path.GetFileName(Path.GetDirectoryName(FullPath)); + public string Title => mgrItem.Title.Replace("\n", ""); + public string Author => mgrItem.Author; + public bool HasKaraoke => mgrItem.Flags.HasLyrics; + public bool HasGuitar => mgrItem.Flags.HasGuitar; + public bool HasPiano => mgrItem.Flags.HasPiano; + public int Lines => bbp.linesPlus6 - 6; + public int Instruments => bbp.channelInfo.Count(c => c.instrument != 0); + public int Slot { get; } + + public JbMgrFormat.JbMgrItem mgrItem; + public BBPFormat bbp; + public byte[] vocaloid; // uncompressed + + // From known item and packpath + public BBPRecord(JbMgrFormat.JbMgrItem item, string path, int slot) + { + Slot = slot; + FullPath = path; + mgrItem = item; + if (item.Flags.OnSD) + { + var buffer = File.ReadAllBytes(path); + var bytes = buffer; + var nums = Enumerable.Range(0, 17).Select(i => BitConverter.ToInt32(bytes, i * 4)).ToList(); + + var unc1 = Decompress(bytes, nums[3], nums[4]); + bbp = unc1.ToStruct(); + if (nums[5] == 1) vocaloid = Decompress(bytes, nums[7], nums[8]); + } + else + { + bbp = new BBPFormat + { + title = item.Title, + channelInfo = new BBPFormat.ChannelInfo[0] + }; + } + } + + // From a file + public BBPRecord(string path) + { + FullPath = path; + if (Path.GetExtension(path).ToLower() == ".bdx") + { + var bytes = File.ReadAllBytes(path); + if (bytes.Length != 32768) + { + throw new InvalidDataException($"{path} has an incorrect filesize!"); + } + var bdx = bytes.ToStruct(); + bbp = BDX2BBP.Convert(bdx, out mgrItem); + //mgrItem = new JbMgrFormat.JbMgrItem() { Author = bdx.contributor.ToString() }; + } + else + { + var buffer = File.ReadAllBytes(path); + mgrItem = buffer.Take(312).ToArray().ToStruct(); + var bytes = buffer.Skip(312).ToArray(); + var nums = Enumerable.Range(0, 17).Select(i => BitConverter.ToInt32(bytes, i * 4)).ToList(); + + var unc1 = Decompress(bytes, nums[3], nums[4]); + bbp = unc1.ToStruct(); + if (nums[5] == 1) vocaloid = Decompress(bytes, nums[7], nums[8]); + } + } + + public void SaveAsBBPFile(string path) + { + var item = mgrItem; + item.Scores = new byte[50]; + item.Singer = JbMgrFormat.JbMgrItem.JbSinger.None; + item.Icon = JbMgrFormat.JbMgrItem.JbIcon.None; + item.Flags.flag &= 0x7FDFFF; + var itemData = item.StructToArray(); + var packData = Recompile(); + + using (var fo = File.Open(path, FileMode.Create)) + { + fo.Write(itemData, 0, itemData.Length); + fo.Write(packData, 0, packData.Length); + } + } + + public void SaveAsPackFile(string path) + { + File.WriteAllBytes(path, Recompile()); + } + + public byte[] Recompile() + { + var unc1 = bbp.StructToArray(); + var unc2 = vocaloid; + byte[] result; + + var c1 = Compress(unc1); + var nums = new int[17]; + nums[0] = 0x20001; + nums[1] = 1; + nums[2] = unc1.Length; + nums[3] = 68; + nums[4] = c1.Length; + + if (unc2 == null) + { + result = new byte[c1.Length + 68]; + } + else + { + int c1len4 = (c1.Length + 3) & ~3; + var c2 = Compress(unc2); + nums[5] = 1; + nums[6] = unc2.Length; + nums[7] = 68 + c1len4; + nums[8] = c2.Length; + result = new byte[c1len4 + c2.Length + 68]; + c2.CopyTo(result, 68 + c1len4); + } + c1.CopyTo(result, 68); + + for (int i = 0; i < 17; i++) + { + BitConverter.GetBytes(nums[i]).CopyTo(result, i * 4); + } + return result; + } + + MidiPlayer.MidiData midiData; + public MidiPlayer.MidiData GetMidiData() + { + if (midiData == null) + { + SetUpMidi(); + } + return midiData; + } + + public string GetInstrumentName(int chanNumber) + { + var c = bbp.channelInfo[chanNumber]; + int u = c.instrument; + if (u == 0) return "(Blank)"; + string clone = c.cloneID > 0 ? " " + c.cloneID : null; + return $"{InstrumentData.Instruments[u].Name}{clone} ({c.playType})"; + } + + static void RunChangeTracker(TimeValuePair[] array, int length, Action action) + { + for (int i = 0, cursor = 0; i < length; i++) + { + if (cursor < array.Length - 1 && i == (array[cursor + 1]).time) + { + cursor++; + } + action(i, array[cursor].value); + } + } + + static IEnumerable ChangeTracker(IList array, int length) + { + for (int i = 0, cursor = 0; i < length; i++) + { + if (cursor < array.Count - 1 && i == (array[cursor + 1]).time) + { + cursor++; + } + yield return array[cursor].value; + } + } + + void SetUpMidi() + { + midiData = new MidiPlayer.MidiData(ChangeTracker(bbp.tempoChanges, Lines * 48).ToArray()); + + for (int i = 0; i < 10; i++) + { + // volume as well?? + var volume = (from item in ChangeTracker(bbp.volumeChanges[i].changes, Lines * 48) + let vol = item * bbp.channelInfo[i].volume * (bbp.masterVolumeMaybe + 64) + select (byte)Math.Min(127, vol >> 14)) + .ToList(); + + var c = midiData.Channels[i]; + var info = bbp.channelInfo[i]; + var instr = InstrumentData.Instruments[info.instrument]; + c.InstrumentMidi = instr.Midi; + c.InstrumentName = instr.Name; + var drum = instr.DrumNotes; + + // Adjust instrument name + if (c.InstrumentMidi < 128) // 0-127 are real MIDI instruments + { + if (info.cloneID > 0) c.InstrumentName += $" {info.cloneID}"; + c.InstrumentName += $" ({info.playType})"; + } + + List pianoTracker; + if (info.playType == PlayType.Piano) + { + var pianoChordSegment = new ArraySegment(bbp.pianoChordChangeTable, 0, bbp.pianoChordChangesCount); + pianoTracker = (from val in ChangeTracker(pianoChordSegment, Lines * 48) + select new IndexRootPair { index = val.HiByte(), rawRoot = val.LoByte() }) + .ToList(); + } + + // do the guitar remapping at this point + + for (int j = 0; j < Lines * 4; j++) + { + Action, Action, Action> DoThreeIf = (cmp, notes, releaseAction, playAction) => + { + int frame = j * 12; + int isThreeNotes = notes.First() == cmp ? 1 : 0; + + foreach (var note in notes.Skip(isThreeNotes)) + { + if (note < 128) + { + releaseAction(frame); + if (note > 0) playAction(frame, note, volume[frame]); + } + frame += 3 + isThreeNotes; + } + }; + + var seg = new ArraySegment(bbp.channelNotes[i].notes, j * 4, 4); + switch (bbp.channelInfo[i].playType) + { + case PlayType.Standard: + DoThreeIf(0xFF, seg, c.ReleaseNote, c.AddNote); + break; + case PlayType.Drum: + DoThreeIf(0xF, seg.Select(x => (byte)(x & 15)), _ => { }, (frame, note, vol) => c.AddDrum(frame, drum[note], vol)); + DoThreeIf(0xF, seg.Select(x => (byte)(x >> 4)), _ => { }, (frame, note, vol) => c.AddDrum(frame, drum[note], vol)); + break; + case PlayType.Guitar: + break; + case PlayType.Piano: + //DoThreeIf(0xFF, seg, c.ReleaseChord, (frame, note, vol) => c.AddChord(frame, new PianoChord(bbp.pianoOrig[note - 1]), vol)); + break; + } + } + } + + } + + const byte FAKE_VOLUME = 80; + + class GuitarChord : MidiPlayer.Chord + { + int bdxData; + + public GuitarChord(int data) + { + bdxData = data; + } + + public GuitarChord(byte rootNote, int mapIndex) + { + bdxData = BitConverter.ToInt32(InstrumentData.GuitarChords, 52 * rootNote + 4 * mapIndex); + } + + public override byte[] Notes + { + get + { + byte[] notes = new byte[6]; + for (int i = 0; i < 6; i++) + { + int newnote = bdxData >> (5 * i) & 31; + notes[i] = (byte)(newnote < 16 ? InstrumentData.GuitarTuning[i] + newnote : 0); + } + return notes; + } + } + } + + void AddGuitarNotes(MidiPlayer.MidiChannel c, int frame, IEnumerable notes) + { + int isThreeNotes = notes.First() == 0xFF ? 1 : 0; + + foreach (var note in notes.Skip(isThreeNotes)) + { + if (note < 128) + { + c.ReleaseChord(frame); + var i = note / 4; + int remapped = 0; + if (note > 0) c.AddChord(frame, new GuitarChord(remapped), FAKE_VOLUME); + } + frame += 3 + isThreeNotes; + } + } + + class PianoChord : MidiPlayer.Chord + { + public PianoChord(int encodedNotes) + { + Notes = BitConverter.GetBytes(encodedNotes); + } + } + + } +} diff --git a/src/BDXRecordInfo.cs b/src/BDXRecordInfo.cs new file mode 100644 index 0000000..f4f5b88 --- /dev/null +++ b/src/BDXRecordInfo.cs @@ -0,0 +1,220 @@ +using System; +using System.IO; +using System.Diagnostics; +using System.Security.Cryptography; +using Degausser.Properties; +using System.Runtime.InteropServices; +using System.Linq; + +namespace Degausser +{ + public class BDXInformation + { + public string Filename { get; set; } + public string FullPath { get; set; } + public string Title { get; set; } + public int Lines { get; set; } + public bool IsKaraoke { get; set; } + public string Folder { get; set; } + public string Contributor { get; set; } + public TimeSpan Duration { get; set; } + } + + partial class BDXRecord + { + #region A bunch of BDX stuff + int lines; + ChangeList tempoTimer; + short[] tempoValue = new short[32]; + GuitarChord[] guitarOriginal = new GuitarChord[16]; + ChangeTracker guitarTimer = new ChangeTracker(32); + IndexRootPair[][] guitarMapIndexRootPairs = new IndexRootPair[32][]; + byte[] karaokeLyrics; + short[] karaokeTimer = new short[2048]; + PianoChord[] pianoOriginal = new PianoChord[32]; + IndexRootPair[] pianoMapIndexRootPair = new IndexRootPair[32]; + ChangeTracker chordTimer = new ChangeTracker(255); + int[] chordIndex = new int[255]; + PianoChord[] remappedChords = new PianoChord[32]; + GuitarChord[,] remappedGuitar = new GuitarChord[32, 9]; + #endregion + + class BDXChannel + { + public int volume; + public int instrument; + public PlayType playType; + public int panning; + public int master, pro, amateur, beginner; + public int cloneID; + public byte[] notes; + public ChangeTracker changeTimer = new ChangeTracker(32); + public int[] changeValue = new int[32]; + public int[] changeVolume = new int[32]; + public int[] changeType = new int[32]; + //public byte[] clef; + }; + + /* + * To be used in things with changes denoted by time, such as: + * tempo changes (ended by ffff) + * key/volume changes (ended by ffff) + * guitar chord map changes (ended by ffff) + * karaoke lyrics (ended by 3fff) + * chord arrangements in composer (counted) + */ + class ChangeTracker + { + short[] times; + int internalCursor = 0; + int externalCursor = 0; + + public ChangeTracker(int n) + { + times = new short[n]; + } + + public void AddTime(short time) => times[internalCursor++] = time; + + public int this[int index] + { + get + { + if (externalCursor + 1 < times.Length) + { + if (index == times[externalCursor + 1]) + { + externalCursor++; + } + } + return externalCursor; + } + } + } + + class ChangeList + { + T[] BackingArray; + int[] lookup; + + public ChangeList(T[] source, int max) + { + BackingArray = source; + var times = BackingArray.Select(x => (int)((dynamic)x).time).ToList(); + times.Add(int.MaxValue); + + lookup = new int[max]; + for (int i = 0, cursor = 0; i < max; i++) + { + if (i == times[cursor + 1]) + { + cursor++; + } + lookup[i] = cursor; + } + } + + public T GetSnapshotAtTime(int time) + { + return BackingArray[lookup[time]]; + } + } + + public BDXRecord(string path) + { + bdx = File.ReadAllBytes(path).ToStruct(); + Information = new BDXInformation + { + Lines = bdx.linesPlus6 - 6, + Title = String.Concat(bdx.labels).Replace("\n", ""), + Contributor = bdx.contributor.ToString(), + IsKaraoke = bdx.hasKaraoke != 0, + Filename = Path.GetFileName(path), + Folder = Path.GetFileName(Path.GetDirectoryName(path)), + FullPath = path + }; + } + + public static BDXInformation GetInformation(string path) + { + // to be replaced with a lightweight version + return new BDXRecord(path).Information; + } + + public /* SHOULD NOT BE PUBLIC */ BDXFormat bdx; + + public BDXInformation Information { get; } = new BDXInformation(); + + // Extracts information from the GAK Header and BDX Header and uncompressed data + void ParseData() + { + lines = Information.Lines; + + // Populate channel info + for (int i = 0; i < 8; i++) + { + var c = bdx.channelInfo[i]; + var ch = channel[i] = new BDXChannel + { + volume = c.volume, + //instrument = InstrumentMap(c.Instrument), + instrument = c.instrument, + playType = (PlayType)c.playType, + panning = c.panning, + master = c.masterProStar % 16, + pro = c.masterProStar / 16, + cloneID = c.cloneID, + amateur = c.amaBegiStar % 16, + beginner = c.amaBegiStar / 16, + notes = bdx.channelNotes[i].notes + //clef = bdx.keySig[i].stuff + }; + for (int j = 0; j < 8; j++) + { + var c5 = bdx.chanInfo5[i].stuff[j]; + ch.changeTimer.AddTime(c5.time); + ch.changeValue[j] = c5.value; + ch.changeVolume[j] = c5.volume; + ch.changeType[j] = c5.type; + } + } + + tempoTimer = new ChangeList(bdx.tempoTimer, lines * 48); + + for (int i = 0; i < 32; i++) + { + //tempoTimer.AddTime(bdx.TempoTimer[i].Time); + //tempoValue[i] = bdx.TempoTimer[i].Value; + + guitarTimer.AddTime(bdx.guitarTimer[i].time); + guitarMapIndexRootPairs[i] = bdx.guitarTimer[i].pair; + + pianoOriginal[i] = new PianoChord(BitConverter.GetBytes(bdx.pianoOrig[i])); + } + pianoMapIndexRootPair = bdx.pianoPair; + + guitarOriginal = bdx.guitarOrig.Select(x => new GuitarChord(x)).ToArray(); + + karaokeLyrics = bdx.karaokeLyrics; + karaokeTimer = bdx.karaokeTimer; + //chordChanges = bdx.ChordChanges; + + for (int i = 0; i < 255; i++) + { + chordTimer.AddTime(bdx.chordTimer[i].time); + chordIndex[i] = bdx.chordTimer[i].value; + } + + //keySignature = bdx.KeySig[8].Stuff; + //PianoChord.highestNote = bdx.PianoHighestNote; + //if (PianoChord.highestNote == 0) { PianoChord.highestNote = 70; } + //pianoVoicingStyle = bdx.PianoVoicingStyle; + //pianoAvailability = bdx.PianoAvailability; + } + + //public Karaoke GetKaraoke() + //{ + // return new Karaoke(karaokeLyrics, karaokeTimer); + //} + } +} \ No newline at end of file diff --git a/src/BDXRecordMidi.cs b/src/BDXRecordMidi.cs new file mode 100644 index 0000000..9989dc6 --- /dev/null +++ b/src/BDXRecordMidi.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Degausser.Properties; + +namespace Degausser +{ + partial class BDXRecord + { + class PianoChord : MidiPlayer.Chord + { + public PianoChord(byte[] notes) + { + Notes = notes; + } + + public PianoChord(int mapIndex, byte rootNote, int highestNote, int minimum) + { + var notes = (from note in InstrumentData.PianoChords.Skip(4 * mapIndex).Take(4) + let newnote = ((note + rootNote) % 12 - highestNote) % 12 + highestNote + orderby newnote descending + select (byte)newnote) + .ToArray(); + //Array.Copy(InstrumentData.PianoChords, 4 * mapIndex, notes, 0, 4); + //for (int i = 0; i < 4; i++) + //{ + // if (notes[i] > 0) + // { + // notes[i] = (byte)(((notes[i] + rootNote) % 12 - highestNote) % 12 + highestNote); + // } + //} + if (notes[3] == 0 && minimum == 4) + { + notes[3] = (byte)(notes.Take(3).Max() - 12); + } + Notes = notes; + } + } + + class GuitarChord : MidiPlayer.Chord + { + int bdxData; + + public GuitarChord(int data) + { + bdxData = data; + } + + public GuitarChord(byte rootNote, int mapIndex) + { + bdxData = BitConverter.ToInt32(InstrumentData.GuitarChords, 52 * rootNote + 4 * mapIndex); + } + + public override byte[] Notes + { + get + { + byte[] notes = new byte[6]; + for (int i = 0; i < 6; i++) + { + int newnote = bdxData >> (5 * i) & 31; + notes[i] = (byte)(newnote < 16 ? InstrumentData.GuitarTuning[i] + newnote : 0); + } + return notes; + } + } + } + + BDXChannel[] channel = new BDXChannel[8]; + MidiPlayer.MidiData midiData = null; + + public MidiPlayer.MidiData GetMidiData() + { + if (midiData == null) + { + ParseData(); + //SetUpMidi(); + } + return midiData; + } + + //public void SetUpMidi() + //{ + // midiData = new MidiPlayer.MidiData(lines * 48); + // for (int i = 0; i < lines * 48; i++) + // { + // midiData.Tempo[i] = tempoTimer.GetSnapshotAtTime(i).value; + // } + + // byte[] volume = new byte[lines * 48]; + // for (int i = 0; i < 8; i++) + // { + // midiData[i].InstrumentName = GetInstrumentName(i); + // //midiData[i].Instrument = Resourcez.Instrument[channel[i].instrument]; // uses 0-127 + // midiData[i].Instrument = InstrumentData.Instruments[channel[i].instrument].Midi; // uses 0-127 + // #region Set Volumes + // for (int j = 0; j < lines * 48; j++) + // { + // int vol = channel[i].changeVolume[channel[i].changeTimer[j]]; + // vol *= channel[i].volume * (bdx.masterVolume + 64); + // volume[j] = (byte)Math.Min(127, vol >> 14); + // } + // #endregion + // int frame = 0; + // byte note = 0; + // bool IsThreeNotes = false; + + // switch (channel[i].playType) + // { + // case PlayType.Standard: + // #region Normal Instrument Code + // for (int j = 0; j < lines * 16; j++) + // { + // note = channel[i].notes[j]; + // if ((j & 3) == 0 && (IsThreeNotes = (note == 0xFF))) + // { + // continue; + // } + // else if ((note & 0x80) == 0) + // { + // midiData[i].ReleaseNote(frame); + // if (note > 0) + // { + // midiData[i].AddNote(frame, note, volume[frame]); + // } + // } + // frame += IsThreeNotes ? 4 : 3; + // } + // #endregion + // break; + // case PlayType.Drum: + // #region Drum Code + // for (int j = 0; j < lines * 4; j++) + // { + // int[,] drumnote = new int[2, 4]; + // for (int k = 0; k < 4; k++) + // { + // drumnote[0, k] = channel[i].notes[4 * j + k] >> 4; + // drumnote[1, k] = channel[i].notes[4 * j + k] & 0xF; + // } + // int[] delta = { 3, 3 }, cursor = { 0, 0 }; + // for (int k = 0; k < 1; k++) + // { + // if (drumnote[0, k] == 0xF) + // { + // delta[k] = 4; + // cursor[k]++; + // } + // } + // for (int k = 0; k < 12; k++) + // { + // for (int m = 0; m < 1; m++) + // { + // if (k % delta[m] == 0) + // { + // byte sound = InstrumentData.Instruments[channel[i].instrument].DrumNotes[drumnote[m, cursor[m]++]]; + // midiData[i].AddDrum(frame, sound, volume[frame]); + // } + // } + // frame++; + // } + // } + // #endregion + // break; + // case PlayType.Guitar: + // #region Guitar Chord Code + // RemapGuitarChords(); + // for (int j = 0; j < lines * 16; j++) + // { + // note = channel[i].notes[j]; + // if ((j & 3) == 0 && (IsThreeNotes = (note == 0xFF))) + // { + // continue; + // } + // else if ((note & 0x80) == 0) + // { + // midiData[i].ReleaseChord(frame); + // if (note > 0) + // { + // midiData[i].AddChord(frame, remappedGuitar[guitarTimer[frame], note / 4], volume[frame]); + // } + // } + // frame += IsThreeNotes ? 4 : 3; + // } + // #endregion + // break; + // case PlayType.Piano: + // #region Piano Chord Code + // RemapPianoChords(); + // for (int j = 0; j < lines * 16; j++) + // { + // note = channel[i].notes[j]; + // if ((j & 3) == 0 && (IsThreeNotes = (note == 0xFF))) + // { + // continue; + // } + // else if ((note & 0x80) == 0) + // { + // midiData[i].ReleaseChord(frame); + // if (note > 0) + // { + // midiData[i].AddChord(frame, remappedChords[note - 1], volume[frame]); + // } + // } + // frame += IsThreeNotes ? 4 : 3; + // } + // #endregion + // break; + // } + // } + //} + + void RemapPianoChords() + { + int minimum = 3 + (bdx.pianoVoicingStyle / 3); + int spread = bdx.pianoVoicingStyle % 3; // don't know what this does yet + int highestNote = bdx.pianoHighestNote == 0 ? 70 : bdx.pianoHighestNote; + for (int i = 0; i < 32; i++) + { + var pair = pianoMapIndexRootPair[i]; + if (pair.Root == 0xFF) + { + if (pair.index == 0xFF) { break; } + remappedChords[i] = pianoOriginal[pair.index]; + } + else + { + remappedChords[i] = new PianoChord(pair.index, pair.Root, bdx.pianoHighestNote, minimum); + } + } + } + + void RemapGuitarChords() + { + for (int i = 0; i < 32; i++) + { + for (int j = 0; j < 9; j++) + { + if (guitarMapIndexRootPairs[i][j].Root == 0xFF) + { + if (guitarMapIndexRootPairs[i][j].index == 0xFF) { continue; } + remappedGuitar[i, j] = guitarOriginal[guitarMapIndexRootPairs[i][j].index]; + } + else + { + remappedGuitar[i, j] = new GuitarChord(guitarMapIndexRootPairs[i][j].Root, guitarMapIndexRootPairs[i][j].index); + } + } + } + } + + public string GetInstrumentName(int chanNumber) + { + int u = channel[chanNumber].instrument; + if (u == 0) return "(Blank)"; + string clone = channel[chanNumber].cloneID > 0 ? " " + channel[chanNumber].cloneID : null; + return $"{InstrumentData.Instruments[u].Name}{clone} ({channel[chanNumber].playType})"; + } + } +} diff --git a/src/Degausser.csproj b/src/Degausser.csproj new file mode 100644 index 0000000..dd6af66 --- /dev/null +++ b/src/Degausser.csproj @@ -0,0 +1,137 @@ + + + + + Debug + AnyCPU + {72B177C2-0594-4275-9FCD-F74617DC0557} + WinExe + Properties + Degausser + Degausser + v4.5.2 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + SortableListView.xaml + + + + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + + + Code + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Degausser.sln b/src/Degausser.sln new file mode 100644 index 0000000..b848af6 --- /dev/null +++ b/src/Degausser.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Degausser", "Degausser.csproj", "{72B177C2-0594-4275-9FCD-F74617DC0557}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {72B177C2-0594-4275-9FCD-F74617DC0557}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72B177C2-0594-4275-9FCD-F74617DC0557}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B177C2-0594-4275-9FCD-F74617DC0557}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72B177C2-0594-4275-9FCD-F74617DC0557}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/File Formats/BBPFormat.cs b/src/File Formats/BBPFormat.cs new file mode 100644 index 0000000..deaaffd --- /dev/null +++ b/src/File Formats/BBPFormat.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; + +namespace Degausser +{ + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + class BBPFormat + { + public int version; + public int titleID; + public int dateCreated; + public int dateModified; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 252)] + public string title; + public byte linesPlus6; + public byte timeSignature; + public byte masterVolumeMaybe; + public byte mainInstrumentMaybe; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelInfo + { + public PlayType playType; + public byte instrument; + public byte volume; + public byte cloneID; + public byte masterProStar; + public byte amaBegiStar; + public byte zero; + public byte eight; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public int[] otherInformation; // master pro star etc. + public SoundEnvelope env1; + public int unknown1; + public SoundEnvelope env2; + public int unknown2; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public ChannelInfo[] channelInfo; // attack? decay? etc. + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelNotes + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2400)] + public byte[] notes; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public ChannelNotes[] channelNotes; + + //[System.Diagnostics.DebuggerDisplay("({string.Join(\",\",stuff.Distinct())})")] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SomeMask + { + public override string ToString() => string.Join(",", stuff.Distinct()); + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 600)] + public short[] stuff; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public SomeMask[] unknownMask; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] + public TimeValuePair[] tempoChanges; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Changes512 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] + public TimeValuePair[] changes; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Changes1024 + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] + public TimeValuePair[] changes; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public Changes1024[] volumeChanges; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public Changes512[] rangeChanges; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public Changes512[] effectorChanges; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct PanningStuff + { + public short zero1; + public short value; + public short minusOne; + public short zero2; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public PanningStuff[] panningMaybe; + + // 0xE2DC + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public int[] guitarOrig; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct GuitarStuff + { + public short time; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public IndexRootPair[] pair; + } + + // 0xE35C + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] + public GuitarStuff[] guitarTimer; + + // 0xE77C + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 600)] + public long[] unknownGuitar3; + + // 0xFA3C + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 120)] + public IndexRootPair[] pianoPair; + + // 0xFB2C + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)] + public int[] pianoOrig; // need to convert the indexed bdx pianopairs to pianoorig + + // 0xFC2C + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 3000)] + public string karaokeLyrics; + + // 0x1139C + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3000)] + public short[] karaokeTimer; // all ORed with 0x8000 + + // 0x12B0C + //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6600)] + //public byte[] unknownStuff; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct KeySignature + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 150)] + public int[] stuff; + } + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + public KeySignature[] keySig; + + // 0x144D4 + public int pianoChordChangesCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 300)] + public TimeValuePair[] pianoChordChangeTable; + + // 0x14988 + public int guitarChordChangesCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 300)] + public TimeValuePair[] guitarChordChangeTable; + } +} diff --git a/src/File Formats/BDX2BBP.cs b/src/File Formats/BDX2BBP.cs new file mode 100644 index 0000000..b9f6231 --- /dev/null +++ b/src/File Formats/BDX2BBP.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using static Degausser.Utils.Language; + +namespace Degausser +{ + class BDX2BBP + { + public static BBPFormat Convert(BDXFormat bdx, out JbMgrFormat.JbMgrItem mgr) + { + var converted = new BDX2BBP(bdx); + mgr = converted.mgr; + return converted.bbp; + } + + readonly BDXFormat bdx; + readonly BBPFormat bbp; + readonly JbMgrFormat.JbMgrItem mgr; + + BDX2BBP(BDXFormat bdx) + { + this.bdx = bdx; + bbp = new byte[Marshal.SizeOf()].ToStruct(); + DoCommonStuff(); + DoChannelStuff(); + var instrTypes = bbp.channelInfo.Select(c => c.playType); + var hasPiano = instrTypes.Contains(PlayType.Piano); + var hasGuitar = instrTypes.Contains(PlayType.Guitar); + var hasDrum = instrTypes.Contains(PlayType.Drum); + + if (hasPiano && hasGuitar) throw new Exception("Not expecting both to exist!!!"); + + if (hasPiano) + { + DoPianoStuff(); + } + else if (hasGuitar) + { + DoGuitarStuff(); + } + DoKaraokeStuff(); + // DoMetadataStuff() // mainly Author + + // metadata stuff temporarily here + mgr = JbMgrFormat.JbMgrItem.Empty; + //mgr.Author = bdx.contributor.ToString(); + mgr.Author = "Degausser2.0a"; + mgr.Title = bbp.title; + mgr.TitleSimple = mgr.Title; + //mgr.Flags + mgr.ID = 0x80000001; + mgr.Flags = new JbMgrFormat.JbMgrItem.JbFlags + { + HasDrum = hasDrum, + HasGuitar = hasGuitar, + HasPiano = hasPiano, + HasLyrics = bdx.hasKaraoke != 0, + HasMelody = bdx.mainInstrument != 0xFF, // not sure about this + IsValid = true, + OnSD = true, + Parts = bdx.channelInfo.Count(c => c.instrument != 0) + }; + } + + void DoCommonStuff() + { + bbp.title = string.Concat(bdx.labels).Replace("\n", ""); + bbp.version = 0x20001; + bbp.dateCreated = bdx.dateCreated; + bbp.dateModified = bdx.dateModified; + bbp.linesPlus6 = bdx.linesPlus6; + bbp.timeSignature = bdx.timeSignature; + bbp.masterVolumeMaybe = bdx.masterVolume; + bbp.mainInstrumentMaybe = bdx.mainInstrument; + + bdx.tempoTimer.CopyTo(bbp.tempoChanges, 0); // tempo + } + + void DoChannelStuff() + { + for (int i = 0; i < 8; i++) + { + bbp.channelInfo[i].cloneID = bdx.channelInfo[i].cloneID; + bbp.channelInfo[i].env1 = bdx.channelEnvelopes[i]; + bbp.channelInfo[i].env2 = bdx.channelEnvelopes[i]; + bbp.channelInfo[i].instrument = bdx.channelInfo[i].instrument; + bbp.channelInfo[i].playType = bdx.channelInfo[i].playType; + bbp.channelInfo[i].volume = (byte)bdx.channelInfo[i].volume; + bbp.channelInfo[i].masterProStar = bdx.channelInfo[i].masterProStar; + bbp.channelInfo[i].amaBegiStar = bdx.channelInfo[i].amaBegiStar; + //bbp.channelInfo[i].zero = 0; // ?? + bbp.channelInfo[i].eight = 8; // ?? + //bbp.channelInfo[i].otherInformation // has information about stars and stuff + //bbp.channelInfo[i].unknown1 = 0; // ?? + //bbp.channelInfo[i].unknown2 = 0; // ?? + + bdx.channelNotes[i].notes.CopyTo(bbp.channelNotes[i].notes, 0); + + bbp.panningMaybe[i].value = (short)(bdx.channelInfo[i].panning * 2); + + for (int j = 0; j < 32; j++) + { + bbp.volumeChanges[i].changes[j] = new TimeValuePair + { + time = bdx.chanInfo5[i].stuff[j].time, + value = bdx.chanInfo5[i].stuff[j].volume + }; + } + for (int j = 0; j < 600; j++) + { + bbp.unknownMask[i].stuff[j] = (short)-1; + } + //bbp.unknownMask + //bbp.volumeChanges + //bbp.rangeChanges + //bbp.effectorChanges // effectively zero, doesn't need changing + foreach (var x in bbp.effectorChanges) + { + x.changes[1].time = -1; + } + } + } + + void DoPianoStuff() + { + // do piano chords first + bbp.pianoPair = Enumerable.Repeat(new IndexRootPair { index = 0xFF, rawRoot = 0xFF }, 120).ToArray(); + //bbp.pianoOrig = new int[64]; + + int next = bdx.pianoPair.Where(p => p.rawRoot == 0xFF).Max(p => (sbyte)p.index) + 1; + Array.Copy(bdx.pianoOrig, bbp.pianoOrig, next); + + int minimum = 3 + (bdx.pianoVoicingStyle / 3); + int spread = bdx.pianoVoicingStyle % 3; // don't know what this does yet + int highestNote = bdx.pianoHighestNote == 0 ? 70 : bdx.pianoHighestNote; + + var dic = new Dictionary(); + + for (int i = 0; i < 32; i++) + { + var bdxpp = bdx.pianoPair[i]; + if (bdxpp.rawRoot == 0xFF) + { + if (bdxpp.index == 0xFF) break; + bbp.pianoPair[i] = bdxpp; + } + else + { + bbp.pianoPair[i] = new IndexRootPair { index = (byte)next, rawRoot = 0xFF }; + + var notes = (from basenote in new ArraySegment(InstrumentData.PianoChords, 4 * bdxpp.index, 4) + let note = basenote == 0 ? 0 : ((basenote + bdxpp.Root) % 12 - highestNote) % 12 + highestNote + orderby note descending + select (byte)note) + .ToArray(); + if (notes[3] == 0 && minimum == 4) + { + notes[3] = (byte)(notes[0] - 12); + } + bbp.pianoOrig[next] = BitConverter.ToInt32(notes, 0); + dic.Add(bdxpp.Short, bbp.pianoPair[i]); + next++; + } + } + + bbp.pianoChordChangesCount = bdx.chordChanges; + for (int i = 0; i < bbp.pianoChordChangesCount; i++) + { + var pair = bdx.chordTimer[i]; + if (pair.value >> 8 != -1) + { + pair.value = dic[pair.value].Short; + } + bbp.pianoChordChangeTable[i] = pair; + } + //bdx.chordTimer.CopyTo(bbp.pianoChordChangeTable, 0); + } + + void DoGuitarStuff() + { + bdx.guitarOrig.CopyTo(bbp.guitarOrig, 0); + for (int i = 0; i < 32; i++) + { + bbp.guitarTimer[i].time = bdx.guitarTimer[i].time; + for (int j = 0; j < 10; j++) + { + var oldpair = bdx.guitarTimer[i].pair[j]; + int index = oldpair.index; + int rawRoot = oldpair.rawRoot; + //({ index * 16 + 15}, { ((rawRoot << 5) | (rawRoot >> 3)) & 0xFF}) + bbp.guitarTimer[i].pair[j] = new IndexRootPair + { + index = (byte)(index * 16 + 15), + rawRoot = (byte)((rawRoot << 5) | (rawRoot >> 3)) + }; + } + } + + // todo: consolidate the above and below functions + + bbp.guitarChordChangesCount = bdx.chordChanges; + for (int i = 0; i < bdx.chordChanges; i++) + { + bbp.guitarChordChangeTable[i].time = bdx.chordTimer[i].time; + var bytes = BitConverter.GetBytes(bdx.chordTimer[i].value); + bytes[0] = (byte)(bytes[0] * 16 + 15); + bytes[1] = (byte)((bytes[1] << 5) | (bytes[1] >> 3)); + bbp.guitarChordChangeTable[i].value = BitConverter.ToInt16(bytes, 0); + } + } + + void DoKaraokeStuff() + { + if (bdx.hasKaraoke == 0) + { + bbp.karaokeTimer[0] = 0x3FFF; + } + else + { + var tmp = GetBDXJPString(bdx.karaokeLyrics, true); + //bbp.karaokeLyrics = tmp; + //bdx.karaokeTimer.CopyTo(bbp.karaokeTimer, 0); + int m = Array.FindIndex(bdx.karaokeTimer, t => (t & 0x3FFF) == 0x3FFF); + + short prev = -1; + var sb = new StringBuilder(); + for (int i = 0; i < 2048; i++) + { + if (tmp[i] == zeroWidthSpace) continue; + short curr = (short)(bdx.karaokeTimer[i] & 0x3FFF); + + if (curr == 0x3FFF) + { + // ended + bbp.karaokeTimer[sb.Length] = curr; + break; + } + + bbp.karaokeTimer[sb.Length] = (short)(curr | 0x8000); + if (curr == prev) + { + bbp.karaokeTimer[sb.Length] |= 0x4000; + } + else + { + prev = curr; + } + sb.Append(tmp[i]); + } + bbp.karaokeLyrics = sb.ToString(); + + } + } + } +} diff --git a/src/File Formats/BDXFormat.cs b/src/File Formats/BDXFormat.cs new file mode 100644 index 0000000..72ec690 --- /dev/null +++ b/src/File Formats/BDXFormat.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static System.Runtime.InteropServices.LayoutKind; +using static System.Runtime.InteropServices.UnmanagedType; + +namespace Degausser +{ + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + class BDXFormat + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 72)] + public byte[] bdxHeader; // not required + + #region GAKHeader + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public JapWordz[] labels; // 32 bytes each + public short definitelyZero1; + public byte timeSignature; + public byte linesPlus6; + public byte definitelyZero2; + public byte otherLineCount; + public byte mainInstrument; + public byte hasKaraoke; + public int someThingy0x20080116; // 0x20080116 + public byte unknown1; + public byte unknown2; + public byte unknown3; + public byte masterVolume; + public int recordID; + public int dateCreated; + public int dateModified; + public int definitelyZero3; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public ChannelInfo[] channelInfo; // 16 bytes each + public short isOfficial; + public short contributorLength; + public JapWordz contributor; // 32 bytes + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)] + public int[] definitelyZero4; + ///////////////////////////////////////////////////// + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct JapWordz + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + byte[] label; + + public override string ToString() => Utils.Language.GetBDXJPString(label); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelInfo + { + public short volume; + public byte instrument; + public PlayType playType; // Standard, Drum, Guitar, Chord + public byte panning; + public byte masterProStar; + public byte cloneID; + public byte amaBegiStar; + public long zero; + } + #endregion + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelNotes + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)] + public byte[] notes; + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelInfo5 + { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ChannelInfo4 + { + public short time; + public short value; + public byte volume; + public byte type; + public byte unknown; + public byte zero; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public ChannelInfo4[] stuff; + } + // guitar chord stuff + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct GuitarStuff + { + public short time; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + public IndexRootPair[] pair; + } + // karaoke stuff + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct KeySignature + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] + public int[] stuff; + } + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public SoundEnvelope[] channelEnvelopes; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public ChannelNotes[] channelNotes; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public TimeValuePair[] tempoTimer; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public ChannelInfo5[] chanInfo5; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public int[] guitarOrig; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public GuitarStuff[] guitarTimer; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)] + public byte[] karaokeLyrics; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)] + public short[] karaokeTimer; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public IndexRootPair[] pianoPair; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public int[] pianoOrig; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 74)] + public long[] totallyUnknown; // Totally unknown + public int chordChanges; // note that you can only have piano or guitar, but not both + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 255)] + public TimeValuePair[] chordTimer; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] + public KeySignature[] keySig; + public byte pianoHighestNote; + public byte pianoVoicingStyle; + public byte pianoUnknown; + public byte pianoAvailability; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 44)] + public byte[] comments; + } +} diff --git a/src/File Formats/Common.cs b/src/File Formats/Common.cs new file mode 100644 index 0000000..a0fd749 --- /dev/null +++ b/src/File Formats/Common.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Degausser +{ + public enum PlayType : byte { Standard, Drum, Guitar, Piano }; + + [DebuggerDisplay("({index}, {rawRoot})")] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IndexRootPair + { + public byte index; + public byte rawRoot; + + enum Accidental { Natural, Sharp, Flat }; + + public byte Root + { + get + { + if (rawRoot == 0xFF) { return rawRoot; } + byte note = InstrumentData.MajorScale[rawRoot & 0xF]; + switch ((Accidental)(rawRoot >> 4)) + { + case Accidental.Sharp: + return ++note; + case Accidental.Flat: + return --note; + case Accidental.Natural: + default: + return note; + } + } + } + + public short Short => (short)((rawRoot << 8) | index); + } + + [DebuggerDisplay("({time}, {value})")] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct TimeValuePair + { + public short time; + public short value; + } + + [DebuggerDisplay("({attack}, {decay}, {sustain}, {release})")] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SoundEnvelope + { + public byte attack; + public byte decay; + public byte sustain; + public byte release; + public byte shape; + public byte hold; + public byte delay; + public byte depth; + public byte speed; + public byte zero; + public byte effectorType; + public byte effectorValue; + } +} diff --git a/src/File Formats/JbMgrFormat.cs b/src/File Formats/JbMgrFormat.cs new file mode 100644 index 0000000..11dba3f --- /dev/null +++ b/src/File Formats/JbMgrFormat.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace Degausser +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + class JbMgrFormat + { + public int Magic; + public short Version; + public short Count; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3700)] + public JbMgrItem[] Items; + + public int BinSize => 1155072; + + public static JbMgrFormat Empty = new JbMgrFormat + { + Magic = 0x4A4B4258, + Version = 0x101, + Count = 3700, + Items = Enumerable.Repeat(JbMgrItem.Empty, 3700).ToArray() + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + [DebuggerDisplay("{ID==-1?\"(empty)\":Title,nq}")] + public struct JbMgrItem + { + [DebuggerDisplay("0x{ID.ToString(\"x8\")}")] + public uint ID; + public uint OtherID; + public JbFlags Flags; + public JbSinger Singer; + public JbIcon Icon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 51)] + public string Title; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 51)] + public string TitleSimple; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)] + public string Author; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)] + public byte[] Scores; // Beginner, Amateur, Pro, Master, Vocals + public short ZeroPadding; + + public static JbMgrItem Empty = new JbMgrItem + { + ID = 0xFFFFFFFF, + Singer = JbSinger.None, + Title = "", + TitleSimple = "", + Author = "", + Scores = new byte[50] + }; + + public enum JbSinger : short + { + None = -1, OriginalMale = -2, OriginalFemale = -3//, Player0 = 0 + } + + public enum JbIcon : short + { + None, Heart, Skull, Music, Warning + } + + [DebuggerDisplay("{IsValid}")] + public struct JbFlags + { + public int flag; + + // Bit0 = 1 + public bool OnSD { get { return this[1]; } set { this[1] = value; } } // best guess + // Bit2 = 0 + public int Parts { get { return (flag >> 3) % 16; } set { flag = (flag & ~120) | (value << 3); } } // Bit3-7 + public bool HasMelody { get { return this[8]; } set { this[8] = value; } } + public bool IsSingable { get { return this[9]; } set { this[9] = value; } } // In the UTAU section + // Bit10 = 0 + public bool HasLyrics { get { return this[11]; } set { this[11] = value; } } + public bool IsReceived { get { return this[12]; } set { this[12] = value; } } + public bool HasFans { get { return this[13]; } set { this[13] = value; } } + public bool HasGuitar { get { return this[14]; } set { this[14] = value; } } + public bool HasDrum { get { return this[15]; } set { this[15] = value; } } + public bool HasPiano { get { return this[16]; } set { this[16] = value; } } + public bool HasInstrX { get { return this[17]; } set { this[17] = value; } } + // Bit18-19 = 0, something to do with DL? + public bool HasVocals { get { return this[20]; } set { this[20] = value; } } // should be same as IsSingEnabled + public bool IsOnlineable { get { return this[21]; } set { this[21] = value; } } // best guess + // Bit22 = 0 + public bool IsHidden { get { return this[23]; } set { this[23] = value; } } + + // Helper functions + public bool this[int i] + { + get { return (flag >> i) % 2 == 1; } + set { flag = (flag & ~(1 << i)) | ((value ? 1 : 0) << i); } + } + public bool IsValid + { + // Check bit 0 is set and bits 2,10,17-19,22 are unset: + get { return (flag & 0x4E0405) == 1 && IsSingable == HasVocals; } + set { flag = (flag & ~0x4E0405) | (value ? 1 : 0); } + } + } + } + } +} diff --git a/src/HackingTests.cs b/src/HackingTests.cs new file mode 100644 index 0000000..8fa2412 --- /dev/null +++ b/src/HackingTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Degausser +{ + public partial class MainWindow : Window + { + void SetTestDirectory() + { + Directory.SetCurrentDirectory(@"C:\Users\Adib\Documents\Daigasso\Test Files"); + } + + void CompareSomeKnownSimilarFiles() + { + Directory.SetCurrentDirectory(@"C:\Users\Adib\Documents\Daigasso\Test Files"); + + JbMgrFormat.JbMgrItem swordmgr, readymgr, harumgr; + + // piano? + var sword1 = new BDXRecord("HEART OF SWORD (L@DX).bdx"); + var sword2 = new BBPRecord("HEART OF SWORD (DXいしょく@L@DX).bbp"); + var sword1bbp = BDX2BBP.Convert(sword1.bdx, out swordmgr); + + // guitar? + var ready1 = new BDXRecord("Sobakasu (Rurouni Kenshin OP1).bdx"); + var ready2 = new BBPRecord("そばかす (DXいしょく@Nintendo).bbp"); + var ready1bbp = BDX2BBP.Convert(ready1.bdx, out readymgr); + + // no karaoke + var haru1 = new BDXRecord("Haru no Umi.bdx"); + var haru2 = new BBPRecord("(無料)春の海 (DXいしょく@Nintendo).bbp"); + var haru1bbp = BDX2BBP.Convert(haru1.bdx, out harumgr); + } + + void LookForSimilarSongs() + { + Func hash = bytes => + { + return bytes.Take(1000).Aggregate(0, (a, b) => a * 23 + b); + }; + var bdxFiles = Directory.EnumerateFiles(@"C:\Users\Adib\Documents\Daigasso\BDX Mega Pack 2.4", "*.bdx", SearchOption.AllDirectories); + var bbpFiles = Directory.EnumerateFiles(@"C:\Users\Adib\Documents\Daigasso\BBP Mega Pack 1.1", "*DXいしょく*.bbp"); + var q = (from b1 in bdxFiles + let h1 = hash(new BDXRecord(b1).bdx.channelNotes[0].notes) + where h1 != 0 + join b2 in bbpFiles on h1 equals hash(new BBPRecord(b2).bbp.channelNotes[0].notes) + select new { h1, b1, b2 }); + + foreach (var x in q) + { + var bdx = new BDXRecord(x.b1).bdx; + var bbp = new BBPRecord(x.b2).bbp; + var s1 = bdx.channelNotes.Take(8).Select(y => hash(y.notes)); + var s2 = bbp.channelNotes.Take(8).Select(y => hash(y.notes)); + + if (s1.Zip(s2, (h1, h2) => h1 == h2).Count(b => b) < 7) continue; + + if (bdx.channelInfo.Any(c => c.playType == PlayType.Guitar)) + { + Debug.Write("[GT] "); + } + else if (bdx.channelInfo.Any(c => c.playType == PlayType.Piano)) + { + Debug.Write("[KB] "); + } + else Debug.Write("[ ] "); + + Debug.WriteLine($"{Path.GetFileName(x.b1)}\t{Path.GetFileName(x.b2)}"); + } + } + + void TestBDXAssertions() + { + var bdxFiles = Directory.EnumerateFiles(@"C:\Users\Adib\Documents\Daigasso\BDX Mega Pack 2.4", "*.bdx", SearchOption.AllDirectories); + foreach (var path in bdxFiles) + { + try + { + if (new FileInfo(path).Length != 32768) throw new Exception($"Wrong filesize {new FileInfo(path).Length}"); + var bdx = new BDXRecord(path).bdx; + if (bdx.dateCreated != 0) + { + var date1 = BitConverter.GetBytes(bdx.dateCreated); + new DateTime(2000 + date1[0], date1[1], date1[2]); + } + if (bdx.dateModified != 0) + { + var date2 = BitConverter.GetBytes(bdx.dateModified); + new DateTime(2000 + date2[0], date2[1], date2[2]); + } + if (bdx.definitelyZero1 != 0) throw new Exception("not zero1"); + if (bdx.definitelyZero2 != 0) throw new Exception("not zero2"); + if (bdx.definitelyZero3 != 0) throw new Exception("not zero3"); + if (bdx.definitelyZero4.Any(x => x != 0)) throw new Exception("not zero4"); + if (bdx.channelInfo.Any(x => x.zero != 0)) throw new Exception("not chanzero"); + if (bdx.chanInfo5.Any(x => x.stuff.Any(y => y.zero != 0))) throw new Exception("not chan5zero2"); + } + catch (Exception e) + { + Debug.WriteLine($"{e.Message}) {path}"); + } + } + } + } +} diff --git a/src/JbManager.cs b/src/JbManager.cs new file mode 100644 index 0000000..80435b4 --- /dev/null +++ b/src/JbManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; + +namespace Degausser +{ + class JbManager + { + const int binSize = 1155072; + + public JbMgrFormat mgr; + public string FilePath { get; } + public string Directory => Path.GetDirectoryName(FilePath); + + public JbManager(string path) + { + FilePath = path; + + if (new FileInfo(path).Length != binSize) + { + throw new InvalidDataException($"{path} has an incorrect filesize!"); + } + + using (var ms = new MemoryStream()) + { + using (var gz = new GZipStream(File.OpenRead(path), CompressionMode.Decompress)) + { + gz.CopyTo(ms); + } + + // Convert file contents into our struct + if (ms.Length != Marshal.SizeOf()) + { + throw new InvalidDataException($"{path} has an incorrect buffer length!"); + } + mgr = ms.ToArray().ToStruct(); + } + } + + public void Save() + { + SaveToFile(FilePath); + SaveToFile(Path.Combine(Path.GetDirectoryName(FilePath), "mgr_.bin")); + } + + void SaveToFile(string path) + { + using (var fo = File.OpenWrite(path)) + { + using (var gz = new GZipStream(fo, CompressionMode.Compress, true)) + { + var buffer = mgr.StructToArray(); + gz.Write(buffer, 0, buffer.Length); + } + var pos = fo.Position; + fo.Position = 8; // Header hack for spoofing the 3DS: + fo.WriteByte(0); // Set compression to 0 instead of 4 + fo.WriteByte(3); // Set OS to UNIX instead of WINDOWS + fo.Position = binSize - 4; + fo.Write(BitConverter.GetBytes(pos), 0, 4); // Write compressed size at end + } + } + + public JbMgrFormat.JbMgrItem this[int index] + { + get { return mgr.Items[index]; } + set { mgr.Items[index] = value; } + } + + public bool Export(int i) + { + var item = this[i]; + var packpath = Path.Combine(Directory, $"gak\\{item.ID:x8}\\pack"); + if (!File.Exists(packpath)) return false; + + try + { + item.Scores = new byte[50]; + item.Singer = JbMgrFormat.JbMgrItem.JbSinger.None; + item.Icon = JbMgrFormat.JbMgrItem.JbIcon.None; + item.Flags.flag &= 0x7DFFFF; + + var itemData = item.StructToArray(); + var packData = File.ReadAllBytes(packpath); + var titleWithoutNewlines = item.Title.Replace("\n", ""); + var bbpPath = Path.Combine("EXPORTFOLDER", $"{titleWithoutNewlines} ({item.Author}).bbp"); + + using (var fo = File.Open(bbpPath, FileMode.Create)) + { + fo.Write(itemData, 0, itemData.Length); + fo.Write(packData, 0, packData.Length); + } + return true; + } + catch (IOException) + { + return false; + } + + + } + } +} diff --git a/src/Karaoke.cs b/src/Karaoke.cs new file mode 100644 index 0000000..20e71a5 --- /dev/null +++ b/src/Karaoke.cs @@ -0,0 +1,73 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Degausser +{ + class Karaoke + { + const int MaxSyllableLength = 96; + readonly List timer; + + public string Lyrics { get; } + public int PositionStart { get; private set; } + public int PositionCount { get; private set; } + public double PositionFraction { get; private set; } + public bool IsEnabled { get; } + + int maxTicks; + + public Karaoke() + { + Lyrics = "(no karaoke)"; + IsEnabled = false; + } + + public Karaoke(BBPFormat bbp) + { + if (IsEnabled = bbp.karaokeTimer[0] != 0x3FFF) + { + Lyrics = bbp.karaokeLyrics; + timer = bbp.karaokeTimer.Select(t => t & 0x3FFF).ToList(); + maxTicks = (bbp.linesPlus6 - 6) * 48; + } + else + { + Lyrics = "(no karaoke)"; + } + } + + public int Position + { + set + { + int left = timer.BinarySearch(0, Lyrics.Length, value, null); + if (left < 0) left = ~left - 1; + + int right = left + 1; + while (left > 0 && timer[left - 1] == timer[left]) left--; + while (left >= 0 && right < timer.Count && timer[right] == timer[left]) right++; + + int timeLower = left < 0 ? 0 : timer[left]; + int timeUpper = right >= timer.Count ? timer[left] + MaxSyllableLength : timer[right]; + int timeDiff = Math.Min(maxTicks - timeLower, Math.Min(timeUpper - timeLower, MaxSyllableLength)); + double frac = (value - timeLower) * 1.0 / timeDiff; + + if (left < 0 || value >= timeLower + MaxSyllableLength) + { + left = right; + frac = 0; + } + + PositionStart = left; + PositionCount = right - left; + PositionFraction = frac; + } + } + } +} diff --git a/src/MainWindow.xaml b/src/MainWindow.xaml new file mode 100644 index 0000000..5fe9581 --- /dev/null +++ b/src/MainWindow.xaml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Karaoke lyrics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MainWindow.xaml.cs b/src/MainWindow.xaml.cs new file mode 100644 index 0000000..40e1a8c --- /dev/null +++ b/src/MainWindow.xaml.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.IO; +using System.Globalization; +using Degausser.Utils; +using Microsoft.Win32; + +namespace Degausser +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + Karaoke karaoke = new Karaoke(); + public static RoutedCommand OpenCommand = new RoutedCommand(); + public static RoutedCommand ImportCommand = new RoutedCommand(); + public static RoutedCommand ExportCommand = new RoutedCommand(); + public static RoutedCommand RefreshCommand = new RoutedCommand(); + public static RoutedCommand PlayPauseCommand = new RoutedCommand(); + public OpenFileDialog openFileDialog = new OpenFileDialog { Filter = "Jb Manager File|mgr.bin" }; + + JbManager jbManager; + + public MainWindow() + { + InitializeComponent(); + + //SetTestDirectory(); + //CompareSomeKnownSimilarFiles(); + //LookForSimilarSongs(); + //TestBDXAssertions(); + + + + //Directory.SetCurrentDirectory(@"C:\Users\Adib\Documents\Daigasso\BDX Mega Pack 2.4"); + + Logging.OnMessage += (s, e) => Dispatcher.BeginInvoke(new Action(() => + { + txtLogView.AppendText($"{e}\n"); + txtLogView.ScrollToEnd(); + })); + Logging.Log($"Current directory: {Directory.GetCurrentDirectory()}"); + + Refresh(this, null); // populate library + + lstMediaLibrary.MouseDoubleClick += (s, e) => + { + PlayItem((e.OriginalSource as FrameworkElement)?.DataContext as BBPRecord); + }; + + lstSaveEditor.MouseDoubleClick += (s, e) => + { + PlayItem((e.OriginalSource as FrameworkElement)?.DataContext as BBPRecord); + }; + + lstMediaLibrary.KeyDown += (s, e) => + { + if (e.Key == Key.Enter) + { + PlayItem((s as SortableListView)?.SelectedItem as BBPRecord); + } + }; + + MidiPlayer.Instance.PropertyChanged += (s, e) => + { + if (e.PropertyName == "Position") + { + Dispatcher.BeginInvoke(new Action(() => UpdateKaraoke())); + } + }; + + lstSaveEditor.KeyDown += (s, e) => + { + if (e.Key == Key.Delete) + { + Delete(); + } + }; + + } + + void PlayItem(BBPRecord record) + { + if (record == null) return; + karaoke = new Karaoke(record.bbp); + karaokeEffect.PositionStart = 0; + karaokeEffect.PositionCount = 0; + karaokeViewer.ScrollToTop(); + karaokeBlock.Text = karaoke.Lyrics; + MidiPlayer.Instance.Play(record.GetMidiData()); + } + + void UpdateKaraoke() + { + if (karaoke.IsEnabled) + { + karaoke.Position = MidiPlayer.Instance.Position; + karaokeEffect.PositionStart = karaoke.PositionStart; + karaokeEffect.PositionCount = karaoke.PositionCount; + //karaokeFraction.Offset = karaoke.PositionFraction; // causes lag?? + } + } + + void Open(string path) + { + jbManager = new JbManager(path); + Title = $"Degausser - {path}"; + + Logging.Log($"Opened {path}"); + + var lst = new List(); + for (int i = 0; i < jbManager.mgr.Items.Length; i++) + { + var item = jbManager.mgr.Items[i]; + if (item.ID != 0xFFFFFFFF && item.Flags.OnSD) + { + var idPath = Path.Combine(jbManager.Directory, $"gak\\{item.ID:x8}\\pack"); + lst.Add(new BBPRecord(item, idPath, i)); + } + } + lstSaveEditor.ItemsSource = lst; + // add another overload of BBPRecord to take stuff + } + + void Open(object s, RoutedEventArgs e) + { + if (openFileDialog.ShowDialog() == true) + { + Open(Path.GetFullPath(openFileDialog.FileName)); + } + } + + void Import(object s, RoutedEventArgs e) + { + if (jbManager == null) + { + Logging.Log("Please open a mgr.bin file first before importing"); + } + else if (lstMediaLibrary.SelectedItems.Count == 0) + { + Logging.Log("Please select some items to import"); + } + else + { + var mgr = jbManager.mgr; + var set = new HashSet(mgr.Items.Select(x => x.ID)); + var items = lstMediaLibrary.SelectedItems.Cast().Select(record => + { + try + { + int n = Enumerable.Range(0, 3700).First(i => mgr.Items[i].ID == 0xFFFFFFFF); + + var item = record.mgrItem; + byte[] packdata; + if ((item.ID >> 16) == 0x8000) + { + var newID = Enumerable.Range(0, 65536).Select(i => (uint)(i + 0x80000000)).First(i => !set.Contains(i)); + item.ID = newID; + record.bbp.titleID = (int)newID; + } + else if (set.Contains(item.ID)) + { + return JbMgrFormat.JbMgrItem.Empty; + } + + packdata = record.bbp.StructToArray(); + var idPath = Path.Combine(jbManager.Directory, $"gak\\{item.ID:x8}\\"); + Directory.CreateDirectory(idPath); + record.SaveAsPackFile(Path.Combine(idPath, "pack")); + mgr.Items[n] = item; + set.Add(item.ID); + + return item; + } + catch (IOException) + { + return JbMgrFormat.JbMgrItem.Empty; + } + }) + .ToList(); + + var count = items.Count(item => item.ID != 0xFFFFFFFF); + + jbManager.Save(); + Open(jbManager.FilePath); + //mgr.Write(mgrpath); + //mgr.Write(Path.Combine(Path.GetDirectoryName(mgrpath), "mgr_.bin")); + //OpenMgrBin(frm, mgrpath); + if (count == lstMediaLibrary.SelectedItems.Count) + { + Logging.Log($"Successfully imported all {count} songs"); + } + else + { + Logging.Log($"Only managed to import {count} of {lstMediaLibrary.SelectedItems.Count} songs. The following files had errors:"); + foreach (var pair in lstMediaLibrary.SelectedItems.Cast().Zip(items, Tuple.Create)) + { + if (pair.Item2.ID == 0xFFFFFFFF) + { + Logging.Log($"- {pair.Item1.FullPath}"); + } + } + } + } + } + + void Export(object s, RoutedEventArgs e) + { + if (jbManager == null) + { + Logging.Log("Please open a mgr.bin file first before exporting"); + } + else if (lstSaveEditor.SelectedItems.Count == 0) + { + Logging.Log("Please select some items to export"); + } + else + { + Directory.CreateDirectory("Ripped"); + var errors = lstSaveEditor.SelectedItems.Cast().Where(record => + { + try + { + var item = record.mgrItem; + string bbpPath = Path.Combine("Ripped", $"{item.Title.Replace("\n", "")} ({item.Author}).bbp"); + record.SaveAsBBPFile(bbpPath); + ((List)lstMediaLibrary.ItemsSource).Add(new BBPRecord(bbpPath)); + return false; + } + catch (IOException) + { + return true; + } + }) + .ToList(); + lstMediaLibrary.Items.Refresh(); + Logging.Log($"Successfully exported {lstSaveEditor.SelectedItems.Count - errors.Count} of {lstSaveEditor.SelectedItems.Count}"); + } + } + + void Refresh(object s, RoutedEventArgs e) + { + lstMediaLibrary.ItemsSource = null; + lstMediaLibrary.IsEnabled = false; + var paths = (from path in Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.*", SearchOption.AllDirectories) + let ext = path.Substring(path.Length - 4).ToLower() + where ext == ".bdx" || ext == ".bbp" + select path).ToList(); + lstMediaLibrary.Items.Add(new { Filename = $"Populating list... please wait (approximately {paths.Count} items)" }); + + Task.Factory.StartNew(() => (from path in paths select new BBPRecord(path)).SkipExceptions(true).ToList()) + .ContinueWith(t => + { + lstMediaLibrary.Items.Clear(); + lstMediaLibrary.ItemsSource = t.Result; + lstMediaLibrary.IsEnabled = true; + Logging.Log($"Media library population complete: added {t.Result.Count} items to list"); + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + + void PlayPause(object s, RoutedEventArgs e) + { + if (MidiPlayer.Instance.State == MidiPlayer.MidiPlayerState.Playing) + { + MidiPlayer.Instance.State = MidiPlayer.MidiPlayerState.Paused; + } + else + { + MidiPlayer.Instance.Play(); + } + } + + void Delete() + { + if (jbManager == null) + { + return; + } + else if (lstSaveEditor.SelectedItems.Count == 0) + { + return; + } + + var count = lstSaveEditor.SelectedItems.Cast().Count(record => + { + try + { + var idPath = Path.Combine(jbManager.Directory, $"gak\\{record.mgrItem.ID:x8}"); + Directory.Delete(idPath, true); + jbManager.mgr.Items[record.Slot] = JbMgrFormat.JbMgrItem.Empty; + return true; + } + catch (IOException) + { + Logging.Log($"Failed to delete Slot {record.Slot}: {record.Title}"); + return false; + } + }); + jbManager.Save(); + + Logging.Log($"Deleted {count} of {lstSaveEditor.SelectedItems.Count} records"); + Open(jbManager.FilePath); + } + } +} diff --git a/src/MidiPlayer.cs b/src/MidiPlayer.cs new file mode 100644 index 0000000..8b9b861 --- /dev/null +++ b/src/MidiPlayer.cs @@ -0,0 +1,323 @@ +using System; +using System.Threading; +using System.Runtime.InteropServices; +using System.ComponentModel; + +namespace Degausser +{ + public class MidiPlayer : INotifyPropertyChanged + { + [DllImport("winmm.dll")] + static extern uint midiOutOpen(out int lphMidiOut, int uDeviceID, int dwCallback, int dwInstance, uint dwFlags); + [DllImport("winmm.dll")] + static extern uint midiOutShortMsg(int hMidiOut, int dwMsg); + [DllImport("winmm.dll")] + static extern uint midiOutClose(int hMidiOut); + [DllImport("winmm.dll")] + static extern int midiOutSetVolume(int uDeviceID, int dwVolume); + [DllImport("winmm.dll")] + static extern int midiOutGetVolume(int uDeviceID, out int lpdwVolume); + + const int MAX_CHANNELS = 10; + const int MAX_TICKS = 7200; + + public enum MidiPlayerState + { + Paused, Playing + }; + + enum MessageType + { + NoteOff = 0x80, + NoteOn = 0x90, + Polyphonic = 0xA0, + ControlChange = 0xB0, + ProgramChange = 0xC0, + Aftertouch = 0xD0, + PitchWheel = 0xE0 + }; + const byte StopAllNotes = 0x7B; + + static int MidiOut; + public static MidiPlayer Instance { get; private set; } + + MidiData midiData = new MidiData(new short[0]); + Thread thread; + + public abstract class Chord + { + public virtual byte[] Notes { get; protected set; } + } + + struct MidiMessage + { + int message; + + public MidiMessage(MessageType msgType, byte channel, byte note, byte volume) + { + message = (volume << 16) + (note << 8) + (int)msgType + channel; + } + + public void Execute() => midiOutShortMsg(MidiOut, message); + } + + public class MidiChannel + { + byte lastNote; + Chord lastChord; + MidiMessage[,] message = new MidiMessage[MAX_TICKS, 12]; + int[] msgCount = new int[MAX_TICKS]; + byte channel; + bool isActive = true; + + public MidiChannel(byte channel) + { + this.channel = channel == 9 ? (byte)10 : channel; // channel 9 reserved for percussion + } + + void AddMessage(int position, MidiMessage message) + { + this.message[position, msgCount[position]++] = message; + } + + public void AddNote(int position, byte note, byte volume) + { + if (note == 0) return; + lastNote = note; + AddMessage(position, new MidiMessage(MessageType.NoteOn, channel, note, volume)); + } + + void ReleaseNote(int position, byte note) + { + if (note == 0) return; + AddMessage(position, new MidiMessage(MessageType.NoteOff, channel, note, 0)); + } + + public void ReleaseNote(int position) + { + ReleaseNote(position, lastNote); + lastNote = 0; + } + + public void AddChord(int position, Chord chord, byte volume) + { + lastChord = chord; + foreach (byte note in chord.Notes) + { + AddNote(position, note, volume); + } + } + + public void ReleaseChord(int position) + { + if (lastChord == null) return; + foreach (byte note in lastChord.Notes) + { + ReleaseNote(position, note); + } + } + + public void AddDrum(int position, byte drum, byte volume) + { + if (drum == 0) return; + AddMessage(position, new MidiMessage(MessageType.NoteOn, 9, drum, volume)); + } + + public void ExecuteMessages(int position) + { + for (int i = 0; i < msgCount[position]; i++) + { + message[position, i].Execute(); + } + } + + public byte InstrumentMidi { get; set; } + + public string InstrumentName { get; set; } + + public void Silent() + { + new MidiMessage(MessageType.ControlChange, channel, StopAllNotes, 0).Execute(); + } + + public void Initialize() + { + new MidiMessage(MessageType.ProgramChange, channel, InstrumentMidi, 0).Execute(); + } + + public bool IsActive + { + get + { + return isActive; + } + set + { + isActive = value; + if (!isActive) Silent(); + } + } + } + + public class MidiData + { + public MidiData(short[] tempo) + { + Tempo = tempo; + for (int i = 0; i < Channels.Length; i++) + { + Channels[i] = new MidiChannel((byte)i); + } + } + + public MidiChannel this[int index] => Channels[index]; + public short[] Tempo { get; } + public int Length => Tempo.Length; + public MidiChannel[] Channels { get; } = new MidiChannel[MAX_CHANNELS]; + + } + + // Opens a MIDI output device for playback + public MidiPlayer() + { + Instance = this; + midiOutOpen(out MidiOut, 0, 0, 0, 0); + for (int i = 0; i < MAX_CHANNELS; i++) + { + midiData.Channels[i].InstrumentName = $"Instrument {i}"; + } + } + + // Closes the MIDI output device + public void Close() + { + midiOutClose(MidiOut); + } + + // Gets and sets the MIDI volume + public ushort Volume + { + get + { + int volume; + midiOutGetVolume(MidiOut, out volume); + return (ushort)(volume & 0xFFFF); + } + set + { + ushort volume = value; + midiOutSetVolume(MidiOut, (volume << 16) | volume); + } + } + + public double TempoModifier { get; set; } + + public MidiPlayerState State { get; set; } + + public void SilentAll() + { + for (int i = 0; i < 8; i++) + { + midiData.Channels[i].Silent(); + } + } + + int position; + public int Position + { + get + { + return position; + } + set + { + position = value; + SilentAll(); + } + } + + public int Length + { + get + { + if (midiData == null) return 0; + return midiData.Length; + } + } + + public void Play(MidiData midiData) + { + State = MidiPlayerState.Paused; + while (thread != null && thread.IsAlive) ; + this.midiData = midiData; + Position = 0; + TempoModifier = 0; + Play(); + NotifyPropertyChanged(nameof(Length)); + NotifyPropertyChanged(nameof(Channels)); + NotifyPropertyChanged(nameof(TempoModifier)); + } + + public void Play() + { + switch (State) + { + case MidiPlayerState.Playing: + return; + case MidiPlayerState.Paused: + State = MidiPlayerState.Playing; + thread = new Thread(MainLoop); + thread.IsBackground = true; + thread.Priority = ThreadPriority.AboveNormal; + thread.Start(); + break; + } + } + + public MidiChannel[] Channels => midiData.Channels; + + // The main loop that runs the MIDI playback + void MainLoop() + { + foreach (var c in midiData.Channels) + { + c.Initialize(); + } + + long nextTick = 0; + while (State == MidiPlayerState.Playing && position < Length) + { + long currentTick = DateTime.Now.Ticks; + if (currentTick >= nextTick) + { + double actualTempoModifier = Math.Pow(2, TempoModifier); + nextTick = currentTick + TimeSpan.TicksPerMinute / (long)(midiData.Tempo[position] * 12 * actualTempoModifier); + foreach (var c in midiData.Channels) + { + if (c.IsActive) + { + c.ExecuteMessages(position); + } + } + NotifyPropertyChanged(nameof(Position)); + position++; + } + Thread.Sleep(1); + } + SilentAll(); + State = MidiPlayerState.Paused; + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void NotifyPropertyChanged(String info) + { + if (PropertyChanged != null) + { + PropertyChanged(this, new PropertyChangedEventArgs(info)); + } + } + + + } +} \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d18886a --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Degausser")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Degausser")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Properties/Resources.Designer.cs b/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000..33e434f --- /dev/null +++ b/src/Properties/Resources.Designer.cs @@ -0,0 +1,102 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Degausser.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Degausser.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] GuitarChords { + get { + object obj = ResourceManager.GetObject("GuitarChords", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> + ///<Instruments> + /// <Instrument ID="0" Name="(Blank)" Type="NUL" Midi="255"/> + /// + /// <!-- SCL Instruments --> + /// <Instrument ID="1" Name="Piano" Type="SCL" Midi="1"/> + /// <Instrument ID="2" Name="Electric Piano" Type="SCL" Midi="3"/> + /// <Instrument ID="3" Name="Rock Organ" Type="SCL" Midi="18"/> + /// <Instrument ID="4" Name="Synth Lead" Type="SCL" Midi="81"/> + /// <Instrument ID="5" Name="Synth Bell" Type="SCL" Midi="88"/> + /// <Instrument ID="6" Name="Pipe Organ" Type="SCL" M [rest of string was truncated]";. + /// + internal static string Instruments { + get { + return ResourceManager.GetString("Instruments", resourceCulture); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] PianoChords { + get { + object obj = ResourceManager.GetObject("PianoChords", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/src/Properties/Resources.resx b/src/Properties/Resources.resx new file mode 100644 index 0000000..34e6509 --- /dev/null +++ b/src/Properties/Resources.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\GuitarChords.tbl;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\Instruments.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + + ..\Resources\PianoChords.tbl;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Properties/Settings.Designer.cs b/src/Properties/Settings.Designer.cs new file mode 100644 index 0000000..01262f9 --- /dev/null +++ b/src/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Degausser.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/src/Properties/Settings.settings b/src/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/src/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Resources/GuitarChords.tbl b/src/Resources/GuitarChords.tbl new file mode 100644 index 0000000..fdd3685 Binary files /dev/null and b/src/Resources/GuitarChords.tbl differ diff --git a/src/Resources/Instruments.xml b/src/Resources/Instruments.xml new file mode 100644 index 0000000..3d35867 --- /dev/null +++ b/src/Resources/Instruments.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Resources/PianoChords.tbl b/src/Resources/PianoChords.tbl new file mode 100644 index 0000000..6533c21 Binary files /dev/null and b/src/Resources/PianoChords.tbl differ diff --git a/src/SortableListView.xaml b/src/SortableListView.xaml new file mode 100644 index 0000000..456f51e --- /dev/null +++ b/src/SortableListView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/src/SortableListView.xaml.cs b/src/SortableListView.xaml.cs new file mode 100644 index 0000000..645738e --- /dev/null +++ b/src/SortableListView.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.ComponentModel; + +namespace Degausser +{ + /// + /// Interaction logic for SortableListView.xaml + /// + public partial class SortableListView : ListView + { + public SortableListView() + { + InitializeComponent(); + + GridViewColumnHeader prevHeader = null; + var direction = ListSortDirection.Ascending; + + AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler((s, e) => + { + var header = e.OriginalSource as GridViewColumnHeader; + if (header == null || header.Role == GridViewColumnHeaderRole.Padding) return; + + if (header == prevHeader && direction == ListSortDirection.Ascending) + { + direction = ListSortDirection.Descending; + } + else + { + direction = ListSortDirection.Ascending; + } + + var dataView = CollectionViewSource.GetDefaultView(ItemsSource); + dataView.SortDescriptions.Clear(); + var sortBy = ((Binding)header.Column.DisplayMemberBinding)?.Path?.Path; + if (sortBy == null) return; + dataView.SortDescriptions.Add(new SortDescription(sortBy, direction)); + dataView.Refresh(); + + header.Column.HeaderTemplate = (DataTemplate)FindResource("HeaderTemplate" + direction); + + // Remove arrow from previously sorted header + if (prevHeader != null && prevHeader != header) + { + prevHeader.Column.HeaderTemplate = null; + } + prevHeader = header; + })); + } + } +} diff --git a/src/Utils/GZip.cs b/src/Utils/GZip.cs new file mode 100644 index 0000000..d4e08d2 --- /dev/null +++ b/src/Utils/GZip.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.IO.Compression; + +namespace Degausser.Utils +{ + static class GZip + { + public static byte[] Compress(byte[] data) + { + using (var compressedStream = new MemoryStream()) + { + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress, true)) + { + zipStream.Write(data, 0, data.Length); + } + var arr = compressedStream.ToArray(); + arr[8] = 0; // these two bytes are not strictly required, but + arr[9] = 3; // will make it look like the original pack file + return arr; + } + } + + public static byte[] Decompress(byte[] buffer, int index, int count) + { + using (var zipStream = new GZipStream(new MemoryStream(buffer, index, count), CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + zipStream.CopyTo(resultStream); + return resultStream.ToArray(); + } + } + } +} diff --git a/src/Utils/InstrumentData.cs b/src/Utils/InstrumentData.cs new file mode 100644 index 0000000..4fb758b --- /dev/null +++ b/src/Utils/InstrumentData.cs @@ -0,0 +1,72 @@ +using System; +using System.Text; +using System.Collections.Generic; +using Degausser.Properties; +using System.IO; +using System.Runtime.InteropServices; +using System.Linq; +using System.Xml.Linq; + +namespace Degausser +{ + static class InstrumentData + { + public class Instrument + { + public string Name; + public byte Midi; + public List DrumNotes; + } + + public static Dictionary Instruments = new Dictionary(); + + static InstrumentData() + { + ParseInstrumentXml(); + } + + static void ParseInstrumentXml() + { + var root = XDocument.Parse(Resources.Instruments).Root; + foreach (var x in root.Elements("Instrument")) + { + int id = int.Parse(x.Attribute("ID").Value); + var midi = x.Attribute("Midi").Value; + var name = x.Attribute("Name").Value; + switch (x.Attribute("Type").Value) + { + case "NUL": + case "SCL": + Instruments[id] = new Instrument + { + Name = name, + Midi = byte.Parse(midi) + }; + break; + case "DRM": + Instruments[id] = new Instrument + { + Name = name, + DrumNotes = midi.Split(',').Select(byte.Parse).ToList() + }; + break; + } + } + + foreach (var x in root.Elements("InstrumentRange")) + { + var id = x.Attribute("ID").Value.Split('-').Select(int.Parse).ToList(); + var offset = int.Parse(x.Attribute("Offset").Value); + for (int i = id[0]; i <= id[1]; i++) + { + Instruments[i] = Instruments[i + offset]; + } + } + } + + public static readonly byte[] PianoChords = Resources.PianoChords; + public static readonly byte[] GuitarChords = Resources.GuitarChords; + public static readonly byte[] MajorScale = { 0, 2, 4, 5, 7, 9, 11 }; // CDEFGAB + public static readonly int[] GuitarTuning = { 64, 59, 55, 50, 45, 40 }; // EBGDAE + } +} diff --git a/src/Utils/InteropExtensions.cs b/src/Utils/InteropExtensions.cs new file mode 100644 index 0000000..25a1914 --- /dev/null +++ b/src/Utils/InteropExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.InteropServices; + +namespace Degausser +{ + static class InteropExtensions + { + public unsafe static T ToStruct(this byte[] buffer) + { + fixed (byte* pBuffer = buffer) + { + return (T)Marshal.PtrToStructure((IntPtr)pBuffer, typeof(T)); + } + } + + public unsafe static byte[] StructToArray(this T item) + { + var buffer = new byte[Marshal.SizeOf(typeof(T))]; + fixed (byte* pBuffer = buffer) + { + Marshal.StructureToPtr(item, (IntPtr)pBuffer, false); + } + return buffer; + } + + public static byte HiByte(this short s) => (byte)(s >> 8); + public static byte LoByte(this short s) => (byte)(s & 0xFF); + } +} diff --git a/src/Utils/Language.cs b/src/Utils/Language.cs new file mode 100644 index 0000000..094c564 --- /dev/null +++ b/src/Utils/Language.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Degausser.Utils +{ + static class Language + { + const string SingleByteCode = "      をぁぃぅぇぉゃゅょっ~あいうえおかきくけこさしすせそ 。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚たちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわん★©"; + const string MultiByteCode = "ガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポヴ\0\0\0\0\0\0がぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽ\0\0\0\0\0\0\0×÷≠→↓←↑※〒♭♪±℃○●◎△▲▽▼□■◇◆☆★°∞∴…™®♂♀αβγπΣ√ゞ制作投稿大中小"; + const string KanaToRomaji = "-a.a..-i.i..-u.u..-e.e..-o.o..ka.ga.ki.gi.ku.gu.ke.ge.ko.go.sa.za.shiji.su.zu.se.ze.so.zo.ta.da.chiji.~..tsudzute.de.to.do.na.ni.nu.ne.no.ha.ba.pa.hi.bi.pi.fu.bu.pu.he.be.pe.ho.bo.po.ma.mi.mu.me.mo.-yaya.-yuyu.-yoyo.ra.ri.ru.re.ro.-wawa.wi.we.wo.n..vu."; + public const char zeroWidthSpace = (char)0x200b; + + public static string GetBDXJPString(IEnumerable bytes, bool lyricsMode = false) + { + var result = new StringBuilder(); + bool multibyte = false; + foreach (var b in bytes) + { + if (multibyte) + { + result.Append(MultiByteCode[b]); + multibyte = false; + } + else if (b == 0) + { + if (lyricsMode) + { + result.Append('\n'); + } + else + { + break; + } + } + else if (b < 0x80) + { + result.Append((char)b); + } + else if (b == 0x80) + { + multibyte = true; + if (lyricsMode) + { + result.Append(zeroWidthSpace); + } + } + else if (b == 0xde || b == 0xdf) + { + if (result.Length > 0) + { + result[result.Length - 1] += (char)(b - 0xdd); + } + if (lyricsMode) + { + result.Append(zeroWidthSpace); + } + } + else // (b > 0x80) + { + result.Append(SingleByteCode[b - 0x80]); + } + } + return result.ToString(); + } + + public static string Jap2Eng(string JapString) + { + var EngString = new StringBuilder(); + foreach (char IteratedCharacter in JapString) + { + char c = IteratedCharacter; + bool katakana = false; + if (c >= 'ァ' && c <= 'ヴ') + { + katakana = true; + c = (char)(c - 'ァ' + 'ぁ'); + } + else if (c >= 'ぁ' && c <= 'ゔ') + { + for (int i = 0; i < 3; i++) + { + char newc = KanaToRomaji[3 * (c - 'ぁ') + i]; + switch (newc) + { + case '.': + break; + case '-': + if (EngString.Length > 0) + { + EngString.Length--; + } + break; + case '~': + EngString.Append(c); + break; + default: + EngString.Append(katakana ? char.ToUpper(newc) : newc); + break; + } + } + } + else + { + EngString.Append(c); + } + } + for (int i = 0; i < EngString.Length; i++) + { + switch (EngString[i]) + { + case 'ー': + if (i > 0) + { + EngString[i] = EngString[i - 1]; + } + break; + case 'っ': + case 'ッ': + if (i < EngString.Length - 1) + { + EngString[i] = EngString[i + 1]; + } + break; + } + } + return EngString.ToString(); + } + } +} diff --git a/src/Utils/Logging.cs b/src/Utils/Logging.cs new file mode 100644 index 0000000..4c7b23b --- /dev/null +++ b/src/Utils/Logging.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace Degausser.Utils +{ + static class Logging + { + public static event EventHandler OnMessage; + + public static void Log(Exception e) + { + Log(e.Message); + } + + public static void Log(string message) + { + var timeStampedMessage = $"[{DateTime.Now:HH:mm:ss}] {message}"; + Trace.WriteLine(timeStampedMessage); + if (OnMessage != null) + { + OnMessage(null, timeStampedMessage); + } + } + + public static void AssertEqual(T item1, T item2, string message) + { + Debug.Assert(item1.Equals(item2), message); + if (!item1.Equals(item2)) + { + throw new InvalidOperationException(message); + } + } + + public static IEnumerable SkipExceptions(this IEnumerable source, bool log) + { + using (var enumerator = source.GetEnumerator()) + { + bool next = true; + while (next) + { + try + { + next = enumerator.MoveNext(); + } + catch (Exception e) + { + if (log) Log(e); + continue; + } + if (next) yield return enumerator.Current; + } + } + } + } +}