Skip to content

Instantly share code, notes, and snippets.

@n3rdopolis
Last active September 12, 2024 03:06
Show Gist options
  • Save n3rdopolis/56beeb16afa6cc6f9f0b889f4fbb9b02 to your computer and use it in GitHub Desktop.
Save n3rdopolis/56beeb16afa6cc6f9f0b889f4fbb9b02 to your computer and use it in GitHub Desktop.
#! /usr/bin/perl
#This makes two files in $ShrineMusicOutputFolder, BotwShrine_Blessing.wav and BotwShrine_Standard.wav that are both $RuntimeSeconds long
#This uses files to generate really random shrine music as heard in Breath of the Wild with the actual rules https://www.youtube.com/watch?v=6logXysLpNk for all the instruments
#BGM_Dungeon_Arp_*.bfwav.wav (8), BGM_Dungeon_Bass_*.bfwav.wav (16), BGM_Dungeon_Bell_*.bfwav.wav (2), BGM_Dungeon_Lead_*.bfwav.wav, BGM_Dungeon_Piano_*.bfwav.wav (8), BGM_Dungeon_Strings_*.bfwav.wav (4)
#must all be under $ShrineMusicFileFolder
#The folder must contain all the files from DungeonBgm, DungeonNormalBgm, and DungeonRewardBgm (probably converted to .wav)
$ShrineMusicFileFolder = "/Path/To/DungeonAll/";
$ShrineMusicOutputFolder = "/tmp";
$RuntimeSeconds = 3600;
#Need this for the floor() and ceil() functions to always round decimal numbers down or always round up
use POSIX;
#Need this for the shuffle() function
use List::Util qw/shuffle/;
#Every measure is 9.729729 seconds long
$MeasureLength = 9.729729;
$TotalMeasureCount = ceil($RuntimeSeconds / $MeasureLength);
#The random number should be a non-decimal
sub GetRandomNumber
{
return floor(rand(0xFFFFFFFF));
}
sub GetRandomNumberBetween
{
my $RandomFrom = $_[0];
my $RandomTo = $_[1];
my $RandomSeed = GetRandomNumber();
my $Range = ($RandomTo - $RandomFrom) + 1;
my $RandomNumber = ($RandomSeed % $Range) + $RandomFrom;
return $RandomNumber;
}
sub GetRandomNumberBetweenWithExclude
{
my $RandomFrom = $_[0];
my $RandomTo = $_[1];
my $ExcludeValue = $_[2];
my $RandomNumber = GetRandomNumberBetween($RandomFrom,$RandomTo);
while ($RandomNumber == $ExcludeValue)
{
$RandomNumber = GetRandomNumberBetween($RandomFrom,$RandomTo);
}
return $RandomNumber;
}
sub GetDirectoryFileMatch
{
my $DirectoryName = $_[0];
my $DirectoryMatch = $_[1];
opendir(DirectoryHandle, $DirectoryName);
@DirectoryFiles = grep(/${DirectoryMatch}/,readdir(DirectoryHandle));
closedir(DirectoryHandle);
return @DirectoryFiles;
}
sub CreateNewInstrument
{
my $InstrumentName = $_[0];
my $MaximumConsecutiveSameVaration = $_[1];
@InstrumentFiles = GetDirectoryFileMatch($ShrineMusicFileFolder,"_${InstrumentName}_");
@InstrumentFilesSorted = [sort(@InstrumentFiles)];
$InstrumentFilesArrayMax = scalar(@InstrumentFiles) - 1;
%Instrument = ( Name => $InstrumentName,
InstrumentFiles => @InstrumentFilesSorted,
InstrumentFilesArrayMax => $InstrumentFilesArrayMax,
ConsecutiveMeasuresPresent => 0,
ConsecutiveMeasuresMissing => 0,
MaximumConsecutiveSameVaration => $MaximumConsecutiveSameVaration,
ConsecutiveMeasuresSameVaration => 0,
PreviousVaration => -1,
);
return %Instrument;
}
%InstrumentArp = CreateNewInstrument("Arp", 1);
%InstrumentBass = CreateNewInstrument("Bass", 1);
%InstrumentBell = CreateNewInstrument("Bell", 4);
%InstrumentLead = CreateNewInstrument("Lead", 2);
%InstrumentPiano = CreateNewInstrument("Piano", 3); #The same Piano sound played 3 times in a row in https://www.youtube.com/watch?v=LFXgY7p99SA
%InstrumentStrings = CreateNewInstrument("Strings", 1);
@ShrineInstrumentsArray = ();
$InstrumentArpIndex = push(@ShrineInstrumentsArray, \%InstrumentArp) - 1;
$InstrumentBassIndex = push(@ShrineInstrumentsArray, \%InstrumentBass) - 1;
$InstrumentBellIndex = push(@ShrineInstrumentsArray, \%InstrumentBell) - 1;
$InstrumentLeadIndex = push(@ShrineInstrumentsArray, \%InstrumentLead) - 1;
$InstrumentPianoIndex = push(@ShrineInstrumentsArray, \%InstrumentPiano) - 1;
$InstrumentStringsIndex = push(@ShrineInstrumentsArray, \%InstrumentStrings) - 1;
$ShrineInstrumentsArrayMax = scalar(@ShrineInstrumentsArray) - 1;
@ShrineModes = ("Standard", "Blessing");
foreach (@ShrineModes)
{
$ShrineMode = $_;
$ShrineMusicWorkFolder = "/tmp/botwshrinework/${ShrineMode}";
#Create the temporary folder
system("mkdir","-p","$ShrineMusicWorkFolder");
if ($ShrineMode eq "Standard")
{
$FFMpegDuration = "longest";
$MinimumInstruments = 2;
$MaximumInstruments = 4;
@ShrineInstruments = ($InstrumentBellIndex, $InstrumentArpIndex, $InstrumentBassIndex, $InstrumentStringsIndex, $InstrumentLeadIndex);
} elsif ($ShrineMode eq "Blessing")
{
#The piano files are all ~12 seconds long, the rest are ~9 seconds, and it doesn't seem like there are 3 more seconds of no sound in blessing shrines
$FFMpegDuration = "shortest";
$MinimumInstruments = 2;
$MaximumInstruments = 3;
@ShrineInstruments = ($InstrumentBellIndex, $InstrumentPianoIndex, $InstrumentStringsIndex);
}
$LastMeasureInstrumentsCount = 0;
$MeasuresWithConsecutiveInstrumentCount = 0;
@ShrineMeasuresSoxArguments = ();
for ($MeasureNumber = 0; $MeasureNumber <= $TotalMeasureCount; $MeasureNumber++)
{
@ShrineInstrumentSoxArguments = ();
$MeasureMinimumInstruments = $MinimumInstruments;
$MeasureMaximumInstruments = $MaximumInstruments;
@MeasureShrineInstruments = @ShrineInstruments;
if ($ShrineMode eq "Standard")
{
#If there were 4 or more channels in the last measure, the 5th one (The lead) is potentially eligable. (Always the lead)
if ($LastMeasureInstrumentsCount >= 4)
{
$MeasureMaximumInstruments = 5;
}
#If 4 measures had the lead in a row, then it is no longer eligible, it can only play 4 times in a row
if ($LastMeasureInstrumentsCount == 5 && $MeasuresWithConsecutiveInstrumentCount >= 4)
{
$MeasureMaximumInstruments = 4;
}
#https://www.youtube.com/watch?v=6logXysLpNk says the "Bell and Arp can play 'a maximum of 3 sequences',
#However, it also says the Arp can play 'a maximum of 20 times in sequence.
#And that the bell always plays, and that if there are two channels, it is always the bell and the Arp.
#Assuming they meant that it is a maximum of three times that they are the ONLY instruments playing.
if ($LastMeasureInstrumentsCount == 2 && $MeasuresWithConsecutiveInstrumentCount >= 3)
{
$MeasureMinimumInstruments = 3;
}
}
#Determine the number of instruments in this measure
$MeasureInstrumentsCount = GetRandomNumberBetween($MeasureMinimumInstruments, $MeasureMaximumInstruments);
if ($ShrineMode eq "Standard")
{
#Standard shrines, if there are more than 2 instruments playing, it can be more than the Bell and the Arp, so randomize the order of the Strings, the Arp, and the Bass if there are more than 2.
#If there are 5 exactly, don't randomize the order, it has no impact on the results, other than using more processing that is needed
if ($MeasureInstrumentsCount > 2 && $MeasureInstrumentsCount < 5 && $InstrumentArp{"ConsecutiveMeasuresPresent"} < 20)
{
@MeasureShrineInstruments = ($InstrumentBellIndex, (shuffle($InstrumentArpIndex, $InstrumentBassIndex, $InstrumentStringsIndex), $InstrumentLeadIndex));
}
#The Arp can't be missing more than one round
if ($InstrumentArp{"ConsecutiveMeasuresMissing"} > 0)
{
@MeasureShrineInstruments = ($InstrumentBellIndex, $InstrumentArpIndex, (shuffle($InstrumentBassIndex, $InstrumentStringsIndex), $InstrumentLeadIndex));
}
#The Arp can only play at most 20 times in a row
#Only if 2 other instruments are playing can the Arp not play
#It has to be the Bass and the Strings (and not the Lead, because the Lead can only play if all are playing
if ($InstrumentArp{"ConsecutiveMeasuresPresent"} >= 20)
{
$MeasureInstrumentsCount = 3;
@MeasureShrineInstruments = ($InstrumentBellIndex, $InstrumentBassIndex, $InstrumentStringsIndex, $InstrumentArpIndex, $InstrumentLeadIndex);
}
}
#Go through all the instruments
print(" Shrine Mode: ", $ShrineMode, ", Measure ", $MeasureNumber, " of ", $TotalMeasureCount, " has ", $MeasureInstrumentsCount, " Instruments:\n");
for ($InstrumentIterator = 0; $InstrumentIterator <= $#MeasureShrineInstruments; $InstrumentIterator++)
{
$InstrumentSelector = @MeasureShrineInstruments[$InstrumentIterator];
$ExcludedVariant = -1;
$SelectedVariant = -1;
#Handle the instruments if they are to be played or not
if ($InstrumentIterator < $MeasureInstrumentsCount)
{
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresPresent"} += 1;
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresMissing"} = 0;
if ($ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresSameVaration"} >= $ShrineInstrumentsArray[$InstrumentSelector]{"MaximumConsecutiveSameVaration"})
{
$ExcludedVariant = $ShrineInstrumentsArray[$InstrumentSelector]{"PreviousVaration"};
}
$SelectedVariant = GetRandomNumberBetweenWithExclude(0, $ShrineInstrumentsArray[$InstrumentSelector]{"InstrumentFilesArrayMax"}, $ExcludedVariant);
if ($SelectedVariant == $ShrineInstrumentsArray[$InstrumentSelector]{"PreviousVaration"})
{
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresSameVaration"} += 1;
} else {
$ShrineInstrumentsArray[$InstrumentSelector]{"PreviousVaration"} = $SelectedVariant;
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresSameVaration"} = 1;
}
$InstrumentVariantFileName = $ShrineInstrumentsArray[$InstrumentSelector]{"InstrumentFiles"}[$SelectedVariant];
$InstrumentVariantFilePath = "${ShrineMusicFileFolder}/${InstrumentVariantFileName}";
print(" Play ", $ShrineInstrumentsArray[$InstrumentSelector]{"Name"}, " Variant: ", $SelectedVariant, " File ", $InstrumentVariantFilePath, "\n");
push(@ShrineInstrumentSoxArguments, $InstrumentVariantFilePath);
} else {
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresPresent"} = 0;
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresMissing"} += 1;
$ShrineInstrumentsArray[$InstrumentSelector]{"ConsecutiveMeasuresSameVaration"} = 0;
$ShrineInstrumentsArray[$InstrumentSelector]{"PreviousVaration"} = -1;
}
}
if ($MeasureInstrumentsCount == $LastMeasureInstruments)
{
$MeasuresWithConsecutiveInstrumentCount++;
} else {
$MeasuresWithConsecutiveInstrumentCount = 0;
}
$LastMeasureInstrumentsCount = $MeasureInstrumentsCount;
#Save the name of the generated file, and generate the file for the current measure
print(" Generating ", "${ShrineMusicWorkFolder}/measure_${MeasureNumber}.wav", "\n");
system("sox", "-v", "1", "--multi-threaded", "-m", @ShrineInstrumentSoxArguments, "${ShrineMusicWorkFolder}/measure_${MeasureNumber}.wav", "trim", "0", "$MeasureLength");
push(@ShrineMeasuresSoxArguments, "${ShrineMusicWorkFolder}/measure_${MeasureNumber}.wav");
}
#Use the list of measure files, generate the final file
print("Generating file ", "${ShrineMusicOutputFolder}/BotwShrine_${ShrineMode}.wav", " of ", $TotalMeasureCount, " files in ", $ShrineMusicWorkFolder, "\n");
system("sox", @ShrineMeasuresSoxArguments, "${ShrineMusicOutputFolder}/BotwShrine_${ShrineMode}.wav");
#Clean up the measure files
unlink(glob("${ShrineMusicWorkFolder}/measure_*.wav"));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment