9public static class AudioDefinitionSerializer
11 private static readonly JsonSerializerSettings Settings =
new()
13 Formatting = Formatting.Indented,
14 NullValueHandling = NullValueHandling.Ignore,
15 DefaultValueHandling = DefaultValueHandling.Include,
16 MissingMemberHandling = MissingMemberHandling.Ignore
26 if (
string.IsNullOrWhiteSpace(filePath))
27 throw new ArgumentException(
"GSND file path must be a non-empty string.", nameof(filePath));
29 var fullPath = Path.GetFullPath(filePath);
30 if (!File.Exists(fullPath))
31 throw new FileNotFoundException($
"GSND file not found: {fullPath}", fullPath);
35 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
36 ApplyDefaultSource(definition, AudioDefinitionSource.LooseDefinitionFile(fullPath));
37 definition.LoadStamp =
new(definition, filePath: fullPath);
40 catch (JsonException ex)
42 throw new InvalidDataException($
"Failed to deserialize GSND file: {fullPath}", ex);
53 ArgumentNullException.ThrowIfNull(stream);
55 throw new ArgumentException(
"Stream must be readable.", nameof(stream));
57 using var reader =
new StreamReader(stream, leaveOpen:
true);
58 return FromJson(reader.ReadToEnd());
67 public static AudioDefinition Load(AssetsFile assetsFile,
string entryName)
69 ArgumentNullException.ThrowIfNull(assetsFile);
70 ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
72 using var stream = assetsFile.
Get(
AssetTypes.AudioDefinition, entryName)
73 ??
throw new FileNotFoundException($
"GSND asset entry not found: {entryName}", entryName);
75 var definition = Load(stream);
76 definition.Source = AudioDefinitionSource.None();
77 if (!
string.IsNullOrWhiteSpace(assetsFile.
SourcePath))
79 definition.Source = AudioDefinitionSource.PackedDefinitionFile(
84 definition.LoadStamp =
new(definition, assetsFilePath: assetsFile.
SourcePath, entryName: entryName);
95 if (
string.IsNullOrWhiteSpace(filePath))
96 throw new ArgumentException(
"GSND file path must be a non-empty string.", nameof(filePath));
98 ArgumentNullException.ThrowIfNull(definition);
100 var fullPath = Path.GetFullPath(filePath);
101 var directory = Path.GetDirectoryName(fullPath) ??
string.Empty;
102 if (!
string.IsNullOrWhiteSpace(directory))
103 Directory.CreateDirectory(directory);
105 var clone = FromJson(ToJson(definition));
106 RebaseReferences(clone, GetReferenceBaseDirectory(definition.Source), directory);
107 clone.Source = AudioDefinitionSource.LooseDefinitionFile(fullPath);
109 File.WriteAllText(fullPath, ToJson(clone));
117 public static void Save(
string filePath, AudioResourceManager manager)
119 ArgumentNullException.ThrowIfNull(manager);
120 Save(filePath, FromManager(manager));
130 ArgumentNullException.ThrowIfNull(definition);
131 return JsonConvert.SerializeObject(definition, Settings);
140 FromJson(json, sourceDescription:
null);
150 AudioResourceManager manager,
151 string? baseDirectory =
null,
152 bool makePathsRelative =
false)
154 ArgumentNullException.ThrowIfNull(manager);
158 Resources = manager.GetAll()
159 .OrderBy(pair => pair.Key, StringComparer.Ordinal)
160 .Select(pair => FromResource(pair.Value))
162 Source = AudioDefinitionSource.Generated()
165 if (makePathsRelative && !
string.IsNullOrWhiteSpace(baseDirectory))
166 RebaseReferences(definition,
null, baseDirectory);
176 public static AudioResourceDefinition FromResource(AudioResource resource)
178 ArgumentNullException.ThrowIfNull(resource);
180 var definition =
new AudioResourceDefinition
183 Volume = resource.Volume,
185 PlaybackSpeed = resource.PlaybackSpeed,
186 IsLooping = resource.IsLooping,
187 SourceExtension = resource.SourceExtension
190 if (resource.AssetIdentifier is { } asset)
193 definition.AssetsFilePath = asset.AssetsFile.SourcePath;
194 definition.AssetEntryName = asset.AssetName;
196 else if (!
string.IsNullOrWhiteSpace(resource.SourceFilePath))
199 definition.FilePath = resource.SourceFilePath;
201 else if (!
string.IsNullOrWhiteSpace(resource.SourceUri))
204 definition.SourceUri = resource.SourceUri;
208 throw new InvalidOperationException(
209 $
"AudioResource '{resource.Key}' does not have a persistable file, packed-asset, or URI source.");
222 public static IReadOnlyList<AudioResource> LoadIntoManager(
224 bool overwriteExisting =
true,
225 string? baseDirectory =
null)
227 ArgumentNullException.ThrowIfNull(definition);
229 var errors = AudioDefinitionValidator.Validate(definition);
230 if (errors.Count != 0)
232 throw new InvalidDataException(
233 "GSND definition is invalid:" + Environment.NewLine +
234 string.Join(Environment.NewLine, errors.Select(error =>
"- " + error)));
237 baseDirectory ??= GetReferenceBaseDirectory(definition.Source);
238 var manager = AudioResourceManager.Instance;
239 var loaded =
new List<AudioResource>();
240 bool skippedExisting =
false;
242 foreach (var resourceDefinition
in definition.Resources)
244 if (manager.TryGet(resourceDefinition.Key, out var existing) && existing is not
null)
246 if (!overwriteExisting)
248 skippedExisting =
true;
249 ApplySettings(existing, resourceDefinition);
250 loaded.Add(existing);
254 manager.Unload(resourceDefinition.Key);
257 AudioResource resource = resourceDefinition.SourceKind
switch
259 AudioResourceSourceKind.LooseFile =>
260 manager.LoadFromFile(
261 resourceDefinition.Key,
262 ResolveReferencePath(resourceDefinition.FilePath!, baseDirectory),
263 resourceDefinition.Volume,
264 resourceDefinition.Pan,
265 resourceDefinition.PlaybackSpeed),
267 AudioResourceSourceKind.Uri =>
269 resourceDefinition.Key,
270 resourceDefinition.SourceUri!,
271 resourceDefinition.Volume,
272 resourceDefinition.Pan,
273 resourceDefinition.PlaybackSpeed),
275 AudioResourceSourceKind.PackedAsset =>
276 LoadPackedResource(resourceDefinition, baseDirectory, manager),
278 _ =>
throw new InvalidDataException(
279 $
"Unknown GSND source kind '{resourceDefinition.SourceKind}'.")
282 resource.IsLooping = resourceDefinition.IsLooping;
283 loaded.Add(resource);
286 manager.DefinitionProvenance = !skippedExisting && manager.GetAll().Count == loaded.Count
287 ? definition.LoadStamp?.Materialized(definition,
288 () => FromManager(manager))
299 public static IReadOnlyList<AudioResource> LoadIntoManager(
301 bool overwriteExisting =
true) =>
305 Path.GetDirectoryName(Path.GetFullPath(filePath)));
307 private static AudioResource LoadPackedResource(
308 AudioResourceDefinition definition,
309 string? baseDirectory,
310 AudioResourceManager manager)
312 var assetsPath = ResolveReferencePath(definition.AssetsFilePath!, baseDirectory);
314 file => !
string.IsNullOrWhiteSpace(file.SourcePath) &&
string.Equals(
315 Path.GetFullPath(file.SourcePath),
316 Path.GetFullPath(assetsPath),
317 StringComparison.OrdinalIgnoreCase))
318 ??
throw new InvalidDataException(
319 $
"GSND resource '{definition.Key}' references assets file '{assetsPath}', but it is not loaded.");
321 using var stream = assetsFile.Get(
AssetTypes.Audio, definition.AssetEntryName!)
322 ??
throw new InvalidDataException(
323 $
"GSND resource '{definition.Key}' references missing audio entry '{definition.AssetEntryName}' in '{assetsPath}'.");
325 var extension = !
string.IsNullOrWhiteSpace(definition.SourceExtension)
326 ? definition.SourceExtension!
327 : Path.GetExtension(definition.AssetEntryName!);
329 var resource = manager.LoadFromStream(
335 definition.PlaybackSpeed);
337 resource.SetAssetIdentifier(
338 new AssetsFileIdentifier(
341 definition.AssetEntryName!));
346 private static void ApplySettings(
347 AudioResource resource,
348 AudioResourceDefinition definition)
350 resource.Volume = definition.Volume;
351 resource.Pan = definition.Pan;
352 resource.PlaybackSpeed = definition.PlaybackSpeed;
353 resource.IsLooping = definition.IsLooping;
358 string? sourceDescription)
360 if (
string.IsNullOrWhiteSpace(json))
361 throw new ArgumentException(
"GSND JSON content must be a non-empty string.", nameof(json));
365 var definition = JsonConvert.DeserializeObject<
AudioDefinition>(json, Settings);
366 if (definition is
null)
368 var source =
string.IsNullOrWhiteSpace(sourceDescription)
369 ?
"GSND JSON content"
371 throw new InvalidDataException($
"Failed to deserialize {source}. Result was null.");
374 definition.Resources ??= [];
375 for (
int i = 0; i < definition.Resources.Count; i++)
377 if (definition.Resources[i] is
null)
378 throw new InvalidDataException($
"GSND Resources cannot contain a null entry at index {i}.");
383 catch (JsonException ex)
385 var source =
string.IsNullOrWhiteSpace(sourceDescription)
386 ?
"GSND JSON content"
388 throw new InvalidDataException($
"Failed to deserialize {source}.", ex);
392 private static void ApplyDefaultSource(
394 AudioDefinitionSource source)
397 definition.Source = source;
400 private static string? GetReferenceBaseDirectory(AudioDefinitionSource source)
402 return source.Kind
switch
404 AudioDefinitionSourceKind.LooseDefinitionFile
405 when !
string.IsNullOrWhiteSpace(source.GsndFilePath) =>
406 Path.GetDirectoryName(Path.GetFullPath(source.GsndFilePath)),
408 AudioDefinitionSourceKind.PackedDefinitionFile
409 when !
string.IsNullOrWhiteSpace(source.AssetsFilePath) =>
410 Path.GetDirectoryName(Path.GetFullPath(source.AssetsFilePath)),
416 private static void RebaseReferences(
418 string? oldBaseDirectory,
419 string newBaseDirectory)
421 foreach (var resource
in definition.Resources)
423 switch (resource.SourceKind)
425 case AudioResourceSourceKind.LooseFile
426 when !
string.IsNullOrWhiteSpace(resource.FilePath):
427 resource.FilePath = MakeReferencePath(
428 ResolveReferencePath(resource.FilePath, oldBaseDirectory),
432 case AudioResourceSourceKind.PackedAsset
433 when !
string.IsNullOrWhiteSpace(resource.AssetsFilePath):
434 resource.AssetsFilePath = MakeReferencePath(
435 ResolveReferencePath(resource.AssetsFilePath, oldBaseDirectory),
442 private static string ResolveReferencePath(
444 string? baseDirectory)
446 if (Path.IsPathRooted(path))
447 return Path.GetFullPath(path);
449 return string.IsNullOrWhiteSpace(baseDirectory)
450 ? Path.GetFullPath(path)
451 : Path.GetFullPath(path, baseDirectory);
454 private static string MakeReferencePath(
456 string baseDirectory)
458 var fullPath = Path.GetFullPath(path);
459 var fullBase = Path.GetFullPath(baseDirectory);
461 Path.GetPathRoot(fullPath),
462 Path.GetPathRoot(fullBase),
463 StringComparison.OrdinalIgnoreCase))
468 return Path.GetRelativePath(fullBase, fullPath);
string? SourcePath
Gets the archive's reconstructible origin, including an optional stream origin.
static IReadOnlyList< AssetsFile > AllAssetsFiles
Gets a read-only list of registered AssetsFile instances.
Stream? Get(AssetTypes type, string name)
Retrieves a stream for the specified asset type and name.
AssetTypes
Defines the types of assets that can be stored and managed by the engine.
@ AudioDefinition
Represents an audio definition file (.gsnd) for Gondwana.Audio.GSND.AudioDefinition.
AudioResourceSourceKind
Identifies how a GSND audio resource locates its underlying media.
AudioDefinitionSourceKind
Identifies the provenance of an audio definition.