Modding talk:Audio

From Stardew Valley Wiki
Revision as of 04:30, 17 October 2021 by Pathoschild (talk | contribs) (→‎Track list export: rework code a bit, add category info)
Jump to navigation Jump to search

Track list export

The initial track list was exported with this quick code, which can be run as a C# mod:

source code 
/// <summary>The main entry point for the mod.</summary>
public class ModEntry : Mod
{
    /*********
    ** Public methods
    *********/
    /// <inheritdoc />
    public override void Entry(IModHelper helper)
    {
        helper.Events.GameLoop.GameLaunched += (_, _) =>
        {
            StringBuilder output = new();

            foreach (var categoryGroup in this.GetTracks().GroupBy(p => p.Category).OrderBy(p => p.Key))
            {
                output.AppendLine($"==={categoryGroup.Key}===");
                output.AppendLine("{| class=\"wikitable sortable\"");
                output.AppendLine("|-\n! name\n! soundbank ID\n! description");

                foreach (SoundInfo sound in categoryGroup.OrderBy(p => p.Name).ThenBy(p => p.Id))
                {
                    string soundIdLabel = sound.Id.ToString("X").ToLower().PadLeft(8, '0');

                    output.AppendLine($"|-\n| <tt>{sound.Name}</tt>\n| data-sort-value=\"{sound.Id}\"| <tt>{soundIdLabel}</tt>\n| ");
                }

                output.AppendLine("|}");
                output.AppendLine();
            }

            string result = output.ToString();
            this.Monitor.Log(result, LogLevel.Info);
        };
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Extract the music/sound tracks from the game's soundbank.</summary>
    private IEnumerable<SoundInfo> GetTracks()
    {
        SoundBank soundBank = this.Helper.Reflection.GetField<SoundBank>(Game1.soundBank, "soundBank").GetValue();
        Dictionary<string, CueDefinition> cues = this.Helper.Reflection.GetField<Dictionary<string, CueDefinition>>(soundBank, "_cues").GetValue();

        foreach (var entry in cues)
        {
            foreach (XactSoundBankSound sound in entry.Value.sounds)
            {
                string name = entry.Key;
                string category = this.GetCategoryName(sound.categoryID);

                // simple sound
                if (!sound.complexSound)
                {
                    using SoundEffectInstance instance = sound.GetSimpleSoundInstance();

                    yield return new SoundInfo
                    {
                        Category = category,
                        Name = name,
                        Id = sound.trackIndex
                    };
                    continue;
                }

                // complex sound
                bool hasVariants = false;
                if (sound.soundClips != null)
                {
                    foreach (XactClip clip in sound.soundClips)
                    {
                        foreach (ClipEvent rawClipEvent in clip.clipEvents)
                        {
                            if (rawClipEvent is not PlayWaveEvent clipEvent)
                            {
                                this.Monitor.Log($"Unexpected clip event type '{rawClipEvent.GetType().FullName}'.", LogLevel.Error);
                                continue;
                            }

                            foreach (PlayWaveVariant variant in clipEvent.GetVariants())
                            {
                                hasVariants = true;

                                using SoundEffect effect = variant.GetSoundEffect();

                                yield return new SoundInfo
                                {
                                    Category = category,
                                    Name = name,
                                    Id = variant.track
                                };
                            }
                        }
                    }
                }

                // invalid sound, should never happen
                if (!hasVariants)
                    this.Monitor.Log($"Complex sound '{name}' unexpectedly has no variants.", LogLevel.Error);
            }
        }
    }

    /// <summary>Get a human-readable name for a raw audio category ID.</summary>
    /// <param name="categoryId">The raw category ID.</param>
    private string GetCategoryName(uint categoryId)
    {
        // the categories seem to be arbitrarily defined for Stardew Valley;
        // these are approximate labels based on the tracks in each group.
        return categoryId switch
        {
            2 => "Music",
            3 => "Sound",
            4 => "Music (ambient)",
            5 => "Footsteps",
            _ => categoryId.ToString()
        };
    }
}

public class SoundInfo
{
    public string Category { get; set; }
    public string Name { get; set; }
    public int Id { get; set; }
}

The game data only has the numerical category IDs, but I confirmed the category names with one of the game developers:

id name my notes
1 Default seems to be unused.
2 Music
3 Sound
4 Ambient listed on the wiki as "Music (ambient)" just to group the music tracks.
5 Footsteps

Pathoschild (talk) 04:30, 17 October 2021 (UTC)