# ------------------- Standard tuning -------------------
STD_TUNING = {
'E': 40,
'A': 45,
'D': 50,
'G': 55,
'B': 59,
'e': 64
}
NST_TUNING = {
6: 36,
5: 43,
4: 50,
3: 57,
2: 64,
1: 67
}
STRINGS = ['E', 'A', 'D', 'G', 'B', 'e']
NST_TO_STD = {
6: 'E',
5: 'A',
4: 'D',
3: 'G',
2: 'B',
1: 'e'
}
# ------------------- pitch -------------------
def pitch_from_std(string, fret):
return STD_TUNING[string] + fret
def nst_candidates(pitch):
out = []
for s in range(1, 7):
fret = pitch - NST_TUNING[s]
if 0 <= fret <= 24:
out.append((s, fret))
return out
# ------------------- parsing -------------------
def parse_tab(tab):
lines = [l for l in tab.split("\n") if "|" in l]
parsed = {}
for l in lines:
parsed[l[0]] = l.split("|")[1]
width = max(len(v) for v in parsed.values())
columns = []
i = 0
while i < width:
notes = []
for s in STRINGS:
line = parsed[s]
if i >= len(line):
continue
ch = line[i]
if ch.isdigit():
num = ch
j = i + 1
while j < len(line) and line[j].isdigit():
num += line[j]
j += 1
notes.append((s, int(num)))
if notes:
columns.append(notes)
i += 1
return columns
# ------------------- position clustering -------------------
def get_position(state):
if not state:
return 0
return sum(f for _, f in state) / len(state)
# ------------------- DP optimizer -------------------
def optimize(columns):
from itertools import product
dp = [{} for _ in range(len(columns) + 1)]
dp[0][()] = (0, None)
for i, col in enumerate(columns):
for state, (cost, _) in dp[i].items():
options = []
valid = True
for (s_std, fret_std) in col:
pitch = pitch_from_std(s_std, fret_std)
cand = nst_candidates(pitch)
if not cand:
valid = False
break
options.append(cand)
if not valid:
continue
prev_pos = get_position(state)
for combo in product(*options):
new_cost = cost
for j, (s, f) in enumerate(combo):
new_cost += f * 0.1
if state:
prev = state[min(j, len(state) - 1)]
new_cost += abs(prev[1] - f) * 0.2
new_cost += abs(prev[0] - s) * 0.3
combo_pos = sum(f for _, f in combo) / len(combo)
new_cost += abs(combo_pos - prev_pos) * 1
combo_state = tuple(combo)
if combo_state not in dp[i + 1] or new_cost < dp[i + 1][combo_state][0]:
dp[i + 1][combo_state] = (new_cost, state)
dp[i + 1] = dict(sorted(dp[i + 1].items(), key=lambda x: x[1][0])[:300])
if not dp[-1]:
return []
best = min(dp[-1], key=lambda s: dp[-1][s][0])
result = []
cur = best
for i in range(len(columns), 0, -1):
result.append(cur)
cur = dp[i][cur][1]
result.reverse()
return result
# ------------------- rendering -------------------
def render_solution(columns, solution):
if not solution:
return "NO SOLUTION FOUND"
COL_WIDTH = 4
grid = {s: ['-' * COL_WIDTH for _ in range(len(solution))] for s in STRINGS}
for i, state in enumerate(solution):
for (nst_string, fret) in state:
s = NST_TO_STD[nst_string]
grid[s][i] = str(fret).center(COL_WIDTH, '-')
display_order = ['e', 'B', 'G', 'D', 'A', 'E']
return "\n".join(f"{s}|{''.join(grid[s])}|" for s in display_order)
# ------------------- pipeline -------------------
def convert(tab):
cols = parse_tab(tab)
sol = optimize(cols)
return render_solution(cols, sol)
# ------------------- INPUT HANDLING (FIXED) -------------------
if __name__ == "__main__":
print("Paste 6-string tab (press Enter twice to finish):\n")
lines = []
while True:
try:
line = input()
# allow blank line to terminate
if line.strip() == "":
if lines:
break
else:
continue
lines.append(line)
except EOFError:
break
print("\n--- OUTPUT ---\n")
print(convert("\n".join(lines)))