Modding talk:Audio

From Stardew Valley Wiki
Jump to navigation Jump to search

Track list export

The initial track list was exported with this code, which can be run as a C# mod in Stardew Valley 1.5.5+:

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.GetCategoryName()).OrderBy(p => p.Key))
            {
                output.AppendLine($"==={categoryGroup.Key}===");
                output.AppendLine("{| class=\"wikitable sortable\"");
                output.AppendLine("|-\n!rowspan=\"2\"| name\n!rowspan=\"2\"| wavebank\n!colspan=\"2\"| soundbank index\n!rowspan=\"2\"| description");
                output.AppendLine("|-\n! decimal\n! hexadecimal");

                foreach (TrackInfo track in categoryGroup.OrderBy(p => p.Name).ThenBy(p => p.Index))
                    output.AppendLine($"|-\n| <samp>{track.Name}</samp>\n| <samp>{track.GetWavebankName()}</samp>\n| <samp>{track.Index}</samp>\n| data-sort-value=\"{track.Index}\"| <samp>{track.GetSoundbankId()}</samp>\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<TrackInfo> GetTracks()
    {
        SoundBank soundBank = this.Helper.Reflection.GetField<SoundBank>(Game1.soundBank, "soundBank").GetValue();
        IEnumerable<CueDefinition> cues = this.Helper.Reflection.GetField<Dictionary<string, CueDefinition>>(soundBank, "_cues").GetValue().Values;

        foreach (CueDefinition cue in cues)
        {
            foreach (XactSoundBankSound sound in cue.sounds)
            {
                // simple sound
                if (!sound.complexSound)
                {
                    yield return new TrackInfo(
                        WavebankIndex: sound.waveBankIndex,
                        CategoryId: sound.categoryID,
                        Name: cue.name,
                        Index: 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;

                                yield return new TrackInfo(
                                    WavebankIndex: variant.waveBank,
                                    CategoryId: sound.categoryID,
                                    Name: cue.name,
                                    Index: variant.track
                                );
                            }
                        }
                    }
                }

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

/// <summary>A sound or music track in a wavebank.</summary>
/// <param name="WavebankIndex">The wavebank which contains the track.</param>
/// <param name="CategoryId">The sound or music category.</param>
/// <param name="Name">The sound name used in the game code.</param>
/// <param name="Index">The offset index in the raw soundbank.</param>
public record TrackInfo(int WavebankIndex, uint CategoryId, string Name, int Index)
{
    /// <summary>Get a human-readable name for the category ID.</summary>
    public string GetCategoryName()
    {
        return this.CategoryId switch
        {
            2 => "Music",
            3 => "Sound",
            4 => "Music (ambient)",
            5 => "Footsteps",
            _ => this.CategoryId.ToString()
        };
    }

    /// <summary>Get a human-readable for the wavebank index.</summary>
    public string GetWavebankName()
    {
        return this.WavebankIndex switch
        {
            0 => "Wavebank",
            1 => "Wavebank(1.4)",
            _ => this.WavebankIndex.ToString()
        };
    }

    /// <summary>Get the hexadecimal soundbank ID which matches the filenames exported by unxwb.</summary>
    public string GetSoundbankId()
    {
        return this.Index
            .ToString("X")
            .ToLower()
            .PadLeft(8, '0');
    }
}

The game data only has the numerical category IDs, but I confirmed the internal 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) 05:21, 17 October 2021 (UTC) (updated 00:12, 20 October 2021 (UTC))