Skip to content

Instantly share code, notes, and snippets.

@ZachIsAGardner
Created January 29, 2024 14:13
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 ZachIsAGardner/87d34f881dcb60c917e298657e2dfb32 to your computer and use it in GitHub Desktop.
Save ZachIsAGardner/87d34f881dcb60c917e298657e2dfb32 to your computer and use it in GitHub Desktop.
ProgressiveSound
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);
}
}
}
}
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;
}
}
}
}
}
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