-
Notifications
You must be signed in to change notification settings - Fork 1
Algorithms
Random Music Generation technically consists of three parts (with some exceptions when talking about chords, staccatos...), which I've called Random Pitch Generation (RPG), Random Rythm Generation (RRG) and Random Dynamics Generation (RDG).
I've decided to take a look at all three of these types of algorithms separately to get the best result (whereas the RDG is seen as an additional extension and will thus not be supported for now).
Before the algorithms are discussed, please be aware that rests use a completely different
way of being generated. It uses generation.rest-ratio value (which is a decimal between
0 and 1) in the config file and it tries to change that percentage of notes to rests.
Not all notes can be changed, in order to prevent inaccurate and terrible-sounding music. These are the exceptions:
- Notes that already have been turned into a rest
- Notes that are tied in any way whatsoever.
These exceptions also disallow possible infinite loops.
The general idea behind RPG is basically an extension on the normal RNGs. After doing
some research, I've come up with the following algorithms for this generation.
For simplicity's sake, we'll suppose there are the following functions, assuming [...] is a sequence of numbers (array,
sequential pointers, vector...) and {...} depicts a map or a dictionary of some sorts
(which maps a key uniquely to a value):
| Function Name | Description |
|---|---|
choose([...]) |
Pick a random value uniformly from the given list. |
choose({...}) |
Pick a random element from the given map, where an element is a key in the map and the corresponding value the chance (between 0 and 1) that this value is chosen. (e.g. weighted random selection) |
choose([...], dist, a, b, c=false) |
Pick a random value from the given list where the chance that a certain value is picked depends on the dist distribution function. a is the lowest possible value to be mapped and b the highest. The probability of the value at index x is a + (x - min)*(b - a)/(max - min), with min and max the minimum (index) and maximum (index) of the list. When c is true, we want to center the 0 in our list to the center of our distribution. |
gaussian([...], a=-3, b=3, c=false) |
Shorthand for choose([...], f_g, a, b, c), where f_g stands for the Gaussian distribution function. |
range(a, b) |
Create a sequential range between a and b. |
This algorithm that is used in the main bulk of the functions described above has the following pseudocode, according to this website.
randomWeight = choose(range(1, sumOfWeights))
for each item in array do
randomWeight = randomWeight - item.weight
if randomWeight <= 0 then
pick this item
fi
done
Besides the listing below, the styling and other config values are taken into a count, rendering this page as a simplification of what's actually going on.
The algorithms I've come up with (with pn the new pitch and po the
old/previous one) are:
This is maybe one of the easiest algorithms that we could use (with the exception of a semitone musical piece). Basically it generates a random note somewhere in the musical spectrum.
pn = choose(range(0, 127))
To use this algorithm, set the generation.pitch value to anything that does not appear
in this file; e.g. randomized.
As a more constrictive version of the Totally Random algorithm, this method generates
a random note somewhere in the spectrum of all notes that can be played on a piano.
pn = choose(range(21, 108))
To use this algorithm, set the generation.pitch value to random-piano.
Most music pieces don't make a lot of jumps. This algorithm, based upon the movement of small particles that are randomly bombarded by molecules of the surrounding medium, basically forces the jumps to be somewhere between three pitches above or below the currently played note. More info here.
pn = choose(range(-3, 3)) + po
Without loss of generality, the range of -3 to 3 can be customizable via the
generation.options.pitch.min and generation.options.pitch.max values.
To use this algorithm, set the generation.pitch value to brownian-motion.
As discussed here, 1/f Noise is a very special class of noise and recurs often in nature. Besides the strangely easy algorithm created by Richard F. Voss (see this), there are a few issues: a good selection of the sides of the dice, the mappings and the table to use.
In the implementation of the code, all possible values to be selected (based on the style and other settings) are distributed uniformly over 3 dice. The table we use is the same as on the page (the binary representation of all numbers in [0, 7]).
To use this algorithm, set the generation.pitch value to 1/f-noise.
Invented as a stepping-stone to the Gaussian Voicing implementation, this algorithm came to be. It basically says that the chance for each note chosen is the highest in the middle of our pitch range, playing the piece mostly around the central octave.
It uses the Gauss-curve of a standard normal distribution.
pn = gaussian(range(0, 127))
To use this algorithm, set the generation.pitch value to centralized.
Voicing generally is a technique most experienced composers and pianists use when writing a piece. It is the idea that the next note that will be played will most likely be close to the one you just played. This is commonly used in the two inversions of a simple chord.
Gaussian Voicing uses the Gauss-curve of a standard normal distribution to focus its center around the justly played note.
pn = gaussian(range(-po, 127-po), -3, 3, true) + po
Note: In the algorithm two shifts of the pitch are required. The first is to centralize
it around po, the second to bring it back to the valid range.
Note: Normal voicing can be obtained using the brownian-motion algorithm.
This algorithm is currently not available for any use.
To use this algorithm, set the generation.pitch value to gaussian-voicing.
There are a few ways to supply for accompaniment. In the scope of this project, we will only take a look at the following two.
A musical piece will sound way better when it's accompanied by some other ostinatonic
stave. Looking at the generation.options.pitch.schematic and the style.chord-progression
values, it will generate a non-random pitch.
The schematic is a string of a power-2 length (e.g. 1, 2, 4, 8...) containing a
sequence of As, Bs and/or Cs. An A means the bottom note needs to be used, a B
says it'll use the chord's middle and C uses the top note. The B takes minor chords
into a count, meaning that any chord-progression may contain a minor chord (e.g. Em).
Note: This algorithm actually does not use any randomization.
To use this algorithm, set the generation.pitch value to accompaniment and set
the generation.options.pitch.schematic and style.chord-progression values
respectively to the required values.
Instead of wondering about the given schematics and progression, it might also be a good idea to look at which notes are played simultaneously with the to-be-generated note.
Based upon this article,
the representation of a random chord that's played at this point in time will be
computed and joined with a random A, B or C value as pseudo-schematic in
order to compute a good note to accompany the chosen chord.
To use this algorithm, set the generation.pitch value to accompaniment, but
keep the generation.options.pitch.schematic and style.chord-progression values
empty.
pn will be the following pitch according to a machine-learned markov chain.
Depending on the trained data, this algorithm can take rests into a count.
To use this algorithm, set the generation.pitch value to markov-chain and
generation.options.pitch.chain to the trained chain file.
RRG differs from normal RNGs because instead of picking a random value from 0 to 1 and shifting it to fit a range, RRG will have to pick a value from the list below:
- 256th
- 128th
- 64th
- 32nd
- 16th
- eighth
- quarter
- half
- whole
- breve
- long
(Note: The values above are to be used when representing a rhythm!)
On top of that, rhythm in music is also determined by the beats per minute, which can change throughout the piece. For simplicity, we'll never change the BPM within the current scope of the project.
Each note is basically a fraction, respectively 1/256, 1/128, 1/64, 1/32,
1/16, 1/8, 1/4, 1/2, 1/1, 2/1, 4/1; or 2^(-n) where n goes from
-8 to 2; or even 2^(n-8) for n going from 0 to 10.
This concept allows for easy RRG, yielding in rhythm = pow(2, choose(range(0, 10))-8),
with respect to the above mentioned functions and pow the power function.
The set of RRG algorithms is notably smaller, but still allows for a customizable and acceptable experience.
All of the below listed algorithms take the generation.options.rhythm.smallest and
generation.options.rhythm.largest config values into a count. These describe respectively
the smallest and largest possible rhythm values.
Starting off easy, we can set the duration of all notes to be the same. This value is
based upon the generation.options.rhythm.duration config value, which defaults to a
quarter note.
To use this algorithm, set the generation.rhythm value to anything that does not appear
in this file; e.g. constant.
Again, we can generate completely random rhythms by doing
rhythm = pow(2, choose(range(0, 10))-8)
But, in order to give the user more comfort in specializing the score, instead of
0 and 10, it will take a look at the generation.options.rhythm.smallest and
generation.options.rhythm.largest config values if they exist.
To use this algorithm, set the generation.rhythm value to random.
As discussed for the RPG, Brownian Motion can be used for a lit of different aspects,
therefore it would make sense to use it also for RRG. This time looking at the
generation.options.rhythm.min and generation.options.rhythm.max values, this algorithm
creates better music than the above-mentioned ones.
To use this algorithm, set the generation.rhythm value to brownian-motion.
Similar as with the Brownian Motion, 1/f Noise can be used in a number of contexts. Personally, I really like the rhythms that are produced by this algorithm.
Note: You can always set the generation.options.rhythm.smallest and
generation.options.rhythm.largest values to get a cleaner result!
To use this algorithm, set the generation.rhythm value to 1/f-noise.
Again, Markov Chains can be used to generate good rhythms.
To use this algorithm, set the generation.rhythm value to markov-chain and
generation.options.rhythm.chain to the trained chain file.