Skip to content

jevontrei/JoelvonTreifeldt_T1A3

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JoelvonTreifeldt_T1A3

Musical Analysis Terminal Application

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.

R1

N/A.

R2

N/A.

R3

See R5 below for references.

R4

Link to public GitHub repository: https://github.com/jevontrei/JoelvonTreifeldt_T1A3

R5

The Python code in this program was styled according to the Python Enhancement Proposals 8 (PEP-8) rules using the VSCode extension "autopep8".

Sources

Features

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.

X. The main.py file and main() function

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.

Package/module details

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.

0. Secret bonus level

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.

1. Find a melody's key

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.

Package/module details

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.

2. Build a pentatonic scale

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.

Package/module details

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.

3. Find a specific chord within a key

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.

Package/module details

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.

4. Build a chord scale

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.

Package/module details

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.

5. Analyse a chord progression

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.

Package/module details

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.

6. See activity log

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.

7. Clear activity log

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.

8. Exit

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.

R6

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.

R7

This project was managed using Trello.

Link to Trello board: https://trello.com/b/RqY9G1RE/joelvontreifeldtt1a3

Screenshots

Progress shots of the Trello board are shown below:

trello-1 trello-2 trello-3 trello-4 trello-5 trello-6 trello-7 trello-8 trello-9 trello-10 trello-11 trello-12 trello-13 trello-14 trello-15 trello-16 trello-17 trello-18 trello-19

R8

To run this terminal application, follow these steps:

  1. In the terminal, navigate to the src directory from the JoelvonTreifeldt_T1A3 root directory:
    cd src

  2. 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

  3. Run the shell file:
    ./run.sh

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages