Click or drag to resize
Example: Conway's Game of Life

This example will use the concept of Conway's Game of Life to create sound. The sound will be made using MIDI Gremlin.

The example is seperated into three parts: "Program", "GameOfLife" and "DirectRender". The Program part is used as a setup class for GameOfLife. The GameOfLife contains all the calculations of the project.

This topic contains the following sections:

The DirectRender class will not be described in this example but can be found in the sourcecode. It is simply a class used for rendering the graphics.

Game of Life music

This section will describe the methods from the "GameOfLife" class that are used to create music.

Game of Life constructor

The constructor sets creates a grid for the values used in the game of life, as well as a grid for the temporary values used between each generation. The constructor also starts a timer that calls the update method every 0.75 seconds.

C#
internal GameOfLife(bool[,] gameGrid)
{
    // This represents each generation of the cells.
    Cells = gameGrid;
    // This represents the stage in between each generation.
    _cellsTemporaryState = new bool[gameGrid.GetLength(0), gameGrid.GetLength(1)];

    // These represent the max values of the x an y values in the grid.
    _xValue = gameGrid.GetLength(0);
    _yValue = gameGrid.GetLength(1);

    // This timer represents how often the update method is called.
    _timer = new Timer(UpdateMethod, null, TimeSpan.Zero, TimeSpan.FromSeconds(0.75));
}

UpdateMethod

The update method is the method used for calling the drawing and music methods, as well as updating all values in the grid.

C#
private void UpdateMethod(object state)
{
    // When IsDrawing is true, the Game will be drawn.
    if (IsDrawing)
        Draw();

    // When the player has asked for the game to play music this will call the PlayMidiMusic method.
    // We set this method to be called every fifth iteration to prevent too much overlap in the music.
    _musicCounter++;
    if (_isPlaying && _musicCounter == 5)
    {
        PlayMidiMusic();
        _musicCounter = 0;
    }

    // This part updates the values of each cell.
    for (int y = 0; y < _yValue; y++)
    {
        for (int x = 0; x < _xValue; x++)
        {
            _cellsTemporaryState[x, y] = IsAlive(x, y);
        }
    }
    // This method simply copies all values from one multidimensional array to another.
    CopyCells();
}

MusicSetup-method

The MusicSetup method enables the music to be played. This method is called from outside the class, and is required before the game will play any music.

C#
// This method initiates the music components.
internal void MusicSetup()
{
    // bpm is a constant deciding the tempo of the music.
    // when bpm is set to 60, one beat will last 1 second. 120 for half a second and so forth.
    const int bpm = 60;
    // Here we create a new orchestra using the output type WinmmOut which plays to the MIDI player in windows.
    _o = new Orchestra(new WinmmOut(0, bpm));
    // Here we use the orchestra to create a new Instrument, in this case a choir.
    // We chose to use the chromatic scale from the Scale class, it is however unneccesary since it uses this scale by default.
    _instrument = _o.AddInstrument(InstrumentType.ChoirAahs, Scale.ChromaticScale, 0);
    _isPlaying = true;
}

PlayMidiMusic-Method

The PlayMidiMusic method plays the music when called. The sounds that are played is decided by the values in the grid. Each true value plays a sound decided by where the value lies on the grid's x-axis. The y-axis instead decides when values are played. A higher y-value means a longer delay before the sound is played.

C#
// This method plays different sounds at different times based on the coordinates of all living cells in the game.
// The x coordinates decides which tones are used.
// The y-coordinates decides when sounds are played.
private void PlayMidiMusic()
{
    SequentialMusicList sMusic = new SequentialMusicList();
    int localInt;

    // This nested for-loop iterates over all cells in the game.
    for (int y = 0; y < _yValue; y++)
    {
        for (int x = 0; x < _xValue; x++)
        {
            // This if statement checks whether a cell is still alive
            if (Cells[x, y])
            {
                localInt = x < _xValue/2 ? x : _xValue - x;
                // This creates a new keystroke with an offset based on where in the grid the cell is, and adds it to the SequentailMusicList.
                sMusic.Add(new Keystroke(Tone.C, 1).OffsetBy(_quintScale, localInt));
            }
        }
        // We add a pause here to play sounds later if they have a greater y-coordinate.
        sMusic.Add(new Pause(1));
    }
    // This plays all the sounds in the list, as longs it contains any ,and as long as the instrument isn't null.
    if(sMusic.Count != 0)
        _instrument?.Play(sMusic);
}
Program: Setup values

These are the intitial values put used to start the game as well as the visual rendering and music. The two "Gliders" are put into a grid, and this grid is then used to start a new Game of life. After the game is started, the drawing is turned on, and the MusicSetupMethod is called to enable music playing.

C#
using System;

namespace ConwaysGameOfLife
{
    internal class Program
    {
        /*
            This example will use the concept of Conway's Game of Life to create sound.
            The sound will be made using MIDI Gremlin.
        */
        private static void Main(string[] args)
        {
            bool[,] grid = new bool[40,40];
            // Setup some start values

            // Glider 1 : depicted as followed
            // 0 1 0
            // 0 0 1
            // 1 1 1
            grid[2, 1] = true;  
            grid[3, 2] = true;
            grid[3, 3] = true;
            grid[2, 3] = true;
            grid[1, 3] = true;

            // Glider 2
            // 1 1 1
            // 0 0 1
            // 0 1 0
            grid[12, 13] = true;
            grid[11, 11] = true;
            grid[12, 11] = true;
            grid[13, 11] = true;
            grid[13, 12] = true;


            // Start The Game of Life
            GameOfLife gol = new GameOfLife(grid);

            // Start Drawing
            gol.IsDrawing = true;

            // Play sounds using the Game of Life
            gol.MusicSetup();

            Console.ReadKey();
        }
    }

}
Game of Life class code.

This is the entire code from the GameOfLife class.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MidiGremlin;

namespace ConwaysGameOfLife
{

    internal class GameOfLife
    {
        //MIDI Gremlin Variables
        private Orchestra _o;
        private Instrument _instrument;
        // A scale representing tones that harmonize well with eachother
        private Scale _quintScale = new Scale(Tone.C, Tone.G, Tone.D, Tone.A, Tone.E, Tone.B, Tone.FSharp);

        // Class-specific valuables
        public bool[,] Cells { get; private set; }
        private bool[,] _cellsTemporaryState;
        private readonly int _xValue;
        private readonly int _yValue;
        private Timer _timer;
        internal bool IsDrawing;
        private bool _isPlaying;
        private int _musicCounter = 0;


        #region Constructor
        internal GameOfLife(bool[,] gameGrid)
        {
            // This represents each generation of the cells.
            Cells = gameGrid;
            // This represents the stage in between each generation.
            _cellsTemporaryState = new bool[gameGrid.GetLength(0), gameGrid.GetLength(1)];

            // These represent the max values of the x an y values in the grid.
            _xValue = gameGrid.GetLength(0);
            _yValue = gameGrid.GetLength(1);

            // This timer represents how often the update method is called.
            _timer = new Timer(UpdateMethod, null, TimeSpan.Zero, TimeSpan.FromSeconds(0.75));
        }
        #endregion

        #region MusicSetup
        // This method initiates the music components.
        internal void MusicSetup()
        {
            // bpm is a constant deciding the tempo of the music.
            // when bpm is set to 60, one beat will last 1 second. 120 for half a second and so forth.
            const int bpm = 60;
            // Here we create a new orchestra using the output type WinmmOut which plays to the MIDI player in windows.
            _o = new Orchestra(new WinmmOut(0, bpm));
            // Here we use the orchestra to create a new Instrument, in this case a choir.
            // We chose to use the chromatic scale from the Scale class, it is however unneccesary since it uses this scale by default.
            _instrument = _o.AddInstrument(InstrumentType.ChoirAahs, Scale.ChromaticScale, 0);
            _isPlaying = true;
        }
        #endregion

        #region UpdateMethod
        private void UpdateMethod(object state)
        {
            // When IsDrawing is true, the Game will be drawn.
            if (IsDrawing)
                Draw();

            // When the player has asked for the game to play music this will call the PlayMidiMusic method.
            // We set this method to be called every fifth iteration to prevent too much overlap in the music.
            _musicCounter++;
            if (_isPlaying && _musicCounter == 5)
            {
                PlayMidiMusic();
                _musicCounter = 0;
            }

            // This part updates the values of each cell.
            for (int y = 0; y < _yValue; y++)
            {
                for (int x = 0; x < _xValue; x++)
                {
                    _cellsTemporaryState[x, y] = IsAlive(x, y);
                }
            }
            // This method simply copies all values from one multidimensional array to another.
            CopyCells();
        }
        #endregion

        #region PlayMidiMusic
        // This method plays different sounds at different times based on the coordinates of all living cells in the game.
        // The x coordinates decides which tones are used.
        // The y-coordinates decides when sounds are played.
        private void PlayMidiMusic()
        {
            SequentialMusicList sMusic = new SequentialMusicList();
            int localInt;

            // This nested for-loop iterates over all cells in the game.
            for (int y = 0; y < _yValue; y++)
            {
                for (int x = 0; x < _xValue; x++)
                {
                    // This if statement checks whether a cell is still alive
                    if (Cells[x, y])
                    {
                        localInt = x < _xValue/2 ? x : _xValue - x;
                        // This creates a new keystroke with an offset based on where in the grid the cell is, and adds it to the SequentailMusicList.
                        sMusic.Add(new Keystroke(Tone.C, 1).OffsetBy(_quintScale, localInt));
                    }
                }
                // We add a pause here to play sounds later if they have a greater y-coordinate.
                sMusic.Add(new Pause(1));
            }
            // This plays all the sounds in the list, as longs it contains any ,and as long as the instrument isn't null.
            if(sMusic.Count != 0)
                _instrument?.Play(sMusic);
        }
        #endregion

        private void CopyCells()
        {
            for (int y = 0; y < _cellsTemporaryState.GetLength(1); y++)
            {
                for (int x = 0; x < _cellsTemporaryState.GetLength(0); x++)
                {
                    Cells[x, y] = _cellsTemporaryState[x, y];
                }
            }
        }

        private void Draw()
        {
            DirectRender render = new DirectRender(_xValue, _yValue);
            render.Update(Cells);
        }

        internal bool IsAlive(int xValue, int yValue)
        {
            // This value depicts the x-value 1 lower than that of the cell being operated on. 
            // If the cell being operated has an x-value of 0, this will instead become the highest possible x-value.
            int xValueIfLow = xValue == 0 ? _xValue - 1 : xValue - 1;

            // This value depicts the x-value 1 higher than that of the cell being operated on. 
            //If the cell being operated has the highest possible x-value , this will instead become 0.
            int xValueIfHigh = xValue == _xValue - 1 ? 0 : xValue + 1;

            // This value depicts the y-value 1 lower than that of the cell being operated on. 
            //If the cell being operated has an y-value of 0, this will instead become the highest possible y-value.
            int yValueIfLow = yValue == 0 ? _yValue - 1 : yValue - 1;

            // This value depicts the y-value 1 higher than that of the cell being operated on. 
            //If the cell being operated has the highest possible y-value , this will instead become 0.
            int yValueIfHigh = yValue == _yValue - 1 ? 0 : yValue + 1;

            /*
                The code is depicted excectly like this
                x  x  x
                x  o  x
                x  x  x
                where o is the point we are working on, and x is the points surrounding it.
            */
            bool[] neighBours =
            {
                Cells[ xValueIfLow, yValueIfLow],  Cells[xValue, yValueIfLow],  Cells[xValueIfHigh, yValueIfLow],
                Cells[ xValueIfLow, yValue],                                    Cells[xValueIfHigh, yValue],
                Cells[ xValueIfLow, yValueIfHigh], Cells[xValue, yValueIfHigh], Cells[xValueIfHigh, yValueIfHigh]
            };

            int liveNeighBours = neighBours.Count(b => b);

            bool isAlive = Cells[xValue, yValue];
            if (liveNeighBours < 2 && isAlive) // Any live cell with fewer than two live neighbours dies, as if caused by under-population.
            {
                isAlive = false;
            }
            else if ((liveNeighBours == 2 || liveNeighBours == 3) && isAlive) // Any live cell with two or three live neighbours lives on to the next generation.
            { }
            else if (liveNeighBours > 3 && isAlive) // Any live cell with more than three live neighbours dies, as if by over-population.
            {
                isAlive = false;
            }
            else if (liveNeighBours == 3 && !isAlive) // Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
            {
                isAlive = true;
            }
            return isAlive;
        }
    }
}