Last active
November 8, 2016 03:01
-
-
Save JLChnToZ/d669f1e22e7411b37bd8 to your computer and use it in GitHub Desktop.
A Generic BM98 Format Loader and Runner for XNA. Buggy stuff and not accurate. See https://github.com/JLChnToZ/Bemusilization for newer implementations.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Globalization; | |
using System.Text; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Audio; | |
using Microsoft.Xna.Framework.Graphics; | |
namespace Be_Music_Player { | |
/// <summary> | |
/// A Generic BM98 Format Loader and Runner for XNA | |
/// </summary> | |
public class BMSLoader { | |
private class timeLine { | |
private List<keyFrame> keyFrames; | |
private bool isTidy; | |
private TimeSpan currentTime; | |
private int currentIndex, currentIndex2 = 0; | |
private struct keyFrame { | |
public TimeSpan position; | |
public int value; | |
} | |
public timeLine() { | |
keyFrames = new List<keyFrame>(); | |
} | |
public void pushKeyFrame(TimeSpan position, int value) { | |
keyFrame KF = new keyFrame(); | |
KF.position = position; | |
KF.value = value; | |
keyFrames.Add(KF); | |
isTidy = false; | |
} | |
public void tidy() { | |
keyFrames.Sort(delegate(keyFrame LHS, keyFrame RHS) { | |
return LHS.position.CompareTo(RHS.position); | |
}); | |
currentIndex = 0; | |
currentIndex2 = 0; | |
isTidy = true; | |
} | |
public float getFrame(TimeSpan position, TimeSpan maxDelay) { | |
List<float> ret = new List<float>(); | |
if(!isTidy) | |
tidy(); | |
if(currentIndex + 2 < keyFrames.Count && keyFrames[currentIndex + 1].position < position) | |
currentIndex = 0; | |
for(int i = currentIndex; i < keyFrames.Count; i++) { | |
if(keyFrames[i].position >= position) | |
return (float)((keyFrames[i].position - position).TotalMilliseconds / maxDelay.TotalMilliseconds); | |
} | |
return -1; | |
} | |
public int getValue(TimeSpan position) { | |
keyFrame? lastFrame = null, nextFrame = null; | |
if(!isTidy) | |
tidy(); | |
if(position < currentTime) | |
currentIndex2 = 0; | |
currentTime = position; | |
for(int i = currentIndex2; i < keyFrames.Count; i++) { | |
if(keyFrames[i].position <= currentTime) { | |
lastFrame = keyFrames[i]; | |
currentIndex2 = i; | |
} | |
if(lastFrame.HasValue && !nextFrame.HasValue && keyFrames[i].position >= currentTime) { | |
nextFrame = keyFrames[i]; | |
break; | |
} | |
} | |
if(!lastFrame.HasValue) | |
return 0; | |
if(!nextFrame.HasValue) | |
nextFrame = lastFrame.Value; | |
return ((position - lastFrame.Value.position) > (nextFrame.Value.position - position) ? lastFrame : nextFrame).Value.value; | |
} | |
} | |
/// <summary> | |
/// Player Count | |
/// </summary> | |
public int playerCount { get; private set; } | |
/// <summary> | |
/// Genre | |
/// </summary> | |
public string genre { get; private set; } | |
/// <summary> | |
/// Title | |
/// </summary> | |
public string title { get; private set; } | |
/// <summary> | |
/// Artist | |
/// </summary> | |
public string artist { get; private set; } | |
/// <summary> | |
/// BPM(Beat Per Minite) at the top of music | |
/// </summary> | |
public float BPM { get; private set; } | |
/// <summary> | |
/// Game Level for player | |
/// </summary> | |
public int playLevel { get; private set; } | |
/// <summary> | |
/// Judgement level | |
/// </summary> | |
public int rank { get; private set; } | |
/// <summary> | |
/// Relative volume control | |
/// </summary> | |
public float volumePerc { get; private set; } | |
/// <summary> | |
/// Graphic Device | |
/// </summary> | |
public GraphicsDevice GD { get; set; } | |
/// <summary> | |
/// Current Position | |
/// </summary> | |
public float position { get; private set; } | |
/// <summary> | |
/// BPM, might be changed during playing | |
/// </summary> | |
public float currentBPM { get; private set; } | |
/// <summary> | |
/// How much sound is playing now? | |
/// </summary> | |
public int polyPhony { | |
get { | |
return SoundInstanceBank.Count; | |
} | |
} | |
private Dictionary<int, Texture2D> TextureBank; | |
private Dictionary<int, SoundEffect> SoundBank; | |
private List<SoundEffectInstance> SoundInstanceBank; | |
private Dictionary<int, BGA> BGABank; | |
private List<trackObject> TrackObjects; | |
private Dictionary<int, timeLine> timeLines; | |
private string BasePath; | |
private TimeSpan position2; | |
private bool isPlaying; | |
private int index; | |
private int currentImage; | |
private struct trackObject { | |
public float Position; | |
public int Channel; | |
public int ID; | |
} | |
private struct BGA { | |
public int pointBMP; | |
public Rectangle displayArea; | |
public Point offset; | |
} | |
/// <summary> | |
/// Constructor | |
/// </summary> | |
/// <param name="BMSPath">Path to the BMS file</param> | |
public BMSLoader() { | |
BasePath = ""; | |
TextureBank = new Dictionary<int, Texture2D>(); | |
SoundBank = new Dictionary<int, SoundEffect>(); | |
BGABank = new Dictionary<int, BGA>(); | |
SoundInstanceBank = new List<SoundEffectInstance>(); | |
TrackObjects = new List<trackObject>(); | |
timeLines = new Dictionary<int, timeLine>(); | |
isPlaying = false; | |
position = 0; | |
index = 0; | |
currentImage = 0; | |
volumePerc = 1; | |
currentBPM = BPM = 130; | |
position2 = TimeSpan.Zero; | |
} | |
public void loadBMS(string BMSPath) { | |
BasePath = Path.GetDirectoryName(BMSPath); | |
ParseBMS(File.ReadAllText(BMSPath)); | |
} | |
private void ParseBMS(String RawBMSString) { | |
int randomTemp = 0; | |
bool randomIfBlock = false; | |
foreach(string Line in RawBMSString.ToUpper().Replace("\r\n", "\n").Replace('\r', '\n').Split('\n')) { | |
string L = Line, commandName = "", commandParam = "", commandName2 = "", commandParam2 = ""; | |
string[] tmp; | |
if(!L.StartsWith("#") || (randomIfBlock && !L.ToUpper().StartsWith("#ENDIF"))) | |
continue; | |
L = L.Substring(1); | |
tmp = L.Split(L.Contains(':') ? ':' : ' '); | |
commandName = tmp[0]; | |
commandName2 = commandName.Length > 2 ? commandName.Substring(0, 3) : commandName; | |
commandParam2 = commandName.Length > 2 ? commandName.Substring(3) : ""; | |
if(tmp.Length > 1) | |
commandParam = L.Substring((L.Contains(':') ? L.IndexOf(':') : L.IndexOf(' ')) + 1); | |
switch(commandName.ToUpper()) { | |
case "PLAYER": // Player | |
playerCount = int.Parse(commandParam); | |
break; | |
case "GENRE": // Song genre | |
genre = commandParam; | |
break; | |
case "TITLE": // Song title | |
title = commandParam; | |
break; | |
case "ARTIST": // Artist | |
artist = commandParam; | |
break; | |
case "BPM": // Beat per minute | |
BPM = float.Parse(commandParam); | |
break; | |
case "MIDIFILE": // MIDI File (Not Supported) | |
// Currently not supported | |
break; | |
case "PLAYLEVEL": // Game Level | |
playLevel = int.Parse(commandParam); | |
// FIXME: I Don't know what data type it should be | |
break; | |
case "RANK": // Judgement Rank | |
rank = int.Parse(commandParam); | |
break; | |
case "VOLWAV": // Wave vol | |
volumePerc = float.Parse(commandParam) / 100; | |
break; | |
case "RANDOM": // Simple random | |
randomTemp = new Random().Next(1, int.Parse(commandParam)); | |
break; | |
case "IF": // Simple random if | |
if(randomTemp != int.Parse(commandParam)) | |
randomIfBlock = true; | |
break; | |
case "ENDIF": // End simple random if | |
randomIfBlock = false; | |
break; | |
case "STAGEFILE": | |
loadBMP(-1, commandParam); | |
break; | |
default: | |
switch(commandName2.ToUpper()) { | |
case "WAV": // #WAVXX | |
loadWAV(Base36.Decode(commandParam2), commandParam); | |
break; | |
case "BMP": // #BMPXX | |
loadBMP(Base36.Decode(commandParam2), commandParam); | |
break; | |
case "BGA": | |
BGA b = new BGA(); | |
b.pointBMP = Base36.Decode(tmp[1]); | |
b.displayArea = new Rectangle(int.Parse(tmp[2]), int.Parse(tmp[3]), int.Parse(tmp[4]), int.Parse(tmp[5])); | |
b.offset = new Point(int.Parse(tmp[6]), int.Parse(tmp[7])); | |
BGABank.Add(Base36.Decode(commandParam2), b); | |
break; | |
case "BPM": // Extended BPM | |
break; | |
default: | |
int verse = 0; | |
if(int.TryParse(commandName2, out verse)) { | |
int length = commandParam.Length / 2; | |
for(int i = 0; i < length; i++) { | |
trackObject TO = new trackObject(); | |
TO.Position = verse + (float)i / length; | |
TO.Channel = int.Parse(commandParam2); | |
TO.ID = Base36.Decode(commandParam.Substring(i * 2, 2)); | |
TrackObjects.Add(TO); | |
} | |
} | |
break; | |
} | |
break; | |
} | |
} | |
TrackObjects.Sort(delegate(trackObject LHS, trackObject RHS) { | |
return LHS.Position.CompareTo(RHS.Position); | |
}); | |
float tempBPM = BPM, tempVerse = 0; | |
TimeSpan tempTime = TimeSpan.Zero; | |
foreach(trackObject TO in TrackObjects) { | |
if(!timeLines.ContainsKey(TO.Channel)) | |
timeLines.Add(TO.Channel, new timeLine()); | |
if(TO.Channel == 3) | |
tempBPM = int.Parse(Base36.Encode(TO.ID), NumberStyles.AllowHexSpecifier); | |
tempTime += TimeSpan.FromSeconds((TO.Position - tempVerse) / tempBPM * 240); | |
timeLines[TO.Channel].pushKeyFrame(tempTime, TO.ID); | |
} | |
stop(); | |
Console.WriteLine("Data loaded."); | |
} | |
private void loadWAV(int ID, String FileName) { | |
if(SoundBank.ContainsKey(ID)) { | |
SoundBank[ID].Dispose(); | |
SoundBank.Remove(ID); | |
} | |
SoundBank.Add(ID, SoundEffect.FromStream(loadStream(FileName))); | |
Console.WriteLine("LoadWAV #{0}: {1}", ID, FileName); | |
} | |
private void loadBMP(int ID, String FileName) { | |
if(TextureBank.ContainsKey(ID)) { | |
TextureBank[ID].Dispose(); | |
TextureBank.Remove(ID); | |
} | |
System.Drawing.Bitmap BM = new System.Drawing.Bitmap(loadStream(FileName)); | |
MemoryStream MS = new MemoryStream(); | |
BM.Save(MS, System.Drawing.Imaging.ImageFormat.Png); | |
TextureBank.Add(ID, Texture2D.FromStream(GD, MS)); | |
Console.WriteLine("LoadBMP #{0}: {1}", ID, FileName); | |
} | |
private Stream loadStream(String FileName) { | |
return File.OpenRead(Path.Combine(BasePath, FileName)); | |
} | |
/// <summary> | |
/// Should be called per frame | |
/// </summary> | |
/// <param name="GT">Game Time Snapshot</param> | |
public int Update(GameTime GT) { | |
if(!isPlaying) | |
return 0; | |
int updatedCount = 0; | |
/*double eclipsedTime = GT.ElapsedGameTime.TotalMilliseconds / 1000; | |
position += (float)(eclipsedTime / 240 * currentBPM); | |
for(int i = index; i < TrackObjects.Count; i++) { | |
trackObject item = TrackObjects[i]; | |
updatedCount = i - index; | |
index = i; | |
if(item.Position > position) | |
break; | |
if(item.ID == 0) | |
continue; | |
switch(item.Channel) { | |
case 1: // BGM | |
if(SoundBank.ContainsKey(item.ID)) { | |
Console.WriteLine("{1}\tWAVE: #{0}", item.ID, Math.Round(item.Position, 3)); | |
SoundEffectInstance SEI = SoundBank[item.ID].CreateInstance(); | |
SEI.Volume = volumePerc; | |
SEI.Play(); | |
SoundInstanceBank.Add(SEI); | |
} | |
break; | |
case 3: // Tempo | |
Console.WriteLine("{1}\tTempo: {0}", int.Parse(Base36.Encode(item.ID), NumberStyles.AllowHexSpecifier), Math.Round(item.Position, 3)); | |
currentBPM = int.Parse(Base36.Encode(item.ID), NumberStyles.AllowHexSpecifier); | |
break; | |
case 4: // BGA | |
Console.WriteLine("{2}\tBGA: #{0} -> #{1}", item.ID, BGABank.ContainsKey(item.ID) ? BGABank[item.ID].pointBMP : item.ID, Math.Round(item.Position, 3)); | |
currentImage = item.ID; | |
break; | |
case 6: // PoorBMP | |
Console.WriteLine("{1}\tPoor Bitmap: {0}", item.ID, Math.Round(item.Position, 3)); | |
// currentImage = item.ID; | |
break; | |
case 7: | |
Console.WriteLine("{1}\tLayer: {0}", item.ID, Math.Round(item.Position, 3)); | |
break; | |
case 8: | |
Console.WriteLine("{1}\tExt.BPM: {0}", item.ID, Math.Round(item.Position, 3)); | |
break; | |
case 9: | |
Console.WriteLine("{1}\tSTOP: {0}", item.ID, Math.Round(item.Position, 3)); | |
break; | |
case 11: | |
case 12: | |
case 13: | |
case 14: | |
case 15: | |
case 16: | |
case 17: | |
case 18: | |
case 19: // Player 1 | |
case 20: | |
case 21: | |
case 22: | |
case 23: | |
case 24: | |
case 25: | |
case 26: | |
case 27: | |
case 28: // Player 2 | |
if(SoundBank.ContainsKey(item.ID)) { | |
Console.WriteLine("{1}\tPlayer[{2}]: {0}", item.ID, Math.Round(item.Position, 3), item.Channel); | |
SoundEffectInstance SEI = SoundBank[item.ID].CreateInstance(); | |
SEI.Volume = volumePerc; | |
SEI.Play(); | |
SoundInstanceBank.Add(SEI); | |
} | |
break; | |
default: | |
Console.WriteLine("{1}\tUnknown[{2}]: {0}", item.ID, Math.Round(item.Position, 3), item.Channel); | |
break; | |
} | |
}*/ | |
for(int i = SoundInstanceBank.Count - 1; i >= 0; i--) { | |
if(SoundInstanceBank[i].State != SoundState.Playing) { | |
SoundInstanceBank[i].Dispose(); | |
SoundInstanceBank.RemoveAt(i); | |
} | |
} | |
if(index > TrackObjects.Count - 2) | |
stop(); | |
return updatedCount; | |
} | |
/// <summary> | |
/// Get current texture to display | |
/// </summary> | |
/// <returns></returns> | |
public Texture2D getCurrentFrameTexture() { | |
int imgid = BGABank.ContainsKey(currentImage) ? BGABank[currentImage].pointBMP : (TextureBank.ContainsKey(currentImage) ? currentImage : 0); | |
return imgid != 0 ? TextureBank[imgid] : null; | |
} | |
/// <summary> | |
/// Get current texture crop area | |
/// </summary> | |
/// <returns></returns> | |
public Rectangle? getCurrentFrameTextureCropArea() { | |
if(BGABank.ContainsKey(currentImage)) | |
return BGABank[currentImage].displayArea; | |
else | |
return null; | |
} | |
/// <summary> | |
/// Get current texture offset; | |
/// </summary> | |
/// <returns></returns> | |
public Point? getCurrentFrameTextureOffset() { | |
if(BGABank.ContainsKey(currentImage)) | |
return BGABank[currentImage].offset; | |
else | |
return null; | |
} | |
/// <summary> | |
/// Stop playing | |
/// </summary> | |
public void stop() { | |
isPlaying = false; | |
position = 0; | |
index = 0; | |
currentBPM = BPM; | |
currentImage = -1; | |
Console.WriteLine("Stopped."); | |
} | |
public void panic() { | |
for(int i = SoundInstanceBank.Count - 1; i >= 0; i--) { | |
SoundInstanceBank[i].Stop(); | |
SoundInstanceBank[i].Dispose(); | |
SoundInstanceBank.RemoveAt(i); | |
} | |
} | |
/// <summary> | |
/// Start playing | |
/// </summary> | |
public void play() { | |
isPlaying = true; | |
Console.WriteLine("Continured."); | |
} | |
/// <summary> | |
/// Pause the game | |
/// </summary> | |
public void pause() { | |
isPlaying = false; | |
Console.WriteLine("Paused."); | |
} | |
} | |
/// <summary> | |
/// A Base36 De- and Encoder | |
/// </summary> | |
public static class Base36 { | |
private const string CharList = "0123456789abcdefghijklmnopqrstuvwxyz"; | |
/// <summary> | |
/// Encode the given number into a Base36 string | |
/// </summary> | |
/// <param name="input"></param> | |
/// <returns></returns> | |
public static String Encode(int input) { | |
if(input < 0) throw new ArgumentOutOfRangeException("input", input, "input cannot be negative"); | |
char[] clistarr = CharList.ToCharArray(); | |
var result = new Stack<char>(); | |
while(input != 0) { | |
result.Push(clistarr[input % 36]); | |
input /= 36; | |
} | |
return new string(result.ToArray()); | |
} | |
/// <summary> | |
/// Decode the Base36 Encoded string into a number | |
/// </summary> | |
/// <param name="input"></param> | |
/// <returns></returns> | |
public static int Decode(string input) { | |
int result = 0; | |
int pos = input.Length-1; | |
foreach(char c in input.ToLower()) { | |
result += CharList.IndexOf(c) * (int)Math.Pow(36, pos); | |
pos--; | |
} | |
return result; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment