Created
January 29, 2024 14:13
-
-
Save ZachIsAGardner/87d34f881dcb60c917e298657e2dfb32 to your computer and use it in GitHub Desktop.
ProgressiveSound
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.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Audio; | |
using Microsoft.Xna.Framework.Content; | |
using Microsoft.Xna.Framework.Graphics; | |
using Newtonsoft.Json; | |
namespace GarbanzoQuest | |
{ | |
public static class Lib | |
{ | |
public static string Loading = "CRAP"; | |
public static float PercentLoaded = 0; | |
static bool skipTextureTasks = false; | |
public static List<Task<Texture2D>> LoadTextureTasks = new List<Task<Texture2D>>() { }; | |
public static List<Task<TextureInfo>> LoadTextureInfoTasks = new List<Task<TextureInfo>>() { }; | |
public static List<Task<Tilemap>> LoadTilemapTasks = new List<Task<Tilemap>>() { }; | |
public static List<Task<ProgressiveSound>> LoadSfxTasks = new List<Task<ProgressiveSound>>() { }; | |
public static bool StartedLoading => (LoadTextureTasks.Count > 0 || skipTextureTasks) | |
|| (LoadTextureInfoTasks.Count > 0 || skipTextureTasks) | |
|| LoadTilemapTasks.Count > 0 | |
|| LoadSfxTasks.Count > 0; | |
public static bool FinishedLoading => (LoadTextureTasks.All(t => t.IsCompleted) || skipTextureTasks) | |
&& (LoadTextureInfoTasks.All(t => t.IsCompleted) || skipTextureTasks) | |
&& LoadTilemapTasks.All(t => t.IsCompleted) | |
&& LoadSfxTasks.All(t => t.IsCompleted); | |
private const float factor = 1f / 8f; | |
public static bool NoAudioDevice = false; | |
private static LoadList currentLoadList; | |
private static ContentManager content => MyGame.Instance.Content; | |
private static bool didLoadFonts; | |
public static Dictionary<string, SpriteFontContainer> Fonts = new Dictionary<string, SpriteFontContainer>(); | |
public static SpriteFontContainer GetFont(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Fonts.TryGet(name); | |
private static bool didLoadBasic; | |
private static bool didLoadSprites; | |
public static Dictionary<string, Texture2D> Textures = new Dictionary<string, Texture2D>(StringComparer.OrdinalIgnoreCase); | |
public static Texture2D GetTexture(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Textures.TryGet(name); | |
public static Dictionary<string, TextureInfo> TextureInfos = new Dictionary<string, TextureInfo>(StringComparer.OrdinalIgnoreCase); | |
public static TextureInfo GetTextureInfo(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : TextureInfos.TryGet(name); | |
private static bool didLoadSfx; | |
public static Dictionary<string, ProgressiveSound> SoundEffects = new Dictionary<string, ProgressiveSound>(StringComparer.OrdinalIgnoreCase); | |
public static ProgressiveSound GetSoundEffect(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : SoundEffects.TryGet(name); | |
private static bool didLoadMusic; | |
public static Dictionary<string, ProgressiveSound> Songs = new Dictionary<string, ProgressiveSound>(StringComparer.OrdinalIgnoreCase); | |
public static ProgressiveSound GetSong(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Songs.TryGet(name); | |
private static bool didLoadEffects; | |
public static Dictionary<string, Effect> Effects = new Dictionary<string, Effect>(StringComparer.OrdinalIgnoreCase); | |
public static Effect GetEffect(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Effects.TryGet(name); | |
private static bool didLoadZones; | |
public static Dictionary<string, Zone> Zones = new Dictionary<string, Zone>(StringComparer.OrdinalIgnoreCase); | |
public static Zone GetZone(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Zones.TryGet(name); | |
private static bool didLoadTilemaps; | |
public static Dictionary<string, Tilemap> Tilemaps = new Dictionary<string, Tilemap>(StringComparer.OrdinalIgnoreCase); | |
public static Tilemap GetTilemap(string name) => | |
String.IsNullOrWhiteSpace(name) ? null : Tilemaps.TryGet(name); | |
// --- | |
static List<string> ParseDirectory(string path, List<string> result = null) | |
{ | |
result = result ?? new List<string>(); | |
if (!Directory.Exists(path)) return result; | |
string[] paths = Directory.GetFiles(path); | |
string[] directories = Directory.GetDirectories(path); | |
result.AddRange(paths); | |
foreach (string directory in directories) | |
{ | |
ParseDirectory(directory, result); | |
} | |
return result; | |
} | |
public static Routine Load() | |
{ | |
return Routine.New(useModifiedDelta: false, action: r => | |
{ | |
LoadFonts(); | |
LoadBasic(); | |
LoadMusic(); | |
LoadEffects(); | |
LoadZones(); | |
ReadSprites(); | |
ReadTilemaps(); | |
ReadSfx(); | |
return true; | |
}, name: "L1") | |
.Then(useModifiedDelta: false, action: r => StartedLoading && FinishedLoading, name: "L2") | |
.Then(useModifiedDelta: false, action: r => | |
{ | |
StoreSprites(); | |
StoreTilemaps(); | |
StoreSfx(); | |
return true; | |
}, name: "L3"); | |
} | |
public static void LoadFonts() | |
{ | |
if (didLoadFonts) return; | |
didLoadFonts = true; | |
Loading = "FONTS"; | |
Fonts["LanaPixel"] = (new SpriteFontContainer() | |
{ | |
Name = "LanaPixel", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/LanaPixel"), | |
Offset = new Vector2(0, 4), | |
CharacterHeight = 7, | |
CharacterWidth = 8, | |
VerticalSpacing = 6 | |
}); | |
Fonts["MonogramExtended"] = (new SpriteFontContainer() | |
{ | |
Name = "MonogramExtended", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/MonogramExtended"), | |
Offset = new Vector2(0, 4), | |
CharacterHeight = 7, | |
CharacterWidth = 6, | |
VerticalSpacing = 6 | |
}); | |
Fonts["SinsGold"] = (new SpriteFontContainer() | |
{ | |
Name = "SinsGold", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/SinsGold"), | |
Offset = new Vector2(0, 4), | |
CharacterHeight = 7, | |
CharacterWidth = 6, | |
VerticalSpacing = 6 | |
}); | |
Fonts["PixelLocale"] = (new SpriteFontContainer() | |
{ | |
Name = "PixelLocale", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/PixelLocale"), | |
Offset = new Vector2(0, 4), | |
CharacterHeight = 7, | |
CharacterWidth = 10, | |
VerticalSpacing = 6 | |
}); | |
Fonts["FindersKeepers"] = (new SpriteFontContainer() | |
{ | |
Name = "FindersKeepers", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/FindersKeepers"), | |
Offset = new Vector2(0, 4), | |
CharacterHeight = 7, | |
CharacterWidth = 6, | |
VerticalSpacing = 6 | |
}); | |
Fonts["Pico8"] = (new SpriteFontContainer() | |
{ | |
Name = "Pico8", | |
SpriteFont = content.Load<SpriteFont>("Assets/Fonts/Pico8"), | |
Offset = new Vector2(0, 0), | |
CharacterHeight = 5, | |
CharacterWidth = 4, | |
VerticalSpacing = 3 | |
}); | |
Fonts.ToList().ForEach(f => f.Value.SpriteFont.LineSpacing = f.Value.FullHeight); | |
PercentLoaded += factor; | |
} | |
public static void LoadBasicTextures() | |
{ | |
Textures["Pixel"] = (Shapes.Rectangle(1, 1, Color.White, "Pixel")); | |
Textures["Pixel25"] = (Shapes.Rectangle(1, 1, new Color(1f, 1f, 1f, 0.25f), "Pixel25")); | |
Textures["Pixel50"] = (Shapes.Rectangle(1, 1, new Color(1f, 1f, 1f, 0.50f), "Pixel50")); | |
Textures["Pixel75"] = (Shapes.Rectangle(1, 1, new Color(1f, 1f, 1f, 0.75f), "Pixel75")); | |
Textures["Pixel90"] = (Shapes.Rectangle(1, 1, new Color(1f, 1f, 1f, 0.90f), "Pixel90")); | |
Textures["TileGizmo"] = (Shapes.CoolRectangleOutline(16, 16, new Color(1f, 1f, 1f, 1f), "TileGizmo")); | |
Textures["TileGizmo8"] = (Shapes.CoolRectangleOutline(8, 8, new Color(1f, 1f, 1f, 1f), "TileGizmo8")); | |
} | |
public static void LoadBasic() | |
{ | |
if (didLoadBasic) return; | |
didLoadBasic = true; | |
Loading = "BASIC"; | |
LoadBasicTextures(); | |
Zones["Load"] = (Zone.Get("Load")); | |
PercentLoaded += factor; | |
} | |
public static void ReadSprites(bool force = false, List<string> filters = null) | |
{ | |
if (force) | |
{ | |
if (filters != null && filters.Count > 0) | |
{ | |
ImportSprites(filters); | |
} | |
else | |
{ | |
Textures.Clear(); | |
TextureInfos.Clear(); | |
ImportSprites(); | |
LoadBasicTextures(); | |
} | |
} | |
else | |
{ | |
if (didLoadSprites) return; | |
} | |
didLoadSprites = true; | |
Loading = "SPRITES"; | |
// Sprites/ Textures | |
string baseSpritesPath = $"{Paths.ContentPublishPath()}/Assets/Sprites"; | |
string customSpritesPath = $"{Paths.UserStorageBasePath()}/Custom/Sprites"; | |
IEnumerable<string> spritePaths = ParseDirectory(baseSpritesPath).Where(x => x.Contains(".png") || x.Contains(".xnb")); | |
spritePaths = spritePaths.Concat(ParseDirectory(customSpritesPath).Where(x => x.Contains(".png") || x.Contains(".xnb"))); | |
List<string> spriteDuplicates = new List<string>() { }; | |
List<string> visitedSprites = new List<string>() { }; | |
foreach (string path in spritePaths) | |
{ | |
// Make sure directory characters are "/" | |
string goodPath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = goodPath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
if (filters != null && filters.Count > 0) | |
{ | |
if (!filters.Any(f => name.ToLower().Contains(f.ToLower()))) continue; | |
} | |
if (visitedSprites.Contains(name) || Textures.ContainsKey(name)) | |
{ | |
spriteDuplicates.Add(name); | |
} | |
else | |
{ | |
LoadTextureTasks.Add(Task.Factory.StartNew(() => | |
{ | |
// Convert into relative path | |
goodPath = goodPath.Split("Content/").Last(); | |
// Remove file extension from good path | |
goodPath = goodPath.Split(".")[0]; | |
// Load texture | |
// Texture2D texture = content.Load<Texture2D>(goodPath); | |
Texture2D texture = Texture2D.FromFile( | |
MyGame.Instance.GraphicsDevice, | |
path | |
); | |
texture.Name = name; | |
PercentLoaded += (factor * 0.5f) / spritePaths.Count(); | |
return texture; | |
})); | |
} | |
visitedSprites.Add(name); | |
} | |
if (spriteDuplicates.Count > 0) throw new Exception("Duplicate Textures detected."); | |
// Sprite/ Texture infos | |
IEnumerable<string> spriteInfoPaths = ParseDirectory(baseSpritesPath).Where(x => x.Contains(".json")).Select(x => x.Replace("\\", "/")); | |
spriteInfoPaths = spriteInfoPaths.Concat(ParseDirectory(customSpritesPath).Where(x => x.Contains(".json"))).Select(x => x.Replace("\\", "/")); | |
List<string> spriteInfoDuplicates = new List<string>() { }; | |
List<string> visitedSpriteInfos = new List<string>() { }; | |
foreach (string path in spriteInfoPaths) | |
{ | |
List<string> innerInfoNames = path.Split("/").Last().Split(".").ToList(); | |
innerInfoNames.RemoveAt(innerInfoNames.Count - 1); | |
innerInfoNames.Reverse(); | |
if (filters != null && filters.Count > 0) | |
{ | |
string n = path.Replace("\\", "/").Split("/").Last().ToLower(); | |
if (!filters.Any(f => n.Contains(f.ToLower()))) | |
{ | |
continue; | |
} | |
} | |
string name = path; | |
// Get just the name. | |
name = name.Split("/").Last(); | |
// Remove file extension and any parents from name | |
name = name.Split(".")[0]; | |
if (visitedSpriteInfos.Contains(name) || TextureInfos.ContainsKey(name)) | |
{ | |
spriteInfoDuplicates.Add(name); | |
} | |
else | |
{ | |
LoadTextureInfoTasks.Add(Task.Factory.StartNew(() => | |
{ | |
TextureInfo info = new TextureInfo(); | |
foreach (string innerInfoName in innerInfoNames) | |
{ | |
string innerPath = spriteInfoPaths.First(x => x.Contains($"/{innerInfoName}.")); | |
string innerJson = File.ReadAllText(innerPath); | |
TextureInfo innerInfo = JsonConvert.DeserializeObject<TextureInfo>(innerJson); | |
info.Merge(innerInfo, innerJson); | |
} | |
info.Name = name; | |
PercentLoaded += (factor * 0.5f) / spriteInfoPaths.Count(); | |
return info; | |
})); | |
} | |
visitedSpriteInfos.Add(name); | |
} | |
if (spriteInfoDuplicates.Count > 0) throw new Exception("Duplicate TextureInfos detected."); | |
skipTextureTasks = LoadTextureTasks.Count() == 0; | |
} | |
public static void StoreSprites() | |
{ | |
LoadTextureTasks.ForEach(t => | |
{ | |
Textures[t.Result.Name] = t.Result; | |
}); | |
LoadTextureTasks.Clear(); | |
LoadTextureInfoTasks.ForEach(t => | |
{ | |
TextureInfos[t.Result.Name] = t.Result; | |
}); | |
LoadTextureInfoTasks.Clear(); | |
skipTextureTasks = false; | |
} | |
public static void ReadSfx(bool force = false, List<string> filters = null) | |
{ | |
if ((didLoadSfx && !force) || CONSTANTS.NO_AUDIO || NoAudioDevice) | |
{ | |
PercentLoaded += factor; | |
return; | |
} | |
if (force) | |
{ | |
if (filters != null && filters.Count > 0) | |
{ | |
ImportSfx(filters); | |
} | |
else | |
{ | |
SoundEffects.Clear(); | |
ImportSfx(); | |
} | |
} | |
didLoadSfx = true; | |
Loading = "SOUND EFFECTS"; | |
string baseSfxPath = $"{Paths.ContentPublishPath()}/Assets/Sfx"; | |
string customSfxPath = $"{Paths.UserStorageBasePath()}/Custom/Sfx"; | |
List<string> duplicates = new List<string>() { }; | |
List<string> visited = new List<string>() { }; | |
try | |
{ | |
IEnumerable<string> sfxPaths = ParseDirectory(baseSfxPath).Where(x => x.Contains(".ogg")); | |
sfxPaths = sfxPaths.Concat(ParseDirectory(customSfxPath).Where(x => x.Contains(".ogg"))); | |
foreach (string path in sfxPaths) | |
{ | |
// Make sure directory characters are "/" | |
string relativePath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = relativePath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
if (filters != null && filters.Count > 0) | |
{ | |
if (!filters.Any(f => name.ToLower().Contains(f.ToLower()))) continue; | |
} | |
if (visited.Contains(name) || SoundEffects.ContainsKey(name)) | |
{ | |
duplicates.Add(name); | |
} | |
else | |
{ | |
LoadSfxTasks.Add(Task.Factory.StartNew(() => | |
{ | |
// Load SoundEffect | |
ProgressiveSound soundEffect = new ProgressiveSound(relativePath); | |
// Read | |
soundEffect.ReadOgg(); | |
return soundEffect; | |
})); | |
} | |
visited.Add(name); | |
} | |
} | |
catch (Exception error) | |
{ | |
// No audio output device detected? | |
Lib.NoAudioDevice = true; | |
} | |
if (duplicates.Count > 0) throw new Exception("Duplicate SFX detected."); | |
PercentLoaded += factor; | |
} | |
public static void StoreSfx() | |
{ | |
LoadSfxTasks.ForEach(t => | |
{ | |
SoundEffects[t.Result.Name] = t.Result; | |
}); | |
LoadSfxTasks.Clear(); | |
} | |
// Load Songs without Reading .oggs into memory | |
public static void LoadMusic(bool force = false) | |
{ | |
if ((didLoadMusic && !force) || CONSTANTS.NO_AUDIO || NoAudioDevice) | |
{ | |
PercentLoaded += factor; | |
return; | |
} | |
if (force) | |
{ | |
SoundEffects.Clear(); | |
ImportSongs(); | |
} | |
Loading = "MUSIC"; | |
didLoadMusic = true; | |
string baseMusicPath = $"{Paths.ContentPublishPath()}/Assets/Music"; | |
string customMusicPath = $"{Paths.UserStorageBasePath()}/Custom/Music"; | |
List<string> duplicates = new List<string>() { }; | |
try | |
{ | |
IEnumerable<string> musicPaths = ParseDirectory(baseMusicPath).Where(x => x.Contains(".ogg")); | |
musicPaths = musicPaths.Concat(ParseDirectory(customMusicPath).Where(x => x.Contains(".ogg"))); | |
foreach (string path in musicPaths) | |
{ | |
// Make sure directory characters are "/" | |
string relativePath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = relativePath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
if (Songs.ContainsKey(name)) | |
{ | |
duplicates.Add(name); | |
} | |
else | |
{ | |
// Load Song | |
ProgressiveSound song = new ProgressiveSound(relativePath); | |
Songs[song.Name] = song; | |
// Don't read yet | |
} | |
} | |
} | |
catch (Exception error) | |
{ | |
// No audio output device detected? | |
Lib.NoAudioDevice = true; | |
} | |
if (duplicates.Count > 0) throw new Exception("Duplicate Songs detected."); | |
PercentLoaded += factor; | |
} | |
// Read some Songs into Memory (loading them all at once is too costly) | |
public static void GenerateSongs() | |
{ | |
foreach (KeyValuePair<string, ProgressiveSound> songToLoad in Songs.Where(s => s.Value.InQueueLoad != null).OrderBy(s => s.Value.InQueueLoad)) | |
{ | |
Task.Factory.StartNew(() => | |
{ | |
songToLoad.Value.ReadOgg(); | |
}); | |
} | |
} | |
public static void LoadEffects() | |
{ | |
if (didLoadEffects) return; | |
didLoadEffects = true; | |
Loading = "EFFECTS"; | |
string baseEffectsPath = $"{Paths.ContentPublishPath()}/Assets/Effects"; | |
IEnumerable<string> effectPaths = ParseDirectory(baseEffectsPath).Where(x => x.Contains(".xnb")); | |
List<string> duplicates = new List<string>() { }; | |
foreach (string path in effectPaths) | |
{ | |
// Make sure directory characters are "/" | |
string goodPath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = goodPath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
if (Effects.ContainsKey(name)) | |
{ | |
duplicates.Add(name); | |
} | |
else | |
{ | |
// Convert into relative path | |
goodPath = goodPath.Split("Content/").Last(); | |
// Remove file extension from good path | |
goodPath = goodPath.Split(".")[0]; | |
// Load Effect | |
Effect effect = content.Load<Effect>(goodPath); | |
effect.Name = name; | |
Effects[name] = (effect); | |
} | |
} | |
if (duplicates.Count > 0) throw new Exception("Duplicates Effects detected."); | |
PercentLoaded += factor; | |
} | |
public static void LoadZones() | |
{ | |
didLoadZones = true; | |
Loading = "ZONES"; | |
string baseZonesPath = $"{Paths.ContentPublishPath()}/Assets/Zones"; | |
string customZonesPath = $"{Paths.UserStorageBasePath()}/Custom/Zones"; | |
IEnumerable<string> zonePaths = ParseDirectory(baseZonesPath).Where(x => x.Contains(".json")); | |
zonePaths = zonePaths.Concat(ParseDirectory(customZonesPath).Where(x => x.Contains(".json"))); | |
List<string> duplicates = new List<string>() { }; | |
foreach (string path in zonePaths) | |
{ | |
// Make sure directory characters are "/" | |
string goodPath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = goodPath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
// Loaded in Basic | |
if (name == "Load") continue; | |
if (Zones.ContainsKey(name)) | |
{ | |
duplicates.Add(name); | |
} | |
else | |
{ | |
// Load Zone | |
Zone zone = Zone.Get(name); | |
zone.Name = name; | |
Zones[name] = zone; | |
} | |
} | |
if (duplicates.Count > 0) throw new Exception("Duplicates Zones detected."); | |
PercentLoaded += factor; | |
} | |
public static void ReadTilemaps() | |
{ | |
if (didLoadTilemaps) return; | |
didLoadTilemaps = true; | |
Loading = "TILEMAPS"; | |
string baseTilemapsPath = $"{Paths.ContentPublishPath()}/Assets/Tilemaps"; | |
string customTilemapsPath = $"{Paths.UserStorageBasePath()}/Custom/Tilemaps"; | |
IEnumerable<string> tilemapPaths = ParseDirectory(baseTilemapsPath).Where(x => x.Contains(".json")); | |
tilemapPaths = tilemapPaths.Concat(ParseDirectory(customTilemapsPath).Where(x => x.Contains(".json"))); | |
List<string> duplicates = new List<string>() { }; | |
List<string> visited = new List<string>() { }; | |
foreach (string path in tilemapPaths) | |
{ | |
// Make sure directory characters are "/" | |
string goodPath = path.Replace("\\", "/"); | |
// Get just the name. | |
string name = goodPath.Split("/").Last(); | |
// Remove file extension from name | |
name = name.Split(".")[0]; | |
if (visited.Contains(name) || Tilemaps.ContainsKey(name)) | |
{ | |
duplicates.Add(name); | |
} | |
else | |
{ | |
LoadTilemapTasks.Add(Task.Factory.StartNew(() => | |
{ | |
string zone = goodPath.Split("/").Last(1); | |
if (zone == "Tilemaps") zone = null; | |
// Load Tilemap | |
Tilemap tilemap = Tilemap.Get(zone, name); | |
PercentLoaded += factor / tilemapPaths.Count(); | |
return tilemap; | |
})); | |
} | |
visited.Add(name); | |
} | |
if (duplicates.Count > 0) throw new Exception("Duplicates Tilemaps detected."); | |
} | |
public static void StoreTilemaps() | |
{ | |
LoadTilemapTasks.ForEach(t => | |
{ | |
if (t.Result != null) Tilemaps[t.Result.Name] = t.Result; | |
}); | |
LoadTilemapTasks.Clear(); | |
} | |
public static void LoadFromList(LoadList loadList) | |
{ | |
if (loadList == null) loadList = new LoadList(); | |
foreach (var songToUnload in Lib.Songs.Where(s => s.Value.Loaded && !loadList.Songs.Contains(s.Value.Name))) | |
{ | |
Music music = MyGame.MusicContainer.GetComponents<Music>().Find(m => m.Entity.Name == songToUnload.Value.Name); | |
// Currently playing | |
if (music?.IsAvailable == true) | |
{ | |
// music.FadeOut(unload: true); | |
} | |
else | |
{ | |
songToUnload.Value.UnLoad(); | |
} | |
} | |
int i = 0; | |
foreach (string name in loadList.Songs) | |
{ | |
ProgressiveSound song = Songs.TryGet(name); | |
if (song == null || song.Loaded || song.InQueueLoad != null) continue; | |
song.InQueueLoad = i; | |
i++; | |
} | |
GenerateSongs(); | |
} | |
// Import | |
private static void CopyFolder(string sourcePath, string targetPath) | |
{ | |
// Now Create all of the directories | |
foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) | |
{ | |
Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); | |
} | |
// Copy all the files & Replaces any files with the same name | |
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories)) | |
{ | |
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); | |
} | |
} | |
public static void ImportSprites(List<string> filters = null) | |
{ | |
string fromPath = $"{Paths.ContentSourcePath()}/Assets/Sprites"; ; | |
string toPath = $"{Paths.ContentPublishPath()}/Assets/Sprites"; | |
if (filters != null && filters.Count > 0) | |
{ | |
// Find to files | |
List<(string Path, string Name)> toPngFiles = Directory.GetFiles(toPath, "*.png", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.ToList(); | |
List<(string Path, string Name)> toJsonFiles = Directory.GetFiles(toPath, "*.json", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.ToList(); | |
// Find from files | |
List<(string Path, string Name)> fromPngFiles = Directory.GetFiles(fromPath, "*.png", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.Where(f => filters.Any(ft => f.Item2.ToLower().Contains(ft.ToLower()))) | |
.ToList(); | |
List<(string Path, string Name)> fromJsonFiles = Directory.GetFiles(fromPath, "*.json", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.Where(f => filters.Any(ft => f.Item2.ToLower().Contains(ft.ToLower()))) | |
.ToList(); | |
// Copy files | |
fromPngFiles.ForEach(p => | |
{ | |
string path = toPngFiles.Find(tp => tp.Name == p.Name).Path; | |
if (path.HasValue()) File.Copy(p.Path, path, true); | |
Textures = Textures.Where(t => $"{t.Key}.png" != $"{p.Name.Split(".").First()}.png").ToDictionary(x => x.Key, x => x.Value); | |
}); | |
fromJsonFiles.ForEach(j => | |
{ | |
string path = toJsonFiles.Find(tp => tp.Name == j.Name).Path; | |
if (path.HasValue()) File.Copy(j.Path, path, true); | |
TextureInfos = TextureInfos.Where(t => $"{t.Key}.json" != $"{j.Name.Split(".").First()}.json").ToDictionary(x => x.Key, x => x.Value); | |
}); | |
} | |
else | |
{ | |
Textures.Clear(); | |
TextureInfos.Clear(); | |
Directory.Delete(toPath, true); | |
CopyFolder(fromPath, toPath); | |
} | |
} | |
public static void ImportSfx(List<string> filters = null) | |
{ | |
string fromPath = $"{Paths.ContentSourcePath()}/Assets/Sfx"; ; | |
string toPath = $"{Paths.ContentPublishPath()}/Assets/Sfx"; | |
if (filters != null && filters.Count > 0) | |
{ | |
// Find to files | |
List<(string Path, string Name)> toFiles = Directory.GetFiles(toPath, "*.ogg", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.ToList(); | |
// Find from files | |
List<(string Path, string Name)> fromFiles = Directory.GetFiles(fromPath, "*.ogg", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.Where(f => filters.Any(ft => f.Item2.ToLower().Contains(ft.ToLower()))) | |
.ToList(); | |
// Copy files | |
fromFiles.ForEach(p => | |
{ | |
string path = toFiles.Find(tp => tp.Name == p.Name).Path; | |
if (path.HasValue()) File.Copy(p.Path, path, true); | |
Textures = Textures.Where(t => $"{t.Key}.ogg" != $"{p.Name.Split(".").First()}.ogg").ToDictionary(x => x.Key, x => x.Value); | |
}); | |
} | |
else | |
{ | |
Lib.SoundEffects.Clear(); | |
Directory.Delete(toPath, true); | |
CopyFolder(fromPath, toPath); | |
} | |
} | |
public static void ImportSongs(List<string> filters = null) | |
{ | |
string fromPath = $"{Paths.ContentSourcePath()}/Assets/Music"; ; | |
string toPath = $"{Paths.ContentPublishPath()}/Assets/Music"; | |
if (filters != null && filters.Count > 0) | |
{ | |
// Find to files | |
List<(string Path, string Name)> toFiles = Directory.GetFiles(toPath, "*.ogg", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.ToList(); | |
// Find from files | |
List<(string Path, string Name)> fromFiles = Directory.GetFiles(fromPath, "*.ogg", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.Where(f => filters.Any(ft => f.Item2.ToLower().Contains(ft.ToLower()))) | |
.ToList(); | |
// Copy files | |
fromFiles.ForEach(p => | |
{ | |
string path = toFiles.Find(tp => tp.Name == p.Name).Path; | |
if (path.HasValue()) File.Copy(p.Path, path, true); | |
Textures = Textures.Where(t => $"{t.Key}.ogg" != $"{p.Name.Split(".").First()}.ogg").ToDictionary(x => x.Key, x => x.Value); | |
}); | |
} | |
else | |
{ | |
Lib.SoundEffects.Clear(); | |
Directory.Delete(toPath, true); | |
CopyFolder(fromPath, toPath); | |
} | |
} | |
public static void ImportDials(List<string> filters = null) | |
{ | |
string fromPath = $"{Paths.ContentSourcePath()}/Assets/Dials"; ; | |
string toPath = $"{Paths.ContentPublishPath()}/Assets/Dials"; | |
if (filters != null && filters.Count > 0) | |
{ | |
// Find to files | |
List<(string Path, string Name)> toFiles = Directory.GetFiles(toPath, "*.txt", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.ToList(); | |
// Find from files | |
List<(string Path, string Name)> fromFiles = Directory.GetFiles(fromPath, "*.txt", SearchOption.AllDirectories) | |
.Select(f => (f, f.Replace("\\", "/").Split("/").Last())) | |
.Where(f => filters.Any(ft => f.Item2.ToLower().Contains(ft.ToLower()))) | |
.ToList(); | |
// Copy files | |
fromFiles.ForEach(p => | |
{ | |
string path = toFiles.Find(tp => tp.Name == p.Name).Path; | |
if (path.HasValue()) File.Copy(p.Path, path, true); | |
Textures = Textures.Where(t => $"{t.Key}.txt" != $"{p.Name.Split(".").First()}.txt").ToDictionary(x => x.Key, x => x.Value); | |
}); | |
} | |
else | |
{ | |
Chat.Entries.Clear(); | |
Directory.Delete(toPath, true); | |
CopyFolder(fromPath, toPath); | |
} | |
} | |
} | |
} |
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.Collections.Generic; | |
using System.Linq; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
using Microsoft.Xna.Framework.Audio; | |
using System.IO; | |
using System.Threading.Tasks; | |
using NVorbis; | |
namespace GarbanzoQuest | |
{ | |
// https://www.reddit.com/r/monogame/comments/9unc7d/music_with_partial_loop/ | |
// Better loopable sound, Used for SFX and Songs. | |
public class ProgressiveSound | |
{ | |
public string Name; | |
public string Path; | |
public bool Loaded { get; private set; } = false; | |
public int? InQueueLoad = null; | |
private float duration; | |
private float bytesOverMilliseconds; | |
private byte[] byteArray; | |
private int count; | |
private int loopLengthBytes; | |
private int loopEndBytes; | |
Int64 loopStartSamples = 0; | |
Int64 loopLengthSamples = 0; | |
Int64 loopEndSamples = 0; | |
int chunkId; | |
int fileSize; | |
int riffType; | |
int fmtId; | |
int fmtSize; | |
int fmtCode; | |
int channels; | |
int sampleRate; | |
int fmtAvgBps; | |
int fmtBlockAlign; | |
int bitDepth; | |
int fmtExtraSize; | |
int dataID; | |
int dataSize; | |
const int bufferDuration = 100; | |
// Private | |
public ProgressiveSound(string path) | |
{ | |
Name = path.Split("/").Last(); | |
Name = Name.Split(".")[0]; | |
Path = path; | |
} | |
// Moving this out here fixes a weird crash | |
DynamicSoundEffectInstance instance = null; | |
public ProgressiveSoundInstance CreateInstance() | |
{ | |
try | |
{ | |
if (instance == null || instance.IsDisposed) | |
{ | |
instance = new DynamicSoundEffectInstance(sampleRate, (AudioChannels)channels); | |
} | |
} | |
catch (Exception error) | |
{ | |
// Factory.UiToast($"SFX CRASH: {MyGame.GetComponents<SfxPlayer>().Count()}"); | |
return null; | |
} | |
count = AlignTo8Bytes(instance.GetSampleSizeInBytes(TimeSpan.FromMilliseconds(bufferDuration)) + 4); | |
loopLengthBytes = AlignTo8Bytes(instance.GetSampleSizeInBytes(TimeSpan.FromSeconds((double)loopLengthSamples / sampleRate))); | |
loopEndBytes = instance.GetSampleSizeInBytes(TimeSpan.FromSeconds((double)loopEndSamples / sampleRate)); // doesn't need alignment | |
return new ProgressiveSoundInstance(Name, instance, byteArray, count, loopLengthBytes, loopEndBytes, bytesOverMilliseconds); | |
} | |
private static int AlignTo8Bytes(int unalignedBytes) | |
{ | |
int result = unalignedBytes + 4; | |
result -= (result % 8); | |
return result; | |
} | |
public void QueueLoad() | |
{ | |
InQueueLoad = -1; | |
Lib.GenerateSongs(); | |
} | |
public void UnLoad() | |
{ | |
if (byteArray != null) byteArray = null; | |
Loaded = false; | |
InQueueLoad = null; | |
} | |
public void ReadOgg(string path = "") | |
{ | |
if (Loaded) return; | |
if (path.HasValue()) Path = path; | |
using (VorbisReader vorbis = new VorbisReader(Path)) | |
{ | |
channels = vorbis.Channels; | |
sampleRate = vorbis.SampleRate; | |
duration = (float)vorbis.TotalTime.TotalMilliseconds; | |
TimeSpan totalTime = vorbis.TotalTime; | |
float[] buffer = new float[channels * sampleRate / 5]; | |
List<byte> byteList = new List<byte>(); | |
int count; | |
while ((count = vorbis.ReadSamples(buffer, 0, buffer.Length)) > 0) | |
{ | |
for (int i = 0; i < count; i++) | |
{ | |
short temp = (short)(32767f * buffer[i]); | |
if (temp > 32767) | |
{ | |
byteList.Add(0xFF); | |
byteList.Add(0x7F); | |
} | |
else if (temp < -32768) | |
{ | |
byteList.Add(0x80); | |
byteList.Add(0x00); | |
} | |
byteList.Add((byte)temp); | |
byteList.Add((byte)(temp >> 8)); | |
} | |
} | |
byteArray = byteList.ToArray(); | |
bytesOverMilliseconds = byteArray.Length / duration; | |
Int64.TryParse( | |
vorbis.Comments.FirstOrDefault(c => c.Contains("LOOPSTART"))?.Split("LOOPSTART=")[1], | |
out loopStartSamples | |
); | |
Int64.TryParse( | |
vorbis.Comments.FirstOrDefault(c => c.Contains("LOOPLENGTH"))?.Split("LOOPLENGTH=")[1], | |
out loopLengthSamples | |
); | |
Int64.TryParse( | |
vorbis.Comments.FirstOrDefault(c => c.Contains("LOOPEND"))?.Split("LOOPEND=")[1], | |
out loopEndSamples | |
); | |
if (loopStartSamples != 0) | |
{ | |
if (loopEndSamples == 0) | |
{ | |
loopEndSamples = ((Int64)duration * (Int64)sampleRate) / 1000; | |
} | |
if (loopLengthSamples == 0) | |
{ | |
loopLengthSamples = loopEndSamples - loopStartSamples; | |
} | |
} | |
} | |
Loaded = true; | |
InQueueLoad = null; | |
} | |
private void ReadWav(string path, string absolutePath) | |
{ | |
byte[] allBytes = File.ReadAllBytes(absolutePath); | |
int byterate = BitConverter.ToInt32(new[] { allBytes[28], allBytes[29], allBytes[30], allBytes[31] }, 0); | |
duration = (int)Math.Floor(((float)(allBytes.Length - 8) / (float)(byterate)) * 1000); | |
Stream waveFileStream = TitleContainer.OpenStream(path); | |
BinaryReader reader = new BinaryReader(waveFileStream); | |
chunkId = reader.ReadInt32(); | |
fileSize = reader.ReadInt32(); | |
riffType = reader.ReadInt32(); | |
fmtId = reader.ReadInt32(); | |
fmtSize = reader.ReadInt32(); | |
fmtCode = reader.ReadInt16(); | |
channels = reader.ReadInt16(); | |
sampleRate = reader.ReadInt32(); | |
fmtAvgBps = reader.ReadInt32(); | |
fmtBlockAlign = reader.ReadInt16(); | |
bitDepth = reader.ReadInt16(); | |
if (fmtSize == 18) | |
{ | |
// Read any extra values | |
fmtExtraSize = reader.ReadInt16(); | |
reader.ReadBytes(fmtExtraSize); | |
} | |
dataID = reader.ReadInt32(); | |
dataSize = reader.ReadInt32(); | |
byteArray = reader.ReadBytes(dataSize); | |
bytesOverMilliseconds = byteArray.Length / duration; | |
// Load metainfo, or specifically, TXXX "LOOP_____" tags | |
char[] sectionHeader = new char[4]; | |
int sectionSize; | |
long sectionBasePosition; | |
char[] localSectionHeader = new char[4]; | |
int localSectionSize; | |
Int16 localFlags; | |
bool isData; | |
char inChar; | |
string tagTitle; | |
string tagData; | |
while (waveFileStream.Position < waveFileStream.Length - 10) // -10s are to prevent overrunning the end of the file when a partial header or filler bytes are present | |
{ | |
sectionHeader = reader.ReadChars(4); | |
sectionSize = reader.ReadInt32(); | |
sectionBasePosition = waveFileStream.Position; | |
if (new string(sectionHeader) != "id3 ") | |
{ | |
waveFileStream.Position += sectionSize; | |
continue; | |
} | |
waveFileStream.Position += 10; // skip the header | |
while ((waveFileStream.Position < sectionBasePosition + sectionSize - 10) && (waveFileStream.Position < waveFileStream.Length)) | |
{ | |
localSectionHeader = reader.ReadChars(4); | |
localSectionSize = 0; | |
// need to read this as big-endian | |
for (int i = 0; i < 4; i++) | |
{ | |
localSectionSize = (localSectionSize << 8) + reader.ReadByte(); | |
} | |
localFlags = reader.ReadInt16(); // probably also needs endian swap... if we were paying attention to it, which we don't need to | |
if (new String(localSectionHeader) != "TXXX") | |
{ | |
waveFileStream.Position += localSectionSize; | |
continue; | |
} | |
isData = false; | |
tagTitle = ""; | |
tagData = ""; | |
reader.ReadByte(); // text encoding byte, we're gonna just ignore this | |
for (int i = 0; i < localSectionSize - 1; i++) // -1 due to aforementioned ignored byte | |
{ | |
inChar = reader.ReadChar(); | |
if (isData) | |
{ | |
tagData += inChar; | |
} | |
else if (inChar == '\x00') | |
{ | |
isData = true; | |
} | |
else | |
{ | |
tagTitle += inChar; | |
} | |
} | |
// Process specific tag types we're looking for. If you want to use this for general tag-reading, you'll need to implement that yourself, | |
// keeping in mind this code has also filtered for TXXX records only. | |
switch (tagTitle) | |
{ | |
case "LOOPSTART": | |
Int64.TryParse(tagData, out loopStartSamples); | |
break; | |
case "LOOPLENGTH": | |
Int64.TryParse(tagData, out loopLengthSamples); | |
break; | |
case "LOOPEND": | |
Int64.TryParse(tagData, out loopEndSamples); | |
break; | |
} | |
} | |
if (loopEndSamples == 0) | |
{ | |
loopEndSamples = ((Int64)duration * (Int64)sampleRate) / 1000; | |
} | |
if (loopLengthSamples == 0) | |
{ | |
loopLengthSamples = loopEndSamples - loopStartSamples; | |
} | |
} | |
} | |
} | |
} |
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.Collections.Generic; | |
using System.Linq; | |
using Microsoft.Xna.Framework; | |
using Microsoft.Xna.Framework.Graphics; | |
using Microsoft.Xna.Framework.Audio; | |
namespace GarbanzoQuest | |
{ | |
public class ProgressiveSoundInstance | |
{ | |
// Properties | |
// Public | |
public string Name; | |
public float Volume | |
{ | |
get => dynamicSound?.Volume ?? 0; | |
set => dynamicSound.Volume = Math.Min(Math.Max(0, value), 1); | |
} | |
public float Pitch | |
{ | |
get => dynamicSound?.Pitch ?? 0; | |
set => dynamicSound.Pitch = Math.Min(Math.Max(-1, value), 1); | |
} | |
public bool IsLooped = false; | |
public SoundState State => (dynamicSound == null || dynamicSound.IsDisposed) | |
? SoundState.Stopped | |
: dynamicSound.State; | |
public bool QueueStop = false; | |
public bool IsAvailable => dynamicSound != null; | |
// Private | |
private float originalVolume; | |
private DynamicSoundEffectInstance dynamicSound; | |
private byte[] byteArray; | |
private int position; | |
private int count; | |
private int loopLengthBytes; | |
private int loopEndBytes; | |
private float bytesOverMilliseconds; | |
// Methods | |
// Public | |
public ProgressiveSoundInstance(string name, DynamicSoundEffectInstance dynamicSound, byte[] byteArray, int count, int loopLengthBytes, int loopEndBytes, float bytesOverMilliseconds) | |
{ | |
Name = name; | |
this.dynamicSound = dynamicSound; | |
this.byteArray = byteArray; | |
this.count = count; | |
this.loopLengthBytes = loopLengthBytes; | |
this.loopEndBytes = loopEndBytes; | |
this.bytesOverMilliseconds = bytesOverMilliseconds; | |
this.dynamicSound.BufferNeeded += new EventHandler<EventArgs>(UpdateBuffer); | |
} | |
public void Dispose() | |
{ | |
if (dynamicSound != null && !dynamicSound.IsDisposed) dynamicSound.Dispose(); | |
} | |
public void Play() | |
{ | |
if (dynamicSound.IsDisposed) return; | |
// dynamicSound.Pitch = 0; | |
if (MyGame.IsMuted) dynamicSound.Volume = 0; | |
dynamicSound.Play(); | |
} | |
public void Pause() | |
{ | |
if (dynamicSound != null) | |
{ | |
dynamicSound.Stop(); | |
} | |
} | |
public void Stop() | |
{ | |
if (dynamicSound != null && !dynamicSound.IsDisposed) | |
{ | |
dynamicSound.Stop(); | |
Dispose(); | |
} | |
dynamicSound = null; | |
} | |
public void SetPosition(float milliseconds) | |
{ | |
position = (int)Math.Floor(milliseconds * bytesOverMilliseconds); | |
while (position % 8 != 0) position -= 1; | |
} | |
public float GetPosition() | |
{ | |
return position / bytesOverMilliseconds; | |
} | |
// Private | |
private void UpdateBuffer(object sender, EventArgs e) | |
{ | |
if (dynamicSound == null || position > byteArray.Length) QueueStop = true; | |
if (QueueStop) return; | |
dynamicSound.SubmitBuffer(byteArray, position, count / 2); | |
dynamicSound.SubmitBuffer(byteArray, position + count / 2, count / 2); | |
position += count; | |
if ((loopEndBytes > 0) && (loopLengthBytes > 0) && (position + count >= loopEndBytes)) | |
{ | |
if (IsLooped) position -= loopLengthBytes; | |
else QueueStop = true; | |
} | |
if (position + count > byteArray.Length) | |
{ | |
if (IsLooped) position = 0; | |
else QueueStop = true; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment