/nino_pianino

Program for generating music.

Primary LanguagePythonMIT LicenseMIT

Nino Pianino

Synopsys

This is a program designed to create music based on previously defined JSON templates. These templates allow you to define blocks of music, and then combine said blocks to create a song. Defining a block of music requires details about the piece of music it should generate (such as number of notes, the key(s) used in the block, time signature and so on), but a lot of these options have defaults, so, in theory, you should be able to just leave a blank block and it should generate some sort of music. this may happen sometime far in the future but yeah.

The generator.py program doesn't necessarily require JSON files to work - you can import it in another python script and pass a dictionary of blocks and use them to generate a song. Examples will be provided below.

This program uses the Midiutil package to add notes to a midi track. I included it in the repo for convenience. I may have been able to use additional functionalities of the package, but I had a vision and lack of experience, so I just used what I needed and may have reinvented some wheels in the process.

Disclaimer: I'm still a novice in software programming. Smelly code and lacking documentation may be present, but do feel free to drop good criticism so I can improve the project and my future endeavours. Of course, if you would want to do so :)

Installation

You can clone this repo and use the setup.py file.

    $git clone https://github.com/NinoDoko/nino_pianino.git
    $cd nino_pianino
    $python setup.py install  #might need su privileges for this

It's also on pip.

    $pip install ninopianino  #you probably need to use sudo for this too

If you do this, you should be able to use the cli.

pianize generate_song --soundfont /path/to/soundfont.sf2 --output ~/path/to/some/directory/

Templates

Example templates can be found in the test_templates/ dir. complextest.json shows off a simple complex_block, nestedcomplextest.json is a more complicated composition of multiple complex blocks, some of which contain nested complex blocks. And finally, 32_beats_test.json uses the previously mentioned complex and nested blocks, but also uses channels and progam_numbers to add a flute and percussions.

The way templates work is by defining a JSON file which contains some blocks. Each block is a dictionary which looks like this :

Blocks

``` { "name": "intro_block1", "track": 0, "play_at": [0, 44, 88, 588, 824], "bpm": 200, "number_of_bars": 4, "number_of_beats_per_bar": 11, "bias_same_note": 55, "bias_separate_notes": 5, "pattern": [1, 2, 1, 2, 1, 3, 1], "root_note": "C#", "scale": "minor", "low_end": "C#3", "high_end": "C#5", "default_accent": 100, "accents": { "0": 100, "3": 80, "6": 80 } } ```
  • name: This is the name of the block. It simply states the name of the track. (note: in the current version, this doesn't work. midiutil doesn't want to properly convert the text to ascii, and I need to figure this out). Default : ''.
  • track: The number of the track this block goes to. Default : 0.
  • channel: This is the channel that notes will be placed on for the midi file. Use channels with program_numbers to assign instruments.
  • program_number: The program number for the notes generated by the block. Including a value for this will cause the notes to be played with an instrument defined by the sf2 file used. You can look up the General MIDI specification, which a lot of soundfonts (including the one I'm using) follow.
  • play_at: A list of integers where each number is the beat when the block is played. The above block will be played at the 0th beat, 44th beat, 88th beat and so on. This is the only way to synchronize blocks to play at certain times. Required
  • bpm: The tempo at which the block is played. Take care when adding tempos to tracks: it you put two blocks on the same track and add different tempos for them, it might not sound as intended. Default: 120.
  • number_of_bars: The number of bars per block. Default: 1
  • number_of_beats_per_bar: The number of beats per bar. This, alongside number_of_bars determines how many notes there are in a block. In the above block, with 4 bars of 11 beats, the block will last for 44 beats, so setting the play_at value at [0, 44, 88] will mean the block will play continualy for 132 beats. Default : 4
  • bias_same_note: This is a value which is used to determine whether two consecutive notes will be the same. The generator will merge two consecutive notes into one note with combined length (unless bias_separate_notes is used). Basically, this is the probability that after generating a note, the next one will be the same. Default: 0.
  • bias_separate_notes: This is a value which dictates whether two same consecutive notes are merged together or not. You can use this to create longer notes for faster tempos. Default: 0.
  • pattern: A list of how notes should be arranged. The number of values is the number of notes in the bar, and the value is how many beats it lasts. In the above example, each bar will have 7 notes and the notes will have length of 1, 2, 1, 2, 1, 3, 1 respectively. Adding up the lengths correctly brings up to 11 beats per bar. Incorrect patterns are not documented. Default: [].
  • repeat: This option will cause the whole block to be repeated multiple times. It is there purely as a convenience if you have a block you know will be played multiple times. The way it works is it simply adds additional points to the play_at list. For instance, a block with 7 beats per bar and 4 bars with play_at: [0, 112] and repeat: 2 will end up with play_at: [0, 28, 112, 140]. To use this, you will need to supply the block with a number_of_bars value as well as a number_of_beats_per_bar, so the program can do the math. Warning: This seems to be malfunctioning atm. It does work but it can be weird, so do try to avoid using it.
  • root_note: The root note that, along with the scale value, will determine the notes generated for the block. Default: A
  • scale: Combined with the root note, this represents a list of notes that the generator will choose from. There's a lot of different types of scales that you can look up online. #TODO spoiler quote with all scales Default: minor
  • base_notes: If there is not a scale that you want to use, you can use a list of base notes. For instance, if you want to use a gypsy scale minus the last two notes, you can write a base_notes value like so: [0, 2, 3, 6, 7]. Default: []
  • low_end: The lowest note that can be generated for the block. Default: A0
  • high_end: The highest note that can be generated for the block. Use it with low_end to define a range from which notes can be chosen. The block above will choose values between C#3 and C#5. Default G#8.
  • default_accent: Accents are basically the volume levels for notes. Non-accented notes will use the default accent. Default: 50.
  • accents: A dictionary where the keys are the number of the note to be accented and the values are the volumes of these notes. For example, the value of {"0":80, "3":80, "5":80} will generate notes with the default_accent, except for the 0th, 3rd and 5th note, which will have a higher accent value of 80. Default: {}.
  • notes_bias: A dictionary where each key is the position of the note in the scale, and the value is how many times the note should be added to the base notes of the key. Basically, the higher the number, the more often that note will come up. For isntance, adding a notes_bias of {"0":5} for C major will generate the base notes: ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C', 'C', 'C', 'C']. Notice the 5 C's. Thus, when generating a random note, it will have a 5 / 11 = 0.4545 chance to hit a C.

Complex blocks

Sometimes, you may want to have certain blocks played together all the time, for instance a chorus segment. This segment may have a block for the main line, that plays higher notes, and a couple of blocks that play lower notes. You may also maybe like to make a polyrythm, for instance 3 bars of 7 beats each and a pause of 1 beat at the end played over a background block of 2 blocks of 11 beats each. You can do this with ordinary blocks, but writing the play_at list will be a pain in the A minor. So you can just use complex blocks instead.

[
{
    "block_type":"complex",
    "play_at":[0, 44, 88],
    "bpm": 500,
    "track" : 0,
    "blocks":
    [
    {
	    "name": "main_block1",
	    "play_at": [0, 22],
	    "number_of_bars": 3,
	    "root_note": "B",
	    "scale": "major",
	    "number_of_beats_per_bar": 7,
	    "low_end": "B3",
	    "high_end": "C5",
	    "bias_separate_notes": 100,
	    "default_accent": 40,
	    "accents": {
		    "0": 70,
		    "3": 70, 
		    "5": 50
	    }
    },{
	    "name": "main_block2",
	    "play_at": [0, 22],
	    "number_of_bars": 2,
	    "root_note": "B",
	    "scale": "major",
	    "number_of_beats_per_bar": 11,
	    "low_end": "B1",
	    "high_end": "C3",
	    "pattern":[3, 2, 2, 1, 2, 2],
	    "accents": {
		    "0": 100,
		    "3": 100, 
		    "6": 80, 
		    "8": 80, 
		    "13": 100
	    }
    }
]
}
]

The complex block must have a block_type value set to 'complex'. This block also must have a play_at value which works as an ordinary block - this one is played on the 0th, 44th and 88th beats. It also has a bpm and track values which act as in a normal block.

Then, it must have a blocks value, which is a list of other blocks. The blocks inherit most of the options from the parent block - the options they don't inherit are : blocks', block_type, play_at, repeat and number_of_blocks. The idea here is that the children blocks have their play_at values act differently - they describe when the block is played inside the complex block. In the above example, if you consider the block as a separate block of music, they will play on the 0th and 22th beat in that block. Then, the complex block makes that block repeat on the 0th, 44th and 88th beat.

Furthermore, you can have complex blocks containing other complex blocks. Check out nestedcomplextest.json in the blocks/ directory.

Usage

You call the generator.py script with certain arguments to get your music, and that script, in turn, calls the other modules which contain the actual meat. If you cloned/downloaded the repo, you can do: ``` $python generator.py --input blocks/longsongtest.json --output myfirstsong ```

This will use an example template and create a midi file - myfirstsong.mid . Adding synthesizers to convert the midi to a wav/mp3/whatever is up to the user. The easiest way is to get fluidsynth.

    #apt-get install fluidsynth

Then, you need a soundfont. This is where I got the ones I use for testing, specifically the FluidR3 GM bank. Mind you, this is an sfArk file, and you need to find a way to decompress it. Alternatively, you can just google around for a standard issue sf2. Decompressing sfArks is a bit annoying, but I managed to get by with following this tutorial.

With fluidsynth, you can then go:

    $fluidsynth -F myfirstsong.wav /path/to/file.sf2 myfirstsong.mid

Or, to save on a few keystrokes, there is an argument you can use for this:

    $python generator.py --input blocks/longsongtest.json --output myfirstsong --use_soundfont /path/to/file.sf2

Alternatively, if you cleanly organized your template into tracks, you can import it into actual music editing software, like rosegarden or lmms.

List of arguments:

  • --output: An argument which defines the name under which the .mid (and .wav) file be saved. Default = 'output' --input: An argument with the path to the .json file containing the template. Default = 'input.json' --use_soundfont: Path to the sf2 file to use for creating a wav. Only works if you have fluidsynth. default = '' --no_tracks: The maximum number of tracks for the mid. Generally you don't have to worry about this. Default is set to 100.

As of later versions, you can also just use song_generator.py. This assumes you have a soundfont. You can use it like this:

    $python song_generator.py path/to/soundfont.sf2

And it should spit out a .mid, .wav and .mp3 file. If you don't have lame installed, it should still create the wav. If you have the FluidRM soundfont in a soundfonts/ directory, you can skip the argument.

Or, if you used the setup.py script, you can use the cli.

$pianize generate_song --soundfont /path/to/soundfont.sf2 --output ~/path/to/some/directory/

Using this script generally creates kinda weird stuff though, and it's a work in progress.

Project overview

This is a section to describe the workings of the program. It does not describe how to actually use it, so it may not be of your interest to read it.

music_models.py

This module contains the models for generating the notes.

Note

The Note model is just a wrapper around the midiutil package and serves as a convenience class to pass around values which are then sent for output. Mind you, while the midiutil package's addNote method requires a pitch value, the note class takes in a note value and then converts it to pitch in the init method. The format of notes that can be passed to this class is [#]. For instance, G#6 means the G# note on the 6th octave on a keyboard. For convenience's sake, I do not use flat notes because, well, this is not a music theory textbook, and it's way more simple this way.

Key

The Key is there to help out generating random notes with some patterns. When creating a Key instance, you give it the root note and the type of scale, and it will generate a list of notes belonging to this scale. You can then call the generate_note method to, well, generate a note. When initiated, a root note and a scale are required to construct a list of basic notes. For instance, a root note of C and a scale with argument 'major' will generate the basic notes : [C, D, E, F, G, A, B]. With these basic notes, the key can then generate all possible notes for these arguments. There are other additional arguments. You can also pass a base_notes argument which will allow you to use a custom scale. You can also add the low_end and high_end arguments, which dictate what is the lowest possible note and the highest possible note.

Bar

The Bar class helps out with timing notes. It is used in the note_timing.py module, and with the accents and default_accent arguments, it will place the notes' volumes at the specified values.

notes_timing.py

This module contains (for the time being) a single method that organizes notes into bars (using the musuc_models.Bar model). The group_notes_for_time_signature takes a list notes and organizes them using the rest of the arguments.

The function takes an already generated list of notes and assigns to them the volumes, length and the time at which they appear in the songs.

template_utils.py

This file contains a bunch of functions which should make writing code that generates the music blocks a bit easier. More details on how to use this below, in section Template utils.

generator.py

This is what brings it all together. It reads in a JSON file, parses the arguments, passes them to the music models for note generating, receives a fresh batch of notes, then sends them to the note_timing module for the notes to be organized into a list of bars, along with values for when to be played and how long to be played for, and then passes the notes to midiutil and ta-da!

This module doesn't actually do any of the work - it just takes input, calls other modules, organizes the returned values and handles the output. In other words, you call this script to generate the songs. Usage is described in the Usage section.

Alternatively, you can import this file in any custom python script and call generate() to get a MIDIFile object, which you can then write with write_mid().

Using different instruments

There's currently no wrapper for using instruments because it depends on the soundfonts that you're using. I may add support for instruments for soundfonts following the General MIDI standard, but I lack the time at the moment.

Currently, it's pretty easy to add normal instruments - all you have to do is to create a block, add a channel not being used already (if possible, otherwise at least a channel that is not being used at the same time), add a program_number for that channel and there you go!

The difficult part is adding percussions. Most of my percussion blocks currently use something like this:

{
    "block_type":"complex",
    "name":"percussions",
    "track":3,
    "channel":10,
    "default_accent":80,
    "play_at":[0],
    "blocks":
    [
        {
            "name": "percussions_1",
            "play_at": [0],
            "root_note": "C",
            "scale": "major",
            "low_end": "B0",
            "pattern":[3, 2, 3, 2, 3, 2, 3, 2, 2],
            "high_end": "G1",
            "default_accent": 50,
            "accents": 
            {
                "0":100,
                "3":70, 
                "8":70,
                "13":70,
                "18":70
            }
        },{
            "name": "percussions_cymbal_1",
            "play_at": [0],
            "bias_same_note":70,
            "base_notes":["F#", "G#", "A#"],
            "low_end": "C1",
            "high_end": "C2",
            "default_accent": 70
        }
    ]
}

This takes advantage of the predefined notes for percussions, but it does sound kinda dorky. Granted, drumming has never been my strong side, I could probably make a better way to add drums to a song. Also note that the parent complex block uses the 10th channel, which is by General Midi standards the channel for adding percussions and program_number is generally ignored.

Template utils

To alleviate this, I've created a template_utils.py file which will aim to help with programatically creating blocks. This file kind of solves the percussion problem, as well as generating a few blocks following a chord pattern. So far, it contains a bunch of methods:

create_base_block(bpm = 120, play_at = [0], name = 'base', track = 1, )

It just returns a dict with the specified parameters. Useful if you want to have a bunch of defaults. 


create_percussion(block, no_hits = 1, no_cymbals = 1)

Returns a percussion block, generated using the supplied _block_ values for number_of_bars and number_of_beats_per_bar. The no_hits argument tells the method how many blocks it should generate for generid drum hits, and the no_cymbals does the same for cymbal hits. As I am no musician, I don't know the actual terms, so feel free to correct me. 

Example usage: 
```
    piano_intro = 
    {
        'track' : 1,
        'number_of_beats_per_bar' : ...
    }
    
    percussion_intro = template_utils.create_percussion(piano_intro)
    percussion_intro['blocks'][0]['pattern'] = [3, 2, 2]
    percussion_intro['blocks'][1]['bias_same_note'] = 60
    
    piano_intro['blocks'].append(percussion_intro)

```

create_chord_progression(block, chords = [], extra_kwargs = {})

Returns a list of blocks following a specific chord progression and according to values from the supplied block. Basically, pass it a complex base block with values such as number_of_beats_per_bar, number_of_bars and a chord progression. The progression needs to be in form [('root_note', 'scale'), ('root_note', 'scale')...]. The method then knows how long the blocks need to be and when they need to be played, and will correctly generate a list of blocks that you can directly append to the block['blocks'] value. At least in theory (and currently in practice). 
You can also pass extra_kwargs, which is a dictionary of any additional arguments you want the resulting blocks to have, such as notes_bias, low_end and high_end etc. 

Example usage: 
```
    piano_intro = 
    {
        'track' : 1,
        'number_of_beats_per_bar' : 7, 
        'number_of_bars' : 4, 
        'name' : 'piano_intro', 
        'play_at' : [0], 
        'block_type' : 'complex',
        'default_accent' : 60, 
        'blocks' : 
        []
    }
    piano_high_intro = template_utils.create_chord_progression(piano_intro, chords = [('G', 'major')], extra_kwargs = {'low_end' : 'G2', 'high_end' : 'G4', 'bias_same_note' : 30, 'notes_bias' : {'0' : 4, '2' : 2, '4' : 4, '7' : 4, '8' : 2}, 'number_of_bars' : 4})
    piano_intro['blocks'] = piano_high_intro
```

repeat_block(block, number_repeats, number_of_bars)

The preferred substitute for the repeat key of a block (for now). Give it a block, how many times it should repeat and the number_of_bars of the block. It will successfuly generate and return a list of integers for when the block needs to be played. 

Example usage:
```
    piano_intro = 
    {
        'track' : 1,
        'number_of_bars' : 4, 
        'number_of_beats_per_bar' : 7,
        'play_at' : [0], 
        ...
    }
    piano_intro['play_at'] = template_utils.repeat_block(piano_intro, 4, 4) #Will result with play_at = [0, 28, 56, 84]
```

TODO

Well, this has a long way to go and I'm sure there's a lot of issues to fix. I have plans for adding a bunch more options, there's inevitably going to be bugs, the code probably needs to be better organized and documented. All in due time. I just wanted to get this out there because I'm still a novice in software development and could use constructive criticism.