SoundLife The algorithm is very simple; tones of eight different frequencies (0.45, 0.4, 0.375, 0.325, 0.25, 0.2, 0.125 and 0.1, according to the sounds library) are given to the program and one of these allocated to each row and column of the game world (assuming it is 8x8; if not the same tone will be used for more than one so that 8 (or 9) is the same as 0 (or 1) and so on). For each generation, a sound for the row and a sound for the column is added to the file (1 second per cell - this can be changed to 0.5 seconds if a livelier sound is preferred!).
For each generation of the world a 0.1s audio clip is created and added to the total audio sequence. This is done by creating an audioSequence object to hold the total audio sequence and then looping through the world passing each generation through an algorithm that creates a new audio clip on the end of the audioSequence object before advancing the audioSequence to the next clip. The audio clip is created by adding a new SineWaveSound for each live cell. Since the frequency of the sound has to be between 0 and 1 it is equal to the cell's distance from the centre calculated using pythagoras' theorem divided by the maximum distance from the centre i.e. the distance of the corner from the centre.
DOCUMENTATION To create my implementation of SoundLife you should do the following: Create all the usual methods: setCell, getCell, computeCell, countNeighbours, nextGeneration. You should have a main method which initialises the world using the Pattern Class. This Pattern Class should be unchanged from tick4. Next, we need to create the audio file. Create a method called createAudio. This method should iterate through the specified number of generations. For each generation it should call a method called averageDistance. The method averageDistance should take the world and do the following: For every live cell it encounters it should work out it's distance from the centre of the board to the live cell. This can be done using pythagoras theorem and the java.lang.Math library for the square root method. It should create a running total of the sums of distances as it is iterating through the world. At the end it should divide the sum of the distances by the number of live cells to give the average distance from the center. This average distance should be returned to createAudio. Back in createAudio the method should create a new Sound using the ratio of the averageDistance to the greatest possible distance it should be. This ratio can be used to give you a frequency for the new Sound. The greater the average distance the higher the pitch of the sound. Then a new Sound block should be created on the audio file for the next generation. Finally the soundFile should be written to disk.
The program mainly reuses components programmed in tick 6 or before (pattern parsing and world implementation). The only new part is the generation of the audio file. Documentation for the audio library used can be found at http://www.cl.cam.ac.uk/teaching/0809/ProgJava/sounddocs/, the specification for the program is found at http://www.cl.cam.ac.uk/teaching/0809/ProgJava/workbook6star.html. The program expects the following command line options: 1. pattern 2. number of generations 3. name of the output file There is a optional argument: 4. length of timeslot in seconds Each timeslot is created in the following way: The amplitude of all sine waves in the current timeslot is 0.2 (normalization factor) * (double) world.getPopulation() / (world.getWidth() * world.getHeight()) So the volume is proportional to the number of living cells. For each living cell, one sine wave is added with the following frequency: 1.0 - ((double)y) / world.getHeight() So cells in the upper part of the world correspond to high sine waves and vice versa.
This program generates the SHA-1 hash of every generation and from that calculates an effectively pseudo-random frequency (evenly distributed between 200 and 3000 Hz) and duration (geometrically distributed between 2 and 10). The same pattern on the same field-dimensions will always generate the same note of same length. This implementation uses HashWorld as an subclass of WorldImpl to represent the state of the field. It therefore implements the World-interface but also provides the function hashContent which generates the SHA-1 hash of the serialized (PackedLong style) bit-pattern of the field. The play() method in SoundLife calls this function for every generation and generates a frequency as well as a duration from that hash that are saved in a container class. This container is stored in a list; for every new timeslot, every tone from that list is played and its duration-counter decreased; if a duration reaches 0, the list-entry is removed. The submitted wav-file is the output when called with pattern "An allegory of life:M Binder (2009):1:1:0:0:1" that contains one living cell on a 1x1 field. It represents life and struggle against futility. During the first generation, the cell (human being) is alive and makes up the whole world - just as the note representing the human is the only one playing. Then, suddenly, the first generation ends and the human disappears from the world; another note, representing eternity, is played, but still the first note can be heard: The human may be dead, but he can still be remembered by others for his accomplishments. Then, however, that note stops as apruptly as the human was torn out of life; all the change he made has been forgotten, what remains is eternity.
My program uses the second approach. It contains a new implementation of the Sound interface which is capable to load samples from WAV files (Mono, 16Bit, 8kHz) and play them whenever a certain cell in the game of life is alive. To do this, the program needs an additional input file, a so called "Sound Map File". This file contains h lines and w rows for a hxw game of life world, each character representing a sample associated with that particular cell. If the character is '.', this means that no sound is associated with that cell, i.e. no sound is played, independently from whether the cell is alive or not. Every other character (e.g. 'c') represents a sample saved within the current working directory (e.g. "c.wav"). A map for a 5x5 game of life could look as follows: ..... ..a.. .1b1. ..0.. ..... This world would use the samples a.wav, b.wav, 0.wav and 1.wav. Note that the SoundMap might also be smaller than the actual game of life world. In this case it is used to describe the top left area of the world. The program has to be called using the name of the SoundMap as additional parameter. If it is missing, the program will still run, but it will assume an empty SoundMap, hence resulting in an empty audio file (frankly, this is just to pass the Unit Tests). Obviously, you could use this program to create proper music. I created a small tune to demonstrate how you could create progressions of different notes blending into each other using a Glider. My example also shows how to use a 2-phase oscillator to create a simple drumkit. The command to create the example was: java uk.ac.cam.mcm57.tick6star.SoundLife "Drums:Martin Maas:40:40:1:1:010 010 010 000 000 010 001 111" 60 competition.wav soundmap.txt All necessary files and samples can be found in the directory "example" within the jar file. The drum samples were downloaded from iBeat.org whereas the synth samples were created using Apple GarageBand and a Software Synthesizer.
Game of Life Sound Generator ============================ - CLI: java -jar package.jar "PATTERN" NUMBER_OF_GENERATIONS FILE_TO_WRITE - The following pattern format is to be supported: NAME:AUTHOR:WIDTH:HEIGHT:STARTX:STARTY:CELLS Where: STARTX and STARTY are the offset of CELLS within initialised world CELLS are the list of cells,i.e 000 010 001 means 3x3 grid with one live cell in the middle and in the bottom-right corner - Implementation should provide helpful error messages if the input doesn't confront to the format above. - Implementation should use standard Game of Life rules - Implementation should generate sounds according to the following algorithm: Traverse each of NUMBER_OF_GENERATIONS generations in given world and for any live cell add a sine wave with frequency scale x+y/WIDTH+HEIGHT, where x and y are co-ordinates of that cell and amplitude scale 0.2
Notes for Java Tick 6* Michael J.A. Clark (mjac4) Main components: - Modified Tick 5* to gather population statistics - Custom Sound implementation to take raw frequencies instead of a real in the range 0.0 to 1.0 - Complicated algorithm for determining musical phrases to play Custom SineWave implementation: - Changed constructor to take a frequency instead of a real Algorithm information: - Based around middle C (but this is customisable, centred on 110 * 2^(2 + 0/12) Hz by mathematics) - Traverses through different modes depending on the growth rate -- the growth rate is normalised and then centred on the major mode -- if many cells die then the tone gets sadder -- if many cells grow then the tone gets happier -- goes through the modes in this order --- Locrian --- Aeolian --- Dorian --- Phrygian --- Ionian (standard C major, distribution centred here) --- Lydian --- Dorian -- this bit is complicated and takes some thinking about - Notes determined by occurrences of live cells in square perimeters around the centre of the - Octave of each note determined by the skewness of these square perimeters, max note is the B above middle C - Amplitude of each note determined by the frequency of live cells in the square perimeter compared to the maximum population (calculated using the stats class) Potential improvements: - I want to use samples instead of sine waves - Split separated patterns into different instruments and centre on their centre of living mass - This'd produce a more relevant sound for patterns which move across the screen - Use an infinite world to get a build up of glider sounds (glider gun) for instance - Have smaller a death bound for changing modes because it only seems to vary by one or two for most boards Competition entry: - Just the standard glider on a 20x20 board - The song tells the story of a lone glider who positively moves through life before encountering a rhino which scares him for a quaver, he then achieves enlightenment and dies (hits the edge of the map and turns into a constant sound) I started this at 11 pm on Wednesday night and finished 3 am on Thursday morning. Bumps tomorrow. Ahh... okay it is now 3:30. Must submit.
First, the number of living cells (n) in a world is counted - to prevent both SoundOverflowExceptions and the resulting sound being inaudiable due to a large proprtion of the world being dead, the ampiltude of all of the sine waves are 0.9/(n*2). n*2 is used because two sine waves per cell should be generated - one for the x-coordinate, and one with a higher frequency for the y-coordinate. The frequency of the x-coordinate wave should have a range of 0.1 and an offset of 0.1, and should be based on the proportion of the distance across the world the cell is: (x*(0.1/width))+0.1. Similarly, the y-coordinate should have a range of 0.1, but it should have an offset of 0.2 to keep the y-waves consistently higher in pitch than the x-waves: ((y*(0.1/height))+0.2). As you may have guessed by now, this system is fairly similar to the DTMF system used in phones (although with the y-waves higher in pitch/frequency than the x-waves, rather than the other way around): http://en.wikipedia.org/wiki/Dual-tone_multi-frequency. Other infmation and instructions for making the program can be found here (I used the code produced by the end of Tick 6): http://www.cl.cam.ac.uk/teaching/0809/ProgJava/ BTW, competition.wav was based on 40 generations of "LWSS:Life lexicon:11:10:3:3:01001 10000 10001 1111".
Modify agingWorld such that it has a makeSound method which when supplied a cell co-ordinate will return a bool array such that the first element is the pitch and second is the volume. The pitch is decided by (getCellAge(i, j)-1000)*100/(getGeneration()+1) and the volume by ((AgingWorld)world).makeSound(x,y)) Modify PatternLife.java such that its play method has 3 for loops, one which iterates generations, one which iterates y and one that iterates x. For each of these iterations call the method defined above and add this as a note to a Sound object with time 0.125. A new segment should be added after each cell and a second after each iteration.
Notes on SoundLife 1. This program requires the Gervill library (https://gervill.dev.java.net/) 2. The SeqMaker class: This class is responsible for creating a java MIDI Sequence. public field: javax.sound.midi.Sequence seq; It's constructor should accept 2 integer parameters. * ticksPerBeat which corresponds to the resolution property of Sequence. * track specify the number of tracks used. When it is called, a new Sequence object with divisionType=PPQ and resolution=ticksPerBeat should be created and assigned to seq. It has a public method add which adds a note to the sequence. This method has 4 parameters: int track: the track this note belongs to int note: the musical note expressed in a number long start: the start position of the note in the track. Specify a negative number for relative position. long duration: the length of the note There's an overloaded version of add which accepts the first two parameters and the start to 0 and duration to 1. Method endTracks is used to add some blank ticks at the end of each track, so the last note won't be cut off. It should add a MIDI MetaMessage of type 47 at a position relative to current tick length + limit. There are 2 unused methods in SeqMaker, playNow() and main(). They're made for testing purpose. All exceptions in this class are thrown to the caller. 3. The SoundLife main program. In the main program, the program does the following: a. Reads the pattern in the first command line parameter by the Pattern class. b. Create and initialize a world as specified by the Pattern. c. Create a new SeqMaker object with ticksPerBeat=3 and track = the smaller of world height and 16 d. Iterate through the rows of the current world. For each row calculate the number of living cells and the mean position of them. e. If the row is not blank (i.e. count>0), a note with the following properties will be added to the Sequence: track=current row mod 16 note = (current generation + mean + current row number) mod 80 + 30 start = 1 tick after the previous note duration = living cell count f. (After iteration,) the current position should be replaced by the maximum position on all rows so that notes from the next generation won't overlap with this one. g. Repeat d-e until generation number has reached the one specified by the second command line parameter. h. Use the Midi2WavRender class provided by the Gervill library to convert the sequence to a wav file. 4. The pattern for the competition is "Silver's p5:Life lexicon:23:19:6:6:11000000000 10000000000 01001000000 00011000000 00010001011 00100001101 00110000000".
SoundLife is an extension on Refactor Life and thus employs some of the same functionality - notably it can use Long, Array or Aging as a World implementation. Aging has no difference on execution (as opposed to array). The sound is generated using the uk.ac.cam.acr31.lifesounds package, usng the SineWaveSound to generate the individual sounds, and then stringing them together in a sequence, with each element of the sequence corresponding to a generation of the game. The sound generation is completed by a World Sounder class which handles the interface between the World, SoundLife and Audio parts of the project. World sounder controls two variables for each sine wave, frequency and amplitude. The amplitude is based on the population currently. This adjusted compared to the max population, using the following formula. amplitude = (0.8 / population) * square_root (population /maxPopulation); This contains two key terms. The first term, sets the base rate. This term includes the population and is neccessary to ensure that total amplitude never exceeds 1. A better method would have been to use the mean population - but this statistic was not available. The second is the square root function which adjusts the amplitude based on how full the game of life is. The square root is used to modify the distrubution to improve the variation over small changes. frequency = square_root( (x * y) / (world_height * world_width) ) The frequency is calculated using the above formula and works out the frequency based on the x and y co-ordinate of the specific alive cell. This is then calibrated against the total height multiplied by the total width. The square root then improves the distrubution of values.
The algorithm I have implemented is able to produce acid techno by evolving a game of life board. The drums are pre-configured (they were originally going to based on population in the centre of the board) while the bassline and main riff are based on the change in population of the entire board (again, with more time, these could be made to react to changes in the population in certain sectors of the board). The competition.wav file can be recreated by entering: java -jar rk395-tick6star.jar "Achim's P16 (Tick4* Entry):Rhodri Karim rk395:17:17:2:2:0000000110000 0000000101000 00100001011 011000001 1001 111 0 0000000000111 0000000001001 000010000011 00110100001 000101 000011" 32 competition.wav
Explanation of Program ---------------------- The first argument is retrieved from the command line, and passed to the constructor Pattern(), which decodes the string and makes a new Pattern object with methods that can be used to retrieve information about the pattern. The second argument is the number of generations to include in the output sound file, so this is stored in the variable generations. The filename is the third argument. A new boolean 2D array is created with dimensions of the pattern, and initialised from the data in the pattern object by calling p.initialise();. This may throw a PatternFormatException if the pattern is not formatted correctly, in which case no further action is taken. A new AudioSequence instance is created, where each 'frame' is 0.2s long. At each generation, a note is added to the current part of the sequence for each live cell where the pitch varies by the cell's position on the board (top left = lowest, bottom right = highest). The amplitude of each section of audio is constant by using the alive method to scale the amplitude for each component of the note. The frame is advanced after every generation, and the new generation is calculated by calling the nextGeneration method of PatternLife. Finally, a new OutputStream is created to write the AudioSequence to a file, the filename of which is specified in the third argument.
SoundLife notes.txt S. Hollington (sh583) My implementation of SoundLife uses AgingWorld to store cell information. As such, there is a public static integer named THRESHOLD that is defined in the SoundLife class that determines the maximum generations a cell needs to be dead before not being considered. The value I decided to use for THRESHOLD is 9 but this can be changed for different results. In order to reproduce my implementation of SoundLife, two helper functions are required. 1. public static AudioSequence worldToAudioSequence( AgingWorld world, int generationcount ) 2. public static void addGenerationToSequence( AudioSequence seq, AgingWorld world ) --- The first function must create a new AudioSequence instance, and then use a for-loop to call addGenerationToSequence() on successive generations of world (until the number of generations reaches the value specified by the function's parameter generationcount). Once the desired number of generations have been added, the function returns the AudioSequence instance. -- The second function iterates through each of the cells with a getCellAge() lower than THRESHOLD, counting them and calculating the following value for each one... double stdzdage = 1.0-world.getCellAge(x,y)/(double)THRESHOLD; This value is then added to a cumulative 'amplitude' variable, and in addition, a cumulative 'frequency' variable is increased by the following amount... frequency += (1.0-((double) y)/world.getHeight())*stdzdage NB: Both 'amplitude' and 'frequency' are stored as doubles. This calculation is designed to result in higher frequencies when more cells are at the top of the board and lower frequencies when more cells are at the bottom. The value is also affected by the age of the cells, and should result in lower frequencies if cells are dying off rapidly. Once all the cells have been visited, the average amplitude and frequency is found by dividing both of these cumulative variables by the threshold cell count. Finally a new SineWaveSound() object is created based on these values, and this Sound is added to the supplied AudioSequence at the current timeslot. Just before this function returns, advance() is called on the AudioSequence instance to advance it by one time-interval, ready for the next generation to be added. --- In order to generate a WAV file, the following code was added to the program's main() method, which should be pretty self explanatory. // Load the pattern Pattern pattern = new Pattern(patternstring); // Create a new instance of AgingWorld AgingWorld world = new AgingWorld(pattern.getWidth(), pattern.getHeight()); pattern.initialise(world); // And generate the audio sequence System.out.println("Generating audio sequence..."); AudioSequence audioseq = worldToAudioSequence(world, generationcount); // Finally, save the sequence as a WAV file FileOutputStream stream = new FileOutputStream(wavpath); System.out.println("Saving audio sequence to disk \""+wavpath+"\"..."); audioseq.write(stream); This code was, however, placed inside a 'try' block and the following exceptions were caught and handled gracefully, printing out any helpful error messages to the user. IOException SoundOverflowException PatternFormatException NumberFormatException
ALGORITHM FUNCTION FOR SOUNDLIFE by T. Edwards (tde21) >Pseudocode The algorithm iterates through all n generations requested. For each generation it iterates through all cells. Where a cell is found alive, its x and y co-ordinates are multiplied together, and this value is then rebased on a 0->1 scale. The corresponding note is stored. >Notes Each note lasts 0.1s. The "competition.wav" file enclosed contains the result of running SoundLife under: "Cheshire cat:Life lexicon:12:12:3:3:010010 011110 100001 101101 100001 011110" for 7 generations
My audible Game of Life implementation is based on Tick5's PatternLife. For each generation, the tone is determined by looking at each cell. If it is alive a 'touch-tone' type sound is added, with the tones being generated by: tone = (y position)/(height) and tone2 = (x position)/(width) These tones are given to a SineWaveSound. The volume is set by: volume = (neighbours)*0.1/9 The frame is advanced at the end of each generation, and if the total volume added to the frame would be greater than 1. This avoids SoundOverflowExceptions, which occur with anything more complex than a glider if the above rule is not used. All possible Game of Life games of any size are represented uniquely in this way. For my competition entry I used the following input: "10 cell row:Unkown Author:50:50:20:24:1111111111" 60 competition.wav