Last active
March 20, 2022 21:59
-
-
Save Stef-Gijsberts/3c80eb235a2dd9a9249e8fb015d35afa to your computer and use it in GitHub Desktop.
Transpose a file with chords
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
Copyright (c) 2022 Stef Gijsberts | |
This code is licensed under the MIT license. | |
This program transposes files with chords. | |
- It reads from standard input and writes to standard output. | |
- It only changes lines that have chords and nothing but chords. | |
- It preserves whitespace. | |
Example input: | |
Am G F C | |
Let it be, let it be, let it be, let it be | |
C G F C/E Dm7 C | |
Whisper words of wisdom, let it be | |
Corresponding output after transposing by -3: | |
F#m E D A | |
Let it be, let it be, let it be, let it be | |
A E D A/C# Bm7 A | |
Whisper words of wisdom, let it be | |
""" | |
import sys | |
import itertools as it | |
from typing import Generator | |
import chordparser | |
from chordparser import Chord | |
Offset = int | |
PositionedWord = tuple[Offset, str] | |
"""Represents a word and its line-offset.""" | |
PositionedChord = tuple[Offset, Chord] | |
"""Represents a chord and its line-offset.""" | |
def decode_chord(chord_name: str) -> Chord: | |
cp = chordparser.Parser() | |
return cp.create_chord(chord_name) | |
def split_on_spaces(text: str) -> Generator[PositionedWord, None, None]: | |
""" | |
Split a string on spaces, creating tuples of the parts along with their | |
positions. | |
# Example | |
## Input | |
``` | |
split_on_spaces("this is a string") | |
``` | |
## Output | |
``` | |
[(0, "this"), (5, "is"), (8, "a"), (13, "string")] | |
``` | |
## Explanation | |
```text | |
this is a string | |
^ ^ ^ ^ | |
0 5 8 13 | |
``` | |
""" | |
current_word = (0, "") | |
for pos, c in enumerate(text): | |
if c == " ": | |
if current_word[1]: | |
yield current_word | |
current_word = (pos + 1, "") | |
else: | |
current_word = (current_word[0], current_word[1] + c) | |
if current_word[1]: | |
yield current_word | |
def combine_with_spaces(parts: list[PositionedWord]) -> str: | |
"""Do the opposite of what `split_on_spaces` does.""" | |
line = "" | |
for column, word in parts: | |
# Make sure that we don't place a new word directly against another one | |
if len(line) > 0 and line[-1] != " ": | |
line += " " | |
# Make sure we are on the right column | |
while len(line) < column: | |
line += " " | |
# Add the word to the line | |
line += word | |
return line | |
def decode(line: str) -> list[PositionedChord]: | |
return [(pos, decode_chord(chord)) for pos, chord in split_on_spaces(line)] | |
def encode_chord(chord: Chord) -> str: | |
"""Return the chord as an ASCII string.""" | |
return ( | |
str(chord) | |
.replace("♯", "#") | |
.replace("♭", "b") | |
.replace("𝄪", "##") | |
.replace("𝄫", "bb") | |
) | |
def encode(chords: list[PositionedChord]) -> str: | |
return combine_with_spaces([(pos, encode_chord(chord)) for pos, chord in chords]) | |
def transpose(chord: Chord, by: int) -> Chord: | |
return chord.transpose_simple(by) | |
def process_line(line: str, transpose_by: int) -> str: | |
try: | |
positioned_chords = decode(line[:-1]) | |
except SyntaxError: | |
return line | |
new_positioned_chords = [ | |
(pos, transpose(chord, transpose_by)) for pos, chord in positioned_chords | |
] | |
return encode(new_positioned_chords) | |
def main(): | |
try: | |
transpose_by = int(sys.argv[1]) | |
except: | |
print(f"USAGE: {sys.argv[0]} <transpose_by>", file=sys.stderr) | |
sys.exit(1) | |
for line in sys.stdin: | |
print(process_line(line, transpose_by)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment