-
Notifications
You must be signed in to change notification settings - Fork 1
/
music.py
226 lines (182 loc) · 6.83 KB
/
music.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""
Music: for creating and playing music
Steps are relative to A.
Registers are in ovtaves with the lowest pitch of 55.
"""
from clay.env import is_posix as _is_posix
if _is_posix():
raise NotImplementedError('music can only be used on Windows')
from collections import OrderedDict as _od
import itertools as _it
import os as _os
import winsound as _ws
from clay import settings
from clay.files.core import save as _save
from clay.shell.core import set_title, notify as _notify
# scale types
NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10, 12]
MAJOR = [0, 2, 4, 5, 7, 9, 11, 12]
WHOLE_TONE = list(range(0, 13, 2))
# key signatures
KEY_SIGNATURE = {'flats': 'BEADGCF',
'sharps': 'FCGDAEB'}
# create steps from A dict
_letters = 'A A# B C C# D D# E F F# G G#'.split()
_counter = _it.count()
STEP_DICT = {note: next(_counter) for note in _letters}
# set enharmonic equals
STEP_DICT['Bb'] = STEP_DICT['A#']
STEP_DICT['Db'] = STEP_DICT['C#']
STEP_DICT['Eb'] = STEP_DICT['D#']
STEP_DICT['Gb'] = STEP_DICT['F#']
STEP_DICT['Ab'] = STEP_DICT['G#']
# data
LEN_FACTOR = 963
# create register frequencies
REGS = _od()
for _num in range(7):
REGS[str(_num + 1)] = 55 * 2 ** _num
def get_hertz(reg, key):
"""Returns the hertz for the given key and register"""
return int(reg * 2 ** (key / 12))
class Note(object):
"""Class Note can be used to keep track of information about a note."""
def __init__(self, name='A4', length=0):
"""Default note is A 440 with length 0"""
self.name = name
self.length = length
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%s)' % (self.__class__.__name__, '{}, {}'.format(self.name, self.length))
from winsound import Beep as note # alias
def play_note(note, register, length, tempo):
"""Plays note of type str or int at the given register with length and tempo"""
if isinstance(note, str):
note = STEP_DICT[note]
_ws.Beep(get_hertz(REGS[register], note), int(length * 60 / tempo * LEN_FACTOR))
def play_scale(notes, tempo, register='4'):
"""Plays a scale from the given notes, tempo, default register is 4"""
for note in notes:
play_note(note, register, 1, tempo)
def play_scale_full(scale, tempo):
"""Plays a full scale of the given notes at the given tempo"""
offset = STEP_DICT[scale.split()[0]]
if scale.lower().endswith('major'):
scale = [x + offset for x in MAJOR]
else:
scale = [x + offset for x in NATURAL_MINOR]
scale += scale[::-1][1:] # decending notes
play_scale(scale, tempo)
def play_scale_half(notes, tempo):
"""Plays a half scale of the given notes at the given tempo"""
play_scale(notes, tempo)
class Song(object):
"""
A class for writing and storing music.
Receives optional file name, selection, and subdivision
Defaults:
file :: type str
selected :: int(0)
sub :: float(0.25)
tempo :: int(60)
Gives functionality including:
writing a note
editing a note
setting the subdivision
deleting notes
"""
def __init__(self, selected=0, sub=0.25):
"""Initializes a new Song object from the given file name"""
self.selected = selected
self.sub = round(sub, 4) # 64th note length limit
self.load()
def change_length(self, direction):
"""
Changes the length of the selected note
by the subdivision in the given direction
"""
self.notes[self.selected] += self.sub * direction
def delete(self):
"""Deletes the selected note from this song"""
self.notes.pop(self.selected)
self.selected -= 1
@staticmethod
def get_note():
"""Prompts the user for a note name and length value"""
name = input('Note? ').strip()
length = eval(input('Length? ').strip())
return name, length
def load(self):
"""Loads a notes file written in standard format"""
file = 'notes{:03d}.txt'.format(int(input('load song #? ')))
notes = []
tempo = settings.SONG_TEMPO
if file == 'new':
_notify('Creating a new template...', 0.5)
tempo = eval(input('tempo? '))
while not _os.path.exists(file) and file != 'new':
print('Path doesn\'t exist, try again')
file = 'notes{:03d}.txt'.format(input('load song #? '))
if _os.path.exists(file):
with open(file) as load:
rd = load.read().strip().split('\n')
for line in rd[:-1]:
parts = line.split()
notes.append(Note(parts[0], eval(parts[1])))
tempo = int(rd[-1])
self.file = file
self.notes = notes
self.tempo = tempo
def mark(self, edit=False):
"""Creates a new note and appends it to this song"""
name, length = self.get_note()
if (self.at_end() or not(self.is_populated())) and not(edit): # add note
self.notes.append(Note(name, length))
if len(self.notes) > 1:
self.selected += 1
else: # modify note
self.notes[self.selected].name = name
self.notes[self.selected].length = length
def play(self):
"""Plays this song using Windows' Beep"""
for note in self.notes:
play_note(note.name[:-1], note.name[-1], note.length, self.tempo)
def save(self):
"""Saves this song to a new file"""
text = '\n'.join(('{} {}'.format(note.name, note.length) for note in self.notes)) + '\n' + str(self.tempo)
_save(text, 'notes.txt', append_epoch=False)
set_title(add='File saved')
def select(self, direction):
"""Moves the selection up or down based on the given direction"""
if direction == 'up':
self.selected -= 1
elif direction == 'down':
self.selected += 1
else:
print('Illegal Argument Exception, try "up" or "down"')
def set_sub(self, sub=0):
"""Prompts and sets the subdivision for this song. Cannot be less than 1/64th"""
while sub < 1 / 64: # 64th notes at minimum
sub = float(input('new sub? ').strip())
self.sub = sub
def set_tempo(self):
"""Prompts and sets the tempo for this song"""
self.tempo = int(input('new tempo? ').strip())
@property
def is_at_end(self):
"""
Returns True if the selection is at the end of the song,
False otherwise
"""
return self.selected + 1 >= len(self.notes)
@property
def is_populated(self):
"""Returns True if this song is populated, False otherwise"""
return bool(self.notes)
if __name__ == '__main__':
t = 120
play_scale_full('C# major', t)
play_scale_half(NATURAL_MINOR, t)
print('BEEP!')
note(1530, 100)