Overall, this script provides a simple way to input musical notes and have them played as sound through the computer's speakers. The notation allows for various musical constructs such as pauses, note duration modifiers, and holding parent notes.
Below is the relevant part of the entire code.
Examples:
A6 E3 A4A6 | E3 | A4 |< A6 E3 A4 >[ A6 E3 A4 ]- This does not work:
A6 + ( E3 A4 )
(Briefly, | stands for a pause, <...> indicates ritardando, [...] is accelerando.)
Explaination for 5.: What I want and can't achieve: Playing a parent-note while child/children-notes are playing sequentially. Here, E3 is the Parent-Note, A4, E3, A6 are Child-Notes.
[ A6 ]
------------
[ A4 E3 A6 ]
This shows that A6 is held till A4 and other keys are done playing. This is represented by A6 + ( A4 E3 A6 ). A6-Parent +Hold what notes (The notes in brackets A4 E3 A6-Child Notes )
Mixed example: A6 E3 < A4 > | [ A6 ] E3 + ( A4 E3 A6 )
What I tried: Threading (I tried, but to no avail) and another function to play the parent note for the period till the child notes are playing; but I'm apparently still too new at this.
import sounddevice as sd
import numpy as np
import time
import re
# Default duration for playing notes
default_duration = 0.5
# Default wait time between notes
default_wait_time = 0.1
# Default pause time for '|'
default_pause_time = 0.5
# Variable to store the last output
last_output = ""
# Flag to track if it's the first input
first_input = True
# Dictionary mapping musical notes to corresponding keyboard keys
note_to_key = {'A6': 'b', 'E3': '0', 'A4': 'p', '|' : '|', " ":" "}
# Dictionary mapping musical notes to their frequencies
frequencies = { 'A6': 1760.00, 'E3': 164.81, 'A4': 440.00}
# Dictionary mapping keyboard keys back to musical notes
key_to_note = {v: k for k, v in note_to_key.items()}
# Function to play a single note
def play_note(note, duration=None, wait_time=None, reverse_dict=False, hold_parent=None):
"""
Play a musical note or a sequence of notes.
Parameters:
- note: The note or sequence of notes to be played.
- duration: The duration of each note.
- wait_time: The time to wait between notes.
- reverse_dict: If True, reverse the note_to_key dictionary.
- hold_parent: If provided, hold the specified parent note.
Note: The function uses sounddevice to play audio and numpy for waveform generation.
"""
# Handling default values for duration and wait_time
if duration is None:
duration = default_duration
if wait_time is None:
wait_time = default_wait_time
# Check for special characters in the note
if '|' in note:
time.sleep(duration)
# Pause for the specified duration
elif '+' in note:
# Handle holding a parent note and playing child notes
parent_key, child_notes = note.split('+')
play_note(child_notes[:-1], duration=duration, hold_parent=parent_key)
# Handle hastening or slowing down notes within brackets
elif '[' in note or '<' in note:
if note[0] == '[':
duration_factor = 0.5
elif note[0] == '<':
duration_factor = 2.0
else:
duration_factor = 1.0
# Extract notes inside brackets
notes_inside = re.findall(r'\S+', note[1:-1])
for note_inside in notes_inside:
play_note(note_inside, duration=duration_factor * duration)
else:
# Play a single note
if reverse_dict:
# Reverse lookup in the key_to_note dictionary
try:
if note in key_to_note:
note = key_to_note[note]
else:
print(f"Note not found in reversed dictionary: {note}")
return
except KeyError:
print(f"Wrong Note/Key: {note}")
return
# Get the frequency of the note
frequency = frequencies[note]
fs = 44100
t = np.linspace(0, duration, int(fs * duration), False)
waveform = 0.5 * np.sin(2 * np.pi * frequency * t)
# Play the waveform using sounddevice
sd.play(waveform, fs)
sd.wait()
time.sleep(wait_time)
# Main loop for user interaction
while True:
prompt = ">" if not first_input else "Select Mode \n3. Play Notes\n7. Exit\nH: Help\n> "
mode = input(prompt)
first_input = False
# User wants to play notes
if mode == '3':
input_sequence = input("Enter Notes: ")
items = re.findall(r'\[.*?\]|\{.*?\}|<.*?>|\S+', input_sequence)
# Check if all items are valid notes or keys
if all(item in frequencies or item in note_to_key.values() or re.match(r'[\[<{].*?[\]>}]', item) for item in items):
# Play each item in the sequence
for item in items:
play_note(item)
else:
print("Invalid input. Please enter either piano notes or keyboard keys.")
# Display help information
elif mode.lower() == 'h':
print("'|' \tPauses Briefly \n<>' \tSlows Notes Within Them \n'[]' \tHastens Notes Within Them \n'+()' \tHolds Parent Within Them ")
elif mode == '7':
# User wants to exit the program
print("Exiting the program. Goodbye!")
break
else:
# Invalid mode selected
print("Invalid mode. Please enter 3, 7 or H.")
Problematic Code-Part (According to me):
def play_note(note, duration=None, wait_time=None, reverse_dict=False, hold_parent=None):
....
elif '+' in note:
parent_key, child_notes = note.split('+')
play_note(child_notes[:-1], duration=duration, hold_parent=parent_key)
End notes:
- Mixing the brackets and pause don't work for unknown reasons.
- The title might be irrelevant to what I am asking, but, I'll change if better suggestions are given.
- There might be some irrelevant code-pieces left, if so, I'll edit and remove them.
- QWERTY Inputs are not accepted, rather, refer to the notes/frequencies dictionaries.
5. I'm not a music/python major, just enthusiastic about both. I don't know the terms, so, anyone is free to correct the wrongs.
Excess information for clarity/ Breakdown of the script:
Default Settings
default_duration: Default duration for playing notes (0.5 seconds).default_wait_time: Default wait time between notes (0.1 seconds).Note and Frequency Mapping
note_to_key: Dictionary mapping musical notes to corresponding keyboard keys.frequencies: Dictionary mapping musical notes to their frequencies.Key to Note Mapping
key_to_note: Dictionary mapping keyboard keys back to musical notes.play_noteFunctionPlays a single note or a sequence of notes.
Parameters
note: The note or sequence of notes to be played.duration: The duration of each note.wait_time: The time to wait between notes.reverse_dict: If True, reverse thenote_to_keydictionary.hold_parent: If provided, holds the specified parent note.Main Loop
Enters a continuous loop for user interaction.
Asks the user to select a mode: play notes (3), display help (H), or exit (7).
User Interaction
If the user selects mode 3 (Play Notes): Prompts the user to enter a sequence of notes. Parses the input using regular expressions. Calls the
play_notefunction for each item in the sequence.Help Information
If the user selects mode H (Help), displays information about special characters used in the input sequence.
Exiting the Program
If the user selects mode 7 (Exit), prints a goodbye message and exits the program.
I went a little overboard here, because this covers a lot more than just the narrow problem of "parent notes".
first_inputneeds to go away.note_to_keyis useless, and you only need akey_to_note.It's not useful to have a frequencies table - it's more code than necessary, and less accurate than just calculating the exact frequency on the fly.
play_noteis troubled because it takes on more responsibilities than it should - in this case, parsing and playing.Not a good idea to
time.sleep, and also not a good idea to individuallysd.play(). You've probably noticed that even without await_time, there are gaps between the notes. This can be avoided with the use of a proper continuous buffer.Your use of
t = np.linspaceintroduces error because it includes the endpoint when it shouldn't.Rather than
sd.wait(), you can just enable blocking behaviour when youplay().I think it's a little obtrusive to ask for a numeric mode on every loop iteration. Exit is just via ctrl+C; help can be printed at the beginning, and in all cases you should just be in the equivalent of "play notes" mode.
The idea of a specialized "parent" note is not particularly useful. Instead, set up a tree where you have a generalised "sustain" binary operator that sets the left and right operands to play simultaneously.
The following does successfully fix the original "parent" problem, and departs a fair distance from the design of the original program. For instance, it can accommodate expressions like
which parses to
or even
or even
The tree-traversal code is not wonderful, but at these scales that doesn't make a performance difference.