- 嘿,您可以播放歌曲X?
只是一个随机朋友来参加您主持的聚会的一个看似无辜的问题。但是,如果您学会了在童年时代弹钢琴并且偶尔只是为了好玩而弹奏钢琴,那可能会很痛苦。麻烦的是,我在耳边玩耍时绝对没有用,所以我唯一的演奏机会就是握住音乐表。因此,我的第一步是去和Google“ x 乐谱pdf”或“ x 人声得分pdf”,具体取决于 x 是什么。不幸的是,您经常遇到以下问题:
- 钢琴得分存在,但仅包含旋律本身 +一些非常有限的伴奏。
- 钢琴得分存在,但已发行。
- 钢琴根本没有版本。
您向要求您玩 X 的朋友报告了您的发现,他们会像:
- 哦,可惜我没有带吉他,我知道和弦,只是我不弹钢琴...
在这一点上,您可能会想着自己:啊,我很久以前就学到了一些音乐理论,仅仅通过阅读他们的名字就可以在钢琴上弹奏和弦,否则...它?
事实证明,如果您以前从未做过,那么它比观看适当的音乐得分要困难得多。您需要很好地思考以在精神上解码和弦(如果您遇到一些更复杂的符号的符号,这会变得更糟),并找到一种合理的方式来用两只手玩它们。我可以向您保证,您将发现自己是混乱的。 Indeed, it's not immediately obvious that in the chord sequence you can just move one single finger by a tone or semitone instead of shifting the entire hand (or rather, two hands) to the right by a minor or major third.
This is where I remembered I was a software engineer after all, not a musician. So, instead of practicing I decided to simply write a script that will make my life easier.
The rest of this article is broken into four parts:
- Working with guitar chords in Python
- Generating a piano score
- Optimizing piano chord representation
- Assembling everything together
I chose Python as a programming language here because, as you could probably guess, there are myriads of libraries in Python for working with music. I found koude00最简单的是使用吉他和弦和koude1生成钢琴得分。不过,只能使用Music21来完成所有操作,因为它非常强大,但是我发现将两个库的混合比使用Music21实现Mingus所需的功能更容易。
。无论如何,您在下面看到的大多数代码都是使用Google Colaboratory作为IDE编写的,您可以在文章末尾找到指向它的链接。
与吉他和弦一起工作
正如人们所说的,有多种方法可以使猫皮肤。同样,大多数和弦都有几种吉他的表示,即使大多数业余吉他手通常会使用每个和弦的单一表示。 mingus
具有一个不错的功能,可以生成所有合理的演奏方式:
from mingus.extra.tunings import get_tuning
from mingus.containers import NoteContainer, Note
guitar_tuning = get_tuning('guitar', 'standard')
guitar_tuning.find_chord_fingering(NoteContainer().from_chord('Dbm6'))
[[0, 1, 2, 1, 2, 0],
[0, 1, 2, 1, 2, 4],
[6, 7, 6, 6, 9, 6],
[6, 7, 8, 6, 9, 6],
[6, 7, 6, 6, 9, 9],
[9, 7, 6, 6, 9, 6],
[9, 11, 11, 9, 11, 9],
[12, 11, 11, 13, 11, 12],
[0, 1, None, 1, 2, 0],
[0, 1, 2, 1, 2, None],
[0, 1, None, 1, 2, 4],
[6, 7, 6, 6, None, 6],
[6, 7, 6, 6, 9, None],
[6, 7, 6, 6, None, 9],
[6, 7, None, 6, 9, 6],
[9, 7, 6, 6, None, 6],
[None, 7, 6, 6, 9, 6],
[9, 11, None, 9, 11, 9],
[12, 11, 11, None, 11, 12],
[12, 11, 11, 13, 11, None],
[None, 11, 11, 13, 11, 12],
[0, 1, None, 1, 2, None],
[6, 7, 6, 6, None, None],
[None, 7, 6, 6, None, 6],
[12, 11, 11, None, 11, None],
[None, 11, 11, None, 11, 12]]
这里的数字对应于每个字符串沿吉他颈部的位置。为了使实际的音高与每个手指相对应,我们可以使用guitar_tuning.get_Note
:
for fingering in guitar_tuning.find_chord_fingering(NoteContainer().from_chord('Dbm6')):
chord_pitches = [ guitar_tuning.get_Note(string=i, fret=fingering[i]) if fingering[i] is not None else None for i in range(6) ]
print(chord_pitches)
['e-2','a#-2','e-3','g#-3','c#-4','e-4'] ...
['E-2', 'A#-2', 'E-3', 'G#-3', 'C#-4', 'E-4']
['E-2', 'A#-2', 'E-3', 'G#-3', 'C#-4', 'G#-4']
['A#-2', 'E-3', 'G#-3', 'C#-4', 'G#-4', 'A#-4']
['A#-2', 'E-3', 'A#-3', 'C#-4', 'G#-4', 'A#-4']
['A#-2', 'E-3', 'G#-3', 'C#-4', 'G#-4', 'C#-5']
['C#-3', 'E-3', 'G#-3', 'C#-4', 'G#-4', 'A#-4']
['C#-3', 'G#-3', 'C#-4', 'E-4', 'A#-4', 'C#-5']
['E-3', 'G#-3', 'C#-4', 'G#-4', 'A#-4', 'E-5']
['E-2', 'A#-2', None, 'G#-3', 'C#-4', 'E-4']
['E-2', 'A#-2', 'E-3', 'G#-3', 'C#-4', None]
['E-2', 'A#-2', None, 'G#-3', 'C#-4', 'G#-4']
['A#-2', 'E-3', 'G#-3', 'C#-4', None, 'A#-4']
['A#-2', 'E-3', 'G#-3', 'C#-4', 'G#-4', None]
['A#-2', 'E-3', 'G#-3', 'C#-4', None, 'C#-5']
['A#-2', 'E-3', None, 'C#-4', 'G#-4', 'A#-4']
['C#-3', 'E-3', 'G#-3', 'C#-4', None, 'A#-4']
[None, 'E-3', 'G#-3', 'C#-4', 'G#-4', 'A#-4']
['C#-3', 'G#-3', None, 'E-4', 'A#-4', 'C#-5']
['E-3', 'G#-3', 'C#-4', None, 'A#-4', 'E-5']
['E-3', 'G#-3', 'C#-4', 'G#-4', 'A#-4', None]
[None, 'G#-3', 'C#-4', 'G#-4', 'A#-4', 'E-5']
['E-2', 'A#-2', None, 'G#-3', 'C#-4', None]
['A#-2', 'E-3', 'G#-3', 'C#-4', None, None]
[None, 'E-3', 'G#-3', 'C#-4', None, 'A#-4']
['E-3', 'G#-3', 'C#-4', None, 'A#-4', None]
[None, 'G#-3', 'C#-4', None, 'A#-4', 'E-5']
请注意,黑色笔记(或更确切地说是与钢琴键盘上的黑键相对应的音符)现在始终带有 , never with a . The notes for the chord should technically consist of , while would refer to . This doesn't matter much for a guitar — after all, the pitches are the same, however, it gets much more important for a piano score since the chords should match the current key signatures. I will show how to address this issue in the next section.
产生钢琴得分
用于使用钢琴分数,我将使用库music21
。
这是手动生成钢琴分数的最基本方法:
import music21 as m21
sc1 = m21.stream.Score()
right_hand = m21.stream.PartStaff()
left_hand = m21.stream.PartStaff()
right_hand.append(m21.key.Key('Ab', 'major'))
right_hand.append(m21.note.Note('D-4'))
right_hand.append(m21.note.Note('F-4'))
right_hand.append(m21.note.Note('A-4'))
right_hand.append(m21.note.Note('B--4'))
left_hand.append(m21.chord.Chord(
[m21.note.Note('D-3'), m21.note.Note('A-3')],
duration=m21.duration.Duration(4)))
sc1.insert(0, right_hand)
sc1.insert(0, left_hand)
sc1.insert(0, m21.layout.StaffGroup([right_hand, left_hand], symbol='brace'))
sc1.show()
您已经可以注意到music21
和mingus
的一些差异,它们表示注释:
-
music21
使用#
或-
进行更改标记; -
mingus
使用#
和b
,而-
只是音符和八度之间的分离器。
如果您尝试简单地按名称从一个库中映射到另一个库的注释,这可能会产生有趣的效果:
mingus_note = Note('A-6')
print(mingus_note.to_hertz())
print(m21.pitch.Pitch(str(mingus_note).strip("'")).freq440) # BAD!
print(m21.pitch.Pitch(mingus_note.name, octave=mingus_note.octave).freq440) # GOOD
print()
# Music21 can parse flats at the input (bemols)
mingus_note = Note('Db-5')
print(mingus_note.to_hertz())
print(m21.pitch.Pitch(mingus_note.name, octave=mingus_note.octave).freq440)
print()
# ... but not double-flats.
mingus_note = Note('Dbb-5')
print(mingus_note.to_hertz())
try:
print(m21.pitch.Pitch(mingus_note.name, octave=mingus_note.octave).freq440)
except Exception as e:
print(e)
上述块的输出为:
1760.0
1661.218790319782
1760.000000000002
554.3652619537442
554.3652619537443
523.2511306011972
bb is not a supported accidental type
现在,让我们定义一些简单的语言来描述我们的和弦序列:每个和弦都将被指定为duration:chord_symbol
,其中duration
在crotchets(季度)中指定了duration
,chord_symbol
用吉他符号:
4:Am 4:H7 2:E 2:E7 8:Am 4:H7 2:E 2:E7 4:Am
您可能在这里被H注释所困扰 - 是的,这首歌显然遵循了德国和弦符号的传统。德语和英文音符名称之间的唯一重要区别是B和H:
英语 | 德语 |
---|---|
Converting from German to English is then pretty trivial:
Here is what we will do — take the first fingering proposed by koude0, play the lower 3 strings with the left hand and the upper 3 strings with the right hand.
code
koude21
正如预期的那样,通过和弦 - 音调序列,我们丢失了有关和弦正确改动标记的所有信息。在上面的示例中,您可能会注意到 is represented as . So let's try to get things right by altering the notes in the chord to valid enharmonics:
code
koude22
这要好得多,但是我们仍然无法控制时间签名 - 是 by default, or the key signature — it's (or rather, , like in the example above). To correct this we will allow adding arbitrary TinyNotation元素都归因于这两个五线段。由于TinyNotation不支持关键更改,因此我们将扩展其功能。
新解析器将支持分数描述:
[kDm 3/4] 2:F 1:Gm 1:Am 2:AmM7 6:Dm
同时,当我们使用它时,我们还将解析和渲染分开,因为以后我们将在它们之间有一个优化步骤:
更多代码
import re
import functools
import copy
class KeyToken(m21.tinyNotation.Token):
def parse(self, parent):
keyName = self.token
return m21.key.Key(keyName)
class BaseElement:
def add_to_score(self, score: m21.stream.Score): ...
@functools.cache
def get_harmony_and_implementations(chord_symbol: str):
harmony = get_harmony_symbol_from_name(chord_symbol)
mingus_chord = NoteContainer().from_chord(chord_symbol)
return harmony, tuple(
get_hands_from_notes(
fix_enharmonisms(
notes_from_fingering(fingering),
harmony
),
)
for fingering in guitar_tuning.find_chord_fingering(mingus_chord)
)
class ChordElement(BaseElement):
def __init__(self, token: str):
duration, chord_symbol = token.split(':')
self.duration = m21.duration.Duration(quarterLength=float(duration))
chord_symbol = de_germanize(chord_symbol)
self.harmony, self.implementations = get_harmony_and_implementations(chord_symbol)
self.best_implementation_index = 0
def add_to_score(self, score: m21.stream.Score):
chord_2h = self.implementations[self.best_implementation_index]
right_hand, left_hand = score.parts
right_hand.append(copy.deepcopy(self.harmony)) # Copy to prevent "object already added"
right_hand.append(chord_or_rest_from_notes(chord_2h.right_hand_notes, duration=self.duration))
left_hand.append(chord_or_rest_from_notes(chord_2h.left_hand_notes, duration=self.duration))
class MetadataElement(BaseElement):
def __init__(self, title: str):
self.metadata = m21.metadata.Metadata(title=title)
def add_to_score(self, score: m21.stream.Score):
score.insert(0, self.metadata)
class RemarkElement(BaseElement):
def __init__(self, text: str):
self.expression = m21.expressions.TextExpression(text)
self.expression.placement = 'above'
def add_to_score(self, score: m21.stream.Score):
score.parts[0].append(self.expression)
class TinyNotationElement(BaseElement):
def __init__(self, token: str):
assert(token[0] == '[' and token[-1] == ']')
tnc = m21.tinyNotation.Converter(makeNotation=False)
tnc.tokenMap.append((r'k(.*)', KeyToken))
tnc.load(token[1:-1])
tnc.parse()
self.stream = tnc.stream
def add_to_score(self, score: m21.stream.Score):
for sub_element in self.stream:
for part in score.parts:
part.append(sub_element)
def parse_chords(chords_string: str) -> List[BaseElement]:
result = []
PATTERNS = (
('TINY', r'\[[^\]]+\]'),
('TITLE', r'TITLE\[(?P<TITLE_TEXT>[^\]]+)\]'),
('REMARK', r'REMARK\[(?P<REMARK_TEXT>[^\]]+)\]'),
('CHORD', r'\d+(\.\d*)?:\S+'),
('SPACE', r'\s+'),
('ERROR', r'.')
)
full_regex = '|'.join(f'(?P<{kind}>{expression})' for kind, expression in PATTERNS)
for mo in re.finditer(full_regex, chords_string):
token = mo.group()
kind = mo.lastgroup
if kind == 'TINY':
result.append(TinyNotationElement(token))
elif kind == 'TITLE':
result.append(MetadataElement(mo.group('TITLE_TEXT')))
elif kind == 'REMARK':
result.append(RemarkElement(mo.group('REMARK_TEXT')))
elif kind == 'CHORD':
result.append(ChordElement(token))
elif kind == 'SPACE':
pass
else: # Error
raise RuntimeError(f'Unexpected token at position {mo.start()}: "{chords_string[mo.start():]}"')
return result
def score_from_chords(chords_string: str):
elements = parse_chords(chords_string)
score, _, _ = init_score_and_hands()
for element in elements:
element.add_to_score(score)
return score
score_from_chords('TITLE[Song of a page] [kDm 3/4] 2:F 1:Gm 1:Am 2:AmM7 REMARK[Arpeggios] 6:Dm').show()
优化钢琴和弦表示
为了选择最佳和弦序列,我们将使用以下模型:
- 每个和弦实现都将具有相关的计算困难。
- 在分数中相互追随两个和弦实现之间的过渡也将有一个计算的难度。
我们旨在最大程度地减少所有和弦和过渡的总难度。这可以使用动态编程方法或图中的最短路径搜索完成。
无论如何,让我们从和弦难度开始,因为这样的优化变得琐碎 - 只需从列表中选择最佳和弦:
def chord_length_penalty(chord_2h: ChordForTwoHands) -> int:
num_notes = len(chord_2h.left_hand_notes) + len(chord_2h.right_hand_notes)
# Prefer chords with 5 notes across both hands and but force at least 3.
return [400, 300, 200, 15, 5, 0, 2][num_notes]
def clef_mismatch_penalty(chord_2h: ChordForTwoHands) -> int:
middle_c = m21.pitch.Pitch('C4')
req_bars = 0
if chord_2h.left_hand_notes:
req_bars += max(chord_2h.left_hand_notes[-1].diatonicNoteNum - middle_c.diatonicNoteNum, 0)
if chord_2h.right_hand_notes:
req_bars += max(middle_c.diatonicNoteNum - chord_2h.right_hand_notes[0].diatonicNoteNum, 0)
return req_bars * 3
def duplicate_note_penalty(chord_2h: ChordForTwoHands) -> int:
all_notes = chord_2h.left_hand_notes + chord_2h.right_hand_notes
# Make sure we don't attempt to put the same
if len(all_notes) != len(set(x.diatonicNoteNum for x in all_notes)):
return 100
return 0
def is_white_key(p: m21.pitch.Pitch) -> bool:
return p.pitchClass not in (1, 3, 6, 8, 10)
def rakhmaninoff_penalty(pitches: Sequence[m21.pitch.Pitch]) -> int:
if not pitches:
return 0
# Slightly penalize chords above one octave in span.
if pitches[0].ps - pitches[-1].ps > 12.0:
return 1
else:
return 0
def black_thumb_penalty(pitches: Sequence[m21.pitch.Pitch]) -> int:
if len(pitches) < 2 or is_white_key(pitches[0]):
return 0
if len(pitches) == 2 and pitches[0].ps - pitches[1].ps <= 7.0:
# Assume that up to pure fifth we can play the interval with other fingers.
return 0
else:
return 4
def black_pinkie_penalty(pitches: Sequence[m21.pitch.Pitch]) -> int:
# Penalize 5th finger on black keys when the middle key is white
if len(pitches) < 3:
# No middle key means many good options to play it.
return 0
if is_white_key(pitches[-1]) or not is_white_key(pitches[-2]):
return 0
return 6
def hand_penalty(pitches: Sequence[m21.pitch.Pitch]) -> int:
return rakhmaninoff_penalty(pitches) + black_thumb_penalty(pitches) + black_pinkie_penalty(pitches)
# Since get_harmony_and_implementations is also cached,
# We will only compute difficulty per every chord implementation at most once.
@functools.cache
def chord_difficulty(chord_2h: ChordForTwoHands) -> float:
return (
hand_penalty(chord_2h.left_hand_notes) +
hand_penalty(chord_2h.right_hand_notes[::-1]) +
duplicate_note_penalty(chord_2h) +
chord_length_penalty(chord_2h) +
clef_mismatch_penalty(chord_2h)
)
def optimize_chords(elements: Sequence[BaseElement]):
for element in elements:
if isinstance(element, ChordElement):
difficulties = list(map(chord_difficulty, element.implementations))
element.best_implementation_index = min(range(len(difficulties)), key=difficulties.__getitem__)
def score_from_chords(chords_string: str):
elements = parse_chords(chords_string)
optimize_chords(elements)
score, _, _ = init_score_and_hands()
for element in elements:
element.add_to_score(score)
return score
score_from_chords('2:Gm 2:A7 2:Dm 2:B 2:Gm 2:Gm6 2:A7 2:Dm').show()
现在让我们建模两个和弦之间移动手的困难。我们将假设和弦之间的距离与其相应说明之间的距离之和相同。如果和弦有不同的长度,这显然将行不通,因此我们将尝试使用动态编程来匹配从一个和弦到另一和弦的笔记:
import numpy as np
def pitch_distance(first: m21.pitch.Pitch, second: m21.pitch.Pitch):
return abs(int(first.ps) - int(second.ps))
def hand_distance(first: Sequence[m21.pitch.Pitch], second: Sequence[m21.pitch.Pitch]):
if len(first) < len(second):
first, second = second, first
n = len(first)
m = len(second)
# m <= n
ADD_REMOVE_PENALTY = 1
d = np.ones((n + 1, m + 1), dtype=int) * 100000
d[:, 0] = np.arange(n + 1) * ADD_REMOVE_PENALTY
for i in range(1, n + 1):
for j in range(1, m + 1):
d[i, j] = min(
d[i - 1, j] + ADD_REMOVE_PENALTY,
d[i-1, j-1] + pitch_distance(first[i - 1], second[j - 1])
)
return d[n, m]
@functools.cache
def chord_distance(previous_chord_2h: ChordForTwoHands, current_chord_2h: ChordForTwoHands) -> int:
return (
hand_distance(previous_chord_2h.left_hand_notes, current_chord_2h.left_hand_notes) +
hand_distance(previous_chord_2h.right_hand_notes, current_chord_2h.right_hand_notes)
)
让我们表示单个和弦实施的复杂性(chord_difficulty
)为
and complexity of the transition between two chords (koude27) as
. Let
be
. Then the graph used for optimization of the chord sequence will look like this:
现在,我们的优化问题等同于在此图中找到最短路径。我正在使用Module networkx
的Dijkstra算法的实现,但是由于该图的性质(它没有循环并且具有明显的拓扑顺序),如果您找到了更有效的方法。
代码
import networkx as nx
def optimize_chords(elements: Sequence[BaseElement]):
graph = nx.DiGraph()
graph.add_node('source')
last_layer = ['source']
for index, element in enumerate(elements):
if isinstance(element, ChordElement):
new_last_layer = []
for impl_idx, impl in enumerate(element.implementations):
n = f'{index}-{impl_idx}'
graph.add_node(n, element=element, implementation_index=impl_idx, implementation=impl)
for prev_node in last_layer:
graph.add_edge(prev_node, n)
new_last_layer.append(n)
last_layer = new_last_layer
graph.add_node('target')
for n in last_layer:
graph.add_edge(n, 'target')
# Use weight function instead of explicit vertex/edge weights because
# this will allow to perform difficulty/distance computation lazily.
def weight_func(u, v, e):
u_attr = graph.nodes[u]
v_attr = graph.nodes[v]
KEY = 'implementation'
if KEY in v_attr:
w = chord_difficulty(v_attr[KEY])
if KEY in u_attr:
w += chord_distance(u_attr[KEY], v_attr[KEY])
return w
else:
return 0
path = nx.algorithms.dijkstra_path(graph, 'source', 'target', weight=weight_func)
for node in path:
attrs = graph.nodes[node]
if 'element' in attrs:
attrs['element'].best_implementation_index = attrs['implementation_index']
score_from_chords('2:Gm 2:A7 2:Dm 2:B 2:Gm 2:Gm6 2:A7 2:Dm').show()
将所有内容组装在一起
我们终于准备好对一首完整的歌曲进行测试:
score = score_from_chords("""
TITLE[Song of a page]
REMARK[transpose up by one tone (+2),
if the instrument supports it]
[kAm]
4:Am 4:H7 2:E 2:E7 8:Am
4:H7 2:E 2:E7 4:Am
2:Dm 2:G7 2:C 2:F 2:Dm 2:E7 4:Am
2:Gm 2:A7 2:Dm 2:B 2:Gm 2:Gm6 2:A7 2:Dm REMARK[(da capo)] [2/4] 2:Dm6
REMARK[(ending)] [4/4] 4:Gm 4:H7 4:E7 1:Am 1:E 2:Am
""").show()
可以找到完整的代码here,随意复制和播放。
本文旨在显示概念证明。因此,完全期望此代码不会适合每首歌。这是该原型中一些重要的缺乏特征:
- 带有低音符号的和弦(就像 < SPAN class =“ Strut” style =“高度:1EM;垂直align:-0.25em;”> e/a )
- 休息
- 歌词
- 重复(然后整个优化问题变得更加困难)
- 将和弦的长度调整到其持续时间
其中一些相对实施相对微不足道,而另一些(例如低音符号和重复)需要更深层的更改,甚至可能完全重新设计整个方法。
尽管如此,非常感谢您的阅读,直到最后,如果我设法激发您为音乐发电的代码写一些代码,甚至只是为了清理您可以演奏的任何音乐乐器的灰尘。<<<,我会很高兴。 /p>