This musical analysis program provides tools for music theory. Analyse melodies and chord progressions, build scales, chords and chord scales!
Important: Instructions for running this app are found at the bottom of this document.
N/A.
N/A.
See R5 below for references.
Link to public GitHub repository: https://github.com/jevontrei/JoelvonTreifeldt_T1A3
The Python code in this program was styled according to the Python Enhancement Proposals 8 (PEP-8) rules using the VSCode extension "autopep8".
- van Rossum, G., Warsaw, B. and Coghlan, N. (2013) PEP 8 – Style Guide for Python Code, Python Enhancement Proposals. Available at: https://peps.python.org/pep-0008/.
- Microsoft (2024) autopep8 (v2024.0.0) [Visual Studio Code extension]. Available at: https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8.
This program has five primary features the user can interact with, each taking input, performing analysis, and providing an output. Four auxilliary features are also provided.
The main.py
file is the central script for running the entire program. It contains internal and external imports, global variable initialisation, and the function definitions for clear_terminal()
and main()
. After these elements, an if
statement is used to call the main()
function if and only if the main.py
file is where the run command is given.
Functions are imported from modules within the local packages called analysis
, build_chords
, build_scales
, formatting
and exceptions
. The imports not defined within the program are datetime
, json
and os
.
The roots
, major_scale_qualities
and all_keys
variables may be defined outside the scope of the main()
function as they do not need to be changed within the function. The roots
variable must be defined early in the program as it is a foundational list of note names which every primary feature depends on. Its entries are defined as "formatted" notes, which means the upper and lower case of each character is correctly formatted, and underscores are appended to natural notes (A, B, D etc.) to make them the same length as the accidental notes (Ab, Bb, Db etc.) for simpler processing. These notes are always "unformatted" with unformat_output_notes()
before being shown to the user (discussed below). Note: to simplify the application, accidental notes such as A# and C# are referred to as their enharmonic equivalents Bb and Db, respectively. Similarly, any features using chord scales depend on the major_scale_qualities
list of chord qualities. The build_all_scales()
function is then used to define the dictionary all_keys
. Note: the terms scale and key are often used interchangeably.
Before using os.system("clear")
to clear the terminal, it is important to check which operating system the program is being run on. The clear_terminal()
function uses an if/else
block embedded in a try/except
block to pass "cls"
instead of "clear"
if the operating system is Windows-based.
The main()
function is where the user begins seeing the program print content to the terminal. First, to avoid having to use a global
keyword later, an activity counter variable is defined inside the scope of the main()
function for any imminent additions to the activity log. Next, the user is asked to provide their name and instrument of choice. They may skip this step, and the program will adjust its output accordingly. If the user data is provided, it as added to the user_log.json
file. In either case, the user log is printed to the terminal.
The user then sees a menu of activities to choose from and enters their selection. This commences the bulk of the main()
function, which is enclosed in a while
loop to ensure the activity menu is always shown after an activity is completed (or failed due to invalid input). The user may enter "8"
to exit the application altogether. The rest of the input options are described as follows.
The build_all_scales()
function takes a list of all 12 root notes as input and returns a heptatonic (seven-note) major scale built from each note. It starts by defining a list of integers which make up the unique intervals in a major scale. Then a scales
variable is initialised as an empty list. Nested for
loops are used to append notes at the appropriate intervals to a key
list which then get appended to scales
. The note names and scales are zipped into a dictionary and returned.
The unformat_output_notes()
function takes formatted notes and returns unformatted notes suitable for displaying to the user. A copy is made of the input list to avoid changing the input variable. The input is placed inside a list if applicable and then loops through this list removing underscores from natural notes. A list of unformatted notes in returned.
This auxilliary feature is called when the user enters "0"
. A well-known melody is presented in the form of note names. The user is given hints as to what it is, and is prompted to guess where it comes from. The input is stripped of whitespace and converted to lowercase before being checked against the correct answer. A message is printed to either congratulate or disappoint the user. In both cases they are redirected back to the menu.
Upon selecting "1"
from the menu, the terminal shows valid note names that it can parse. This is done by looping through the "pre-formatted" elements in the roots
list and "unformatting" them for the user to view (removing underscores on natural notes).
The melody analyser receives an input string of comma-separated notes (a melody) and attempts to tell the user which musical key the melody fits within. The input string is split into a list and formatted to be compatible with the pre-defined key-checking dictionary all_keys
. The format_validate_notes()
function validates the input, and if it succeeds, adjusts the case for each character and appends underscores to natural notes. For example, e
becomes E_
and AB
becomes Ab
. The analyser can handle melodies with one note, but this does not produce a very meaningful result, and the program advises the user of this when applicable.
The validated input is passed into the analyse_melody()
function. If the returned list is empty, the user is advised that a fully compatible key was not found. This betrays a limitation of the application, not necessarily a fault in the user's melody. It is common for melodies to be primarily in one musical key while borrowing some notes from other keys. If one or more compatible keys were found, the output is unformatted and printed.
If the analysis was successful, the activity log is updated with the current activity details. To begin this process, datetime
is used to record when the activity took place. This information is combined with the activity counter variable, activity selection, input and output into a dictionary. The activity_log.json
file is opened in read mode and its content assigned to a variable called log
. The new activity information is appended to log
, which in turn is written to the activity log. Finally, the activity counter is incremented. This activity log process applies to all five primary features.
The format_validate_notes()
function takes in note names and valid root notes. The purpose of this function is to format notes so they may be compared to the roots
variable for validation and for future unformatting before printing to the user. The "notes" parameter should be passed as a list, or optionally as a string (for a single note). First, the input is placed into a list if applicable. An if/else
block within a for
loop then checks which processing is required for each input note. Note names are made upper case and stripped of whitespace, then checked for length. Note names with one character are appended with an underscore. If two characters are detected, the second character (the "flat" sign "b") is made lower case. In both cases, the result is passed through a set to remove duplicates, then passed as a list again. The formatted result is then returned. If the note name does not have one or two characters, a custom ValidationError is raised. Outside of the for
loop, another ValidationError is raised if the formatted note is not found to match anything in roots
.
The analyse_melody()
function takes in a melody notes list and a keys dictionary. It defines an empty list called compatible
, then loops through the keys dictionary and checks if all the melody notes are in each key. If a compatible key is found, it is appended to compatible
. After every melody note and every key is cross-checked, compatible
is returned.
Selecting "2"
from the menu requests a note name and a quality from the user and constructs the appropriate pentatonic (five-note) scale. An input prompt is shown, including examples of valid entries. The input is split into a list and then formatted and validated. A locally-defined ValidationError is raised if certain input conditions are not met. If valid, the input is passed into the build_penta()
function and the output is unformatted and printed. Successful scale builds are recorded in the activity log.
The build_penta()
function takes in a root note, a list of all roots, and a quality as input. It starts by defining the major and minor pentatonic scale intervals in lists. It then initialises an empty penta
list. In a while
loop, the quality argument is validated as either "maj" or "min'. If neither is true, a ValidationError is raised. If a match is found, scale notes are appended to penta
according to the corresponding interval list, and penta
is returned.
Selecting "3"
from the menu requests a note name and a number from the user and builds the corresponding triad (three-note chord). The input prompt shows the user an example of a valid entry. The input string is split into a list, formatted and validated. A scale is selected from the all_keys
dictionary using the input note. Since major scales only have seven unique notes, the degree number input is converted to an integer and validated to be between one and seven. A try/except
block is used to show a graceful error message if an invalid degree is entered. The valid input is passed into the build_triad()
function and the chord is unformatted using unformat_output_chords()
and printed for the user. Successful chord builds are recorded in the activity log.
The build_triad()
function takes in a major scale, a number between 1 and 7, and the chord qualities found in the major scale. To offset Python's zero indexing, the degree number is incremented down by one. An empty list called triad
is defined within the scope of the function to house the triad notes. The inputs are combined to define the name of the triad to be built. A for
loop iterates three times to append every second note to triad
, according to the standard musical chord-building rules. Musical scales use modular arithmetic, so the % 7
mod operator is used to circle back through the scale as necessary. The function returns a triad name and a list of the notes within that chord.
The unformat_output_chords()
function takes formatted chords in a string or a list as input. String inputs are placed in a list if necessary. The input is looped through and all underscores are removed. The function returns unformatted chords suitable for displaying to the user.
Selecting "4"
from the menu requests one note from the user, and shows valid input examples. The input is formatted and validated, then used to pass a scale into the build_chord_scale()
function. The output is unformatted and printed to the terminal. For printing the results, a for
loop is used to print certain details of each chord in the output variable chord_names
. Successful chord scale builds are recorded in the activity log.
The build_chord_scale()
function takes in a major scale and the chord qualities found in the major scale. An empty dictionary and list are initialised to be populated with the chord scale and names, respectively. In a for
loop, each note in the scale is passed into the build_triad()
function and the output is added to the aforementioned dictionary and list. The function returns a chord scale dictionary and a list of chord names.
Selecting "5"
from the menu requests a series of chords and shows example valid inputs. The analyse_progression()
function is called, which formats and validates the input internally using format_validate_chords()
. The outputted result
list is unformatted with unformat_output_notes()
. An if/else
block checks if result
is empty, and if so, tells the user that this application is not yet sophisticated enough to analyse progressions transcending a single key. If result
is not empty, a for
loops is used to print the compatible key/s. Successfully analysed progressions are recorded in the activity log.
The analyse_progression()
function takes in a series of chords, a dictionary of valid key centers/scales, a list of the chord qualities in the major scale, and a list of valid root notes. First, the input is formatted and validated with the format_validate_chords()
function. To avoid duplicated candidate keys, two empty sets pre_candidates
and post_candidates
are initialised for different stages of processing. In a for
loop, build_chord_scale()
is used to construct a chord scale for each key center, essentially checking every valid chord that exists within all 12 major scales. Within the loop, an if
statement checks if the first chord in the input progression is in a key. If it is, that key is added to the pre_candidates
set. Another if
statement assigns a final candidates
equal to pre_candidates
if the progression only contains one chord. Otherwise, if there are mutliple chords, more nested for
loops and if
conditions add key centers to post_candidates
if they match all input chords. Then post_candidates
is assigned to the final candidates
set. The function returns candidates
as a set of key centers that are entirely compatible with the input chords.
The format_validate_chords()
function takes one or more chords as a string and a list of valid roots. The input string is split into a list, then interated through to validate input and remove underscores from natural note names. A ValidationError is raised if the chord name is not four or five characters long, or if the quality does not match "maj", "min" or "dim". The function returns a list of formatted chord names.
Selecting "6"
from the menu shows the activity log. This auxilliary feature displays all saved activities to the user via the activity_log.json
file. This file is opened in read mode and saved to a variable. This variable is iterated through with a for
loop and each entry is printed. An "end of log" messaged is printed at the end, so that printing empty logs do not confuse the user. Details shown are date, time, activities this session, activity selection, input and output.
Selecting "7"
from the menu clears all activity log content, which can get quite lengthy. This auxilliary feature defines a variable as an empty list, then opens activity_log.json
in write mode. The empty list is dumped into the file, overwriting any existing content. A confirmation message is then printed and the activity counter is rere-initialised.
Selecting "8"
from the menu calls the auxilliary feature which stops the application. An if/else
block checks if the user entered their personal information, and tailors the farewell message accordingly. A return
keyword then exits the main()
function, thus exiting the program.
Error handling is mostly done in the input formatting/validation module. As a result, the modules downstream from there do not require much error handling.
All five primary features depend on the format_validate_notes
function, and some others depend on format_validate_chords
where relevant.
The exceptions
package contains the validation_exception.py
and __init__.py
files. The ValidationError class simply defines an instance of the Exception class, inheriting its properties, and executes the pass
keyword. This class definition allows the Python files in this project to raise a customised exception.
This project was managed using Trello.
Link to Trello board: https://trello.com/b/RqY9G1RE/joelvontreifeldtt1a3
Progress shots of the Trello board are shown below:
To run this terminal application, follow these steps:
-
In the terminal, navigate to the
src
directory from theJoelvonTreifeldt_T1A3
root directory:
cd src
-
Be sure to start by manually running the following two lines in the terminal to make the shell files executable:
chmod +x run.sh
chmod +x check_python.sh
-
Run the shell file:
./run.sh