Difference between revisions of "Modding talk:Audio"

From Stardew Valley Wiki
Jump to navigation Jump to search
(→‎Track list export: rework code a bit, add category info)
m (Text replacement - "tt>" to "samp>")
 
(2 intermediate revisions by one other user not shown)
Line 1: Line 1:
 
==Track list export==
 
==Track list export==
The initial track list was exported with this quick code, which can be run as a [[Modding:Modder Guide/Get Started|C# mod]]:
+
The initial track list was exported with this code, which can be run as a [[Modding:Modder Guide/Get Started|C# mod]] in Stardew Valley 1.5.5+:
 
{{collapse|source code|content=<syntaxhighlight lang="c#">
 
{{collapse|source code|content=<syntaxhighlight lang="c#">
 
/// <summary>The main entry point for the mod.</summary>
 
/// <summary>The main entry point for the mod.</summary>
Line 15: Line 15:
 
             StringBuilder output = new();
 
             StringBuilder output = new();
  
             foreach (var categoryGroup in this.GetTracks().GroupBy(p => p.Category).OrderBy(p => p.Key))
+
             foreach (var categoryGroup in this.GetTracks().GroupBy(p => p.GetCategoryName()).OrderBy(p => p.Key))
 
             {
 
             {
 
                 output.AppendLine($"==={categoryGroup.Key}===");
 
                 output.AppendLine($"==={categoryGroup.Key}===");
 
                 output.AppendLine("{| class=\"wikitable sortable\"");
 
                 output.AppendLine("{| class=\"wikitable sortable\"");
                 output.AppendLine("|-\n! name\n! soundbank ID\n! description");
+
                 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 (SoundInfo sound in categoryGroup.OrderBy(p => p.Name).ThenBy(p => p.Id))
+
                 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| ");
                    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("|}");
Line 42: Line 39:
 
     *********/
 
     *********/
 
     /// <summary>Extract the music/sound tracks from the game's soundbank.</summary>
 
     /// <summary>Extract the music/sound tracks from the game's soundbank.</summary>
     private IEnumerable<SoundInfo> GetTracks()
+
     private IEnumerable<TrackInfo> GetTracks()
 
     {
 
     {
 
         SoundBank soundBank = this.Helper.Reflection.GetField<SoundBank>(Game1.soundBank, "soundBank").GetValue();
 
         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();
+
         IEnumerable<CueDefinition> cues = this.Helper.Reflection.GetField<Dictionary<string, CueDefinition>>(soundBank, "_cues").GetValue().Values;
  
         foreach (var entry in cues)
+
         foreach (CueDefinition cue in cues)
 
         {
 
         {
             foreach (XactSoundBankSound sound in entry.Value.sounds)
+
             foreach (XactSoundBankSound sound in cue.sounds)
 
             {
 
             {
                string name = entry.Key;
 
                string category = this.GetCategoryName(sound.categoryID);
 
 
 
                 // simple sound
 
                 // simple sound
 
                 if (!sound.complexSound)
 
                 if (!sound.complexSound)
 
                 {
 
                 {
                    using SoundEffectInstance instance = sound.GetSimpleSoundInstance();
+
                     yield return new TrackInfo(
 
+
                        WavebankIndex: sound.waveBankIndex,
                     yield return new SoundInfo
+
                         CategoryId: sound.categoryID,
                    {
+
                         Name: cue.name,
                         Category = category,
+
                         Index: sound.trackIndex
                         Name = name,
+
                     );
                         Id = sound.trackIndex
 
                     };
 
 
                     continue;
 
                     continue;
 
                 }
 
                 }
Line 86: Line 78:
 
                                 hasVariants = true;
 
                                 hasVariants = true;
  
                                using SoundEffect effect = variant.GetSoundEffect();
+
                                 yield return new TrackInfo(
 
+
                                    WavebankIndex: variant.waveBank,
                                 yield return new SoundInfo
+
                                     CategoryId: sound.categoryID,
                                {
+
                                     Name: cue.name,
                                     Category = category,
+
                                     Index: variant.track
                                     Name = name,
+
                                 );
                                     Id = variant.track
 
                                 };
 
 
                             }
 
                             }
 
                         }
 
                         }
Line 101: Line 91:
 
                 // invalid sound, should never happen
 
                 // invalid sound, should never happen
 
                 if (!hasVariants)
 
                 if (!hasVariants)
                     this.Monitor.Log($"Complex sound '{name}' unexpectedly has no variants.", LogLevel.Error);
+
                     this.Monitor.Log($"Complex sound '{cue.name}' unexpectedly has no variants.", LogLevel.Error);
 
             }
 
             }
 
         }
 
         }
 
     }
 
     }
 +
}
  
    /// <summary>Get a human-readable name for a raw audio category ID.</summary>
+
/// <summary>A sound or music track in a wavebank.</summary>
    /// <param name="categoryId">The raw category ID.</param>
+
/// <param name="WavebankIndex">The wavebank which contains the track.</param>
     private string GetCategoryName(uint categoryId)
+
/// <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()
 
     {
 
     {
         // the categories seem to be arbitrarily defined for Stardew Valley;
+
         return this.CategoryId switch
        // these are approximate labels based on the tracks in each group.
 
        return categoryId switch
 
 
         {
 
         {
 
             2 => "Music",
 
             2 => "Music",
Line 118: Line 113:
 
             4 => "Music (ambient)",
 
             4 => "Music (ambient)",
 
             5 => "Footsteps",
 
             5 => "Footsteps",
             _ => categoryId.ToString()
+
             _ => 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()
 
         };
 
         };
 
     }
 
     }
}
 
  
public class SoundInfo
+
    /// <summary>Get the hexadecimal soundbank ID which matches the filenames exported by unxwb.</summary>
{
+
     public string GetSoundbankId()
     public string Category { get; set; }
+
     {
     public string Name { get; set; }
+
        return this.Index
     public int Id { get; set; }
+
            .ToString("X")
 +
            .ToLower()
 +
            .PadLeft(8, '0');
 +
     }
 
}
 
}
 
</syntaxhighlight>}}
 
</syntaxhighlight>}}
  
The game data only has the numerical category IDs, but I confirmed the category names with one of the game developers:
+
The game data only has the numerical category IDs, but I confirmed the internal category names with one of the game developers:
  
 
{| class="wikitable"
 
{| class="wikitable"
Line 140: Line 148:
 
|-
 
|-
 
| 1
 
| 1
| <tt>Default</tt>
+
| <samp>Default</samp>
 
| seems to be unused.
 
| seems to be unused.
 
|-
 
|-
 
| 2
 
| 2
| <tt>Music</tt>
+
| <samp>Music</samp>
 
|  
 
|  
 
|-
 
|-
 
| 3
 
| 3
| <tt>Sound</tt>
+
| <samp>Sound</samp>
 
|  
 
|  
 
|-
 
|-
 
| 4
 
| 4
| <tt>Ambient</tt>
+
| <samp>Ambient</samp>
 
| listed on the wiki as "Music (ambient)" just to group the music tracks.
 
| listed on the wiki as "Music (ambient)" just to group the music tracks.
 
|-
 
|-
 
| 5
 
| 5
| <tt>Footsteps</tt>
+
| <samp>Footsteps</samp>
 
|  
 
|  
 
|}
 
|}
  
—<small>[[User:Pathoschild|Pathoschild]] ([[User talk:Pathoschild|talk]]) 04:30, 17 October 2021 (UTC)</small>
+
—<small>[[User:Pathoschild|Pathoschild]] ([[User talk:Pathoschild|talk]]) 05:21, 17 October 2021 (UTC)</small> (updated 00:12, 20 October 2021 (UTC))

Latest revision as of 18:48, 4 November 2021

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))