Last active
September 12, 2024 03:06
-
-
Save n3rdopolis/56beeb16afa6cc6f9f0b889f4fbb9b02 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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