<jupyter_start><jupyter_text>Visualization of the grid of chords<jupyter_code>from IPython.display import YouTubeVideo
YouTubeVideo("P1gGTMYBvgk")
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt, mpld3
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

mpl.rc('image', interpolation='nearest', cmap='Set2')
mpld3.enable_notebook()<jupyter_output><empty_output><jupyter_text>As a timeline grid<jupyter_code>all_half_beats = pd.read_csv('hladova-zed-half-beats.txt', header=None)[0]
le = LabelEncoder()
all_half_beats_numeric = le.fit_transform(all_half_beats)
all_half_beats_numeric
print('number of unique chords:', len(le.classes_))
print(le.classes_)
section_lengths = [2,2,2,2,4,2,4,2,2,4,7,2,1]
assert sum(section_lengths)*8 == len(all_half_beats)

section_classes = [
    'instrumental',
    'guitar_intro',
    'verse_A',
    'verse_A',
    'chorus_A',
    'verse_A',
    'chorus_B',
    'verse_B',
    'guitar_solo',
    'chorus_B',
    'verse_C',
    'instrumental',
    'ending'
]
assert len(section_classes) == len(section_lengths)
section_starts = np.cumsum([0]+section_lengths)[:-1]
le_sections = LabelEncoder().fit(section_classes)
le_sections_numeric = le_sections.transform(section_classes)
row_size = 8
row_count = len(all_half_beats_numeric) // row_size
df = pd.DataFrame({'section': le_sections_numeric}, index=section_starts)
df = df.reindex(np.arange(row_count))
df.fillna(method='ffill', inplace=True)
df['section'] = df['section'].astype(int)
df['section_name'] = df['section'].apply(le_sections.inverse_transform)
df
section_per_row = df['section']
# time: first columns top to bottom, then rows left to right
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 5))

fig.suptitle('Nerez: Hladova zed')

ax1.set_xlim(0, row_count)
ax2.set_xlim(0, row_count)
values = all_half_beats_numeric.reshape(-1, 8).T
mesh = ax1.pcolormesh(values, edgecolors='w', linewidth=0.5)
ax1.set_title('chords')
ax1.invert_yaxis()

css = """
.mpld3-tooltip {
    background: white;
    padding: 5px;
    border: 1px solid #aaa;
    border-radius: 2px;
}
"""
labels = [le.inverse_transform(i) for i in values.flatten()]
tooltip = mpld3.plugins.PointHTMLTooltip(mesh, labels, hoffset=10, voffset=10, css=css)
mpld3.plugins.connect(fig, tooltip)


mesh2 = ax2.pcolormesh(section_per_row.reshape(1, -1), edgecolors='w', linewidth=0.5)
tooltip = mpld3.plugins.PointHTMLTooltip(
    mesh2,
    labels=[le_sections.inverse_transform(i) for i in section_per_row],
    hoffset=10, voffset=10, css=css)
mpld3.plugins.connect(fig, tooltip)
ax2.set_title('sections')
ax2.set_yticks([])

plt.savefig('hladova-zed-grid.pdf')<jupyter_output><empty_output><jupyter_text>n-grams2-grams aligned to even indexes (ie. (0, 1) can be extracted, but (1,2) can not).4-grams aligned to indexes modulo 4, etc.<jupyter_code>def aligned_ngrams(values, n):
    return [tuple(values[i]) for i in np.arange(len(values)).reshape(-1, n)]

def aligned_ngram_set(values, n):
    return set(aligned_ngrams(values, n))
ngrams_1 = aligned_ngram_set(all_half_beats, 1)
ngrams_1
ngrams_2 = aligned_ngram_set(all_half_beats, 2)
ngrams_2
ngrams_4 = aligned_ngram_set(all_half_beats, 4)
ngrams_4
ngrams_8 = aligned_ngram_set(all_half_beats, 8)
ngrams_8
for ngrams in ngrams_1, ngrams_2, ngrams_4, ngrams_8:
    print(len(ngrams))<jupyter_output>9
12
10
8<jupyter_text>As a graph<jupyter_code>from graphviz import Digraph
import colorsys
dot = Digraph(graph_attr={'overlap': 'false'})
dot.attr('node', shape='box')
for n in ngrams_1:
    dot.node(n[0])

def draw_ngram_layer(ngrams, n):
    for t in ngrams:
        node = ' '.join(t)
        dot.node(node)
        stride = n//2
        sources = [' '.join((t[stride*i:stride*(i+1)])) for i in range(2)]
        for src in sources:
            dot.edge(src, node)

draw_ngram_layer(ngrams_2, 2)
draw_ngram_layer(ngrams_4, 4)

dot.node('verse_line_A')
dot.edge('Am7 Hm7 Em Hm7', 'verse_line_A')
dot.edge('Am7 Hm7 H7 H7', 'verse_line_B')

dot.node('verse_A')
dot.edge('verse_line_A', 'verse_A', '3')
dot.edge('verse_line_B', 'verse_A')

dot.node('verse_B')
dot.edge('verse_line_A', 'verse_B', '4')

dot.node('guitar_intro')
dot.edge('verse_A', 'guitar_intro')

dot.node('guitar_solo')
dot.edge('verse_A', 'guitar_solo')

dot.node('instrumental')
dot.edge('verse_A', 'instrumental')

dot.node('chorus_part_1_A')
dot.edge(' '.join(4*['Am6']), 'chorus_part_1_A')
dot.edge(' '.join(4*['Em9']), 'chorus_part_1_A')
dot.edge(' '.join(4*['Am6']), 'chorus_part_1_A')
dot.edge(' '.join(2*['H7/4'] + 2*['H7']), 'chorus_part_1_A')

dot.node('chorus_part_1_B')
dot.edge(' '.join(4*['Am6']), 'chorus_part_1_B')
dot.edge(' '.join(4*['Em9']), 'chorus_part_1_B')
dot.edge(' '.join(2*['Am6'] + 2*['F#7']), 'chorus_part_1_B')
dot.edge(' '.join(2*['H7/4'] + 2*['H7']), 'chorus_part_1_B')

dot.node('chorus_part_2')
dot.edge(' '.join(2*['Em'] + 2*['Hm7']), 'chorus_part_2')
dot.edge(' '.join(4*['Am7']), 'chorus_part_2')
dot.edge(' '.join(2*['H4/C'] + ['Hm7', 'Am7']), 'chorus_part_2')
dot.edge(' '.join(4*['Em']), 'chorus_part_2')

dot.node('chorus_A')
dot.edge('chorus_part_1_A', 'chorus_A')
dot.edge('chorus_part_2', 'chorus_A')

dot.node('chorus_B')
dot.edge('chorus_part_1_B', 'chorus_B')
dot.edge('chorus_part_2', 'chorus_B')

dot.node('verse_C')
dot.edge('verse_line_A', 'verse_C', '14')

dot.node('ending')
dot.edge(' '.join(4*['Em']), 'ending', '2')

dot.node('song')
dot.edge('instrumental', 'song')
dot.edge('guitar_intro', 'song')
dot.edge('verse_A', 'song')
dot.edge('verse_A', 'song')
dot.edge('chorus_A', 'song')
dot.edge('verse_A', 'song')
dot.edge('chorus_B', 'song')
dot.edge('verse_B', 'song')
dot.edge('guitar_solo', 'song')
dot.edge('chorus_B', 'song')
dot.edge('verse_C', 'song')
dot.edge('instrumental', 'song')
dot.edge('ending', 'song')
    
dot.render('hladova-zed-graph.gv')
dot<jupyter_output><empty_output><jupyter_text>Sequence of 8-grams Let's treat sections as sequences of unique 8-grams.<jupyter_code>le_8ngram = LabelEncoder().fit([' '.join(n) for n in ngrams_8])
all_ngrams_8 = [' '.join(n) for n in aligned_ngrams(all_half_beats, 8)]
all_ngrams_8
all_ngrams_8_numeric = le_8ngram.transform(all_ngrams_8)<jupyter_output><empty_output><jupyter_text>Plot of sequence unique 8-grams<jupyter_code>fig, ax = plt.subplots(nrows=1, figsize=(10, 3))
ax.grid(True)
scatter = ax.scatter(np.arange(len(all_ngrams_8_numeric)), all_ngrams_8_numeric, c=all_ngrams_8_numeric, s=200)
ax.set_xlim(-1, len(all_ngrams_8_numeric)+1)
ax.set_ylim(-1, len(set(all_ngrams_8_numeric))+1)

mpld3.plugins.connect(fig, mpld3.plugins.PointHTMLTooltip(
        scatter, labels=['[%d] %s' % (le_8ngram.transform(i), i) for i in all_ngrams_8],
        hoffset=10, voffset=10, css=css));

plt.savefig('hladova-zed-sequence-8-gram.pdf')
len(all_ngrams_8_numeric)
df['ngram_8'] = all_ngrams_8_numeric
df['ngram_8_name'] = all_ngrams_8
df
[
    ('guitar_intro', [3,4]),
    ('guitar_solo', [3,4]),
    ('instrumental', [3,4]),
    ('verse_A', [3,4]),
    ('verse_B', [3,3]),
    ('verse_C', [3,3,3,3,3,3,3]),
    ('chorus_A', [0,1,6,7]),
    ('chorus_B', [0,2,6,7]),
    ('ending', [2])
]<jupyter_output><empty_output><jupyter_text>State machine representation  Transitions between chords<jupyter_code>ngrams_1
def ngrams(values, n):
    return sorted(set(zip(values[:-1], values[1:])))

ngrams_2_unaligned = ngrams(all_half_beats, 2)
print(len(ngrams_2_unaligned))
ngrams_2_unaligned
def desc_by_src(ngrams_2):
    return pd.DataFrame(ngrams_2, columns=['src', 'desc']).groupby('src')['desc'].apply(list)

desc_by_src(ngrams_2_unaligned)
def colorize(values):
    norm = mpl.colors.Normalize(vmin=min(values), vmax=max(values))
    m = mpl.cm.ScalarMappable(norm = norm, cmap=mpl.cm.get_cmap()) 
    return [m.to_rgba(x)[:3] for x in values]
dot_ngrams_2 = Digraph(
    graph_attr={'rankdir': 'LR', 'concentrate': 'true'},
    node_attr={'width': '0.7', 'height': '0.7', 'shape': 'circle', 'fixedsize': 'true'},
    edge_attr={'arrowsize': '0.6'}
)
ngrams_2_unaligned_desc_by_src = desc_by_src(ngrams_2_unaligned)
# 8-gram ids in the original order of appearance
keys = list(pd.Series(all_half_beats).drop_duplicates())
colors = colorize(le.transform(keys))
for src, color_rgb in zip(keys, colors):
    dot_ngrams_2.node(src, color='%f, %f, %f' % colorsys.rgb_to_hsv(*color_rgb), style='filled')
for src, dests in ngrams_2_unaligned_desc_by_src.iteritems():
    for dest in dests:
        if src != dest:
            dot_ngrams_2.edge(src, dest)

dot_ngrams_2.render('hladova-zed-chord-state-diagram.gv')
dot_ngrams_2<jupyter_output><empty_output><jupyter_text>Total number of states (nodes) and state transitions (edges) is relatively small. So this might be quite easy to comprehend.On the other hand, we need to animate the current state (highlight the node) and transitions to other states. That still leaves us with visualizing a moment, without past and future. How to visualize past and future using this state graph?Let's make each node's $x$ position unique and represent time in $y$ axis. Then we can make a timeline where the current state is highlighted. There will be the static state graph + moving timeline, but both will share the x axis.  Transitions between 8-grams<jupyter_code># let's make unaligned 2-grams of encoded aligned 8-grams :)
ngrams_8_2 = ngrams(all_ngrams_8_numeric, 2)
print(len(ngrams_8_2))
print(ngrams_8_2)
print(desc_by_src(ngrams_8_2))
dot_ngrams_8 = Digraph(
    graph_attr={'rankdir': 'LR', 'margin': '0.2', 'nodesep': '0.25', 'ranksep': '0.3',
               'concentrate': 'true'},
    node_attr={'width': '0.7', 'height': '0.7', 'shape': 'circle', 'fixedsize': 'true'},
    edge_attr={'arrowsize': '0.6'}
)
ngrams_8_2_desc_by_src = desc_by_src(ngrams_8_2)
# 8-gram ids in the original order of appearance
keys = list(pd.Series(all_ngrams_8_numeric).drop_duplicates())
colors = colorize(keys)
for src, color_rgb in zip(keys, colors):
    dot_ngrams_8.node(str(src), color='%f, %f, %f' % colorsys.rgb_to_hsv(*color_rgb), style='filled')
for src, dests in ngrams_8_2_desc_by_src.iteritems():
    for dest in dests:
        if src != dest:
            dot_ngrams_8.edge(str(src), str(dest))

dot_ngrams_8.attr('node', shape='point', width='0.15')
dot_ngrams_8.node('start', '')
dot_ngrams_8.edge('start', str(all_ngrams_8_numeric[0]))
dot_ngrams_8.node('end', '')
dot_ngrams_8.edge(str(all_ngrams_8_numeric[-1]), 'end')


dot_ngrams_8.render('hladova-zed-8-gram-state-diagram.gv')
dot_ngrams_8
for i, name in enumerate(le_8ngram.classes_):
    print(i, ':', name)<jupyter_output>0 : Am6 Am6 Am6 Am6 Em9 Em9 Em9 Em9
1 : Am6 Am6 Am6 Am6 H7/4 H7/4 H7 H7
2 : Am6 Am6 Am6 F#7 H7/4 H7/4 H7 H7
3 : Am7 Hm7 Em Hm7 Am7 Hm7 Em Hm7
4 : Am7 Hm7 Em Hm7 Am7 Hm7 H7 H7
5 : Em Em Em Em Em Em Em Em
6 : Em Em Hm7 Hm7 Am7 Am7 Am7 Am7
7 : H4/C H4/C Hm7 Am7 Em Em Em Em
{'fraction_non_alphanumeric': 0.16880361590259202, 'fraction_numerical': 0.03449866248501061, 'mean_word_length': 5.932225063938619, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '134976', 'license': 'MIT', 'n_tokens_mistral': 4629, 'n_tokens_neox': 4459, 'n_words': 858, 'path': 'hladova-zed/visualize_chord_grid_hladova_zed.ipynb', 'repo_name': 'bzamecnik/music-viz', 'size': '10841'}