Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active November 8, 2016 03:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/d669f1e22e7411b37bd8 to your computer and use it in GitHub Desktop.
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.
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