Skip to content

Instantly share code, notes, and snippets.

@Stef-Gijsberts
Last active March 20, 2022 21:59
Show Gist options
  • Save Stef-Gijsberts/3c80eb235a2dd9a9249e8fb015d35afa to your computer and use it in GitHub Desktop.
Save Stef-Gijsberts/3c80eb235a2dd9a9249e8fb015d35afa to your computer and use it in GitHub Desktop.
Transpose a file with chords
#!/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