st-language 2.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- st_language/__init__.py +87 -0
- st_language/__main__.py +5 -0
- st_language/_i18n.py +77 -0
- st_language/abc.py +370 -0
- st_language/chords.py +678 -0
- st_language/cli.py +194 -0
- st_language/instruments.py +330 -0
- st_language/locales/en.json +137 -0
- st_language/locales/es.json +137 -0
- st_language/locales/fr.json +137 -0
- st_language/midi.py +516 -0
- st_language/mtxt.py +593 -0
- st_language/musicxml.py +1200 -0
- st_language/notation.py +2487 -0
- st_language/song.py +374 -0
- st_language/stfile.py +332 -0
- st_language/timing.py +259 -0
- st_language-2.7.0.dist-info/METADATA +127 -0
- st_language-2.7.0.dist-info/RECORD +23 -0
- st_language-2.7.0.dist-info/WHEEL +5 -0
- st_language-2.7.0.dist-info/entry_points.txt +7 -0
- st_language-2.7.0.dist-info/licenses/LICENSE +674 -0
- st_language-2.7.0.dist-info/top_level.txt +1 -0
st_language/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ST-language: la notazione musicale testuale di SoundText come libreria
|
|
3
|
+
Python autonoma (nessuna dipendenza esterna, Python 3.8+).
|
|
4
|
+
|
|
5
|
+
import st_language as st
|
|
6
|
+
|
|
7
|
+
events = st.parse("4: c d e f | 2g 2g") # eventi con tempi in quarti
|
|
8
|
+
ok, error = st.validate("4: C Am | F G")
|
|
9
|
+
warnings = st.check("4: c d e | f") # controlli di battuta, testo cantato
|
|
10
|
+
|
|
11
|
+
song = st.load_song("brano.st") # o st.read_song(testo)
|
|
12
|
+
st.to_midi(song, "brano.mid")
|
|
13
|
+
st.to_musicxml(song, "brano.musicxml")
|
|
14
|
+
st.to_abc(song, "brano.abc")
|
|
15
|
+
st.to_mtxt(song, "brano.mtxt") # MTXT 1.0, un evento per riga
|
|
16
|
+
st.mtxt_to_midi(open("take.mtxt").read(), "take.mid")
|
|
17
|
+
|
|
18
|
+
La specifica del linguaggio e del formato .st e' in docs/spec/ del
|
|
19
|
+
repository di SoundText; i comandi da terminale sono 'st-language'
|
|
20
|
+
(check, midi, musicxml, abc, mtxt, events), 'stcheck', 'st2mid', 'st2musicxml',
|
|
21
|
+
'st2abc', 'st2mtxt'.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Dict, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from ._i18n import language, set_language, set_translator # noqa: F401
|
|
27
|
+
from .notation import ( # noqa: F401
|
|
28
|
+
BarIssue, Event, Meter, NotationError, Pattern, check_bar_lines, notation_warnings, parse_track_text,
|
|
29
|
+
tokenize, transpose_tokens, validate_track_text,
|
|
30
|
+
)
|
|
31
|
+
from .song import Clip, Part, Song, load_song, read_song, song_from_notation, text_to_song # noqa: F401
|
|
32
|
+
|
|
33
|
+
__version__ = "2.7.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"BarIssue", "Clip", "Event", "Meter", "NotationError", "Part", "Pattern", "Song", "check", "check_bar_lines",
|
|
37
|
+
"language", "load_song", "notation_warnings", "parse", "parse_track_text", "read_song",
|
|
38
|
+
"mtxt_to_midi", "set_language", "set_translator", "song_from_notation", "text_to_song", "to_abc",
|
|
39
|
+
"to_midi", "to_musicxml", "to_mtxt", "tokenize", "transpose_tokens", "validate", "validate_track_text",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse(text: str, patterns: Optional[Dict[str, Pattern]] = None, default_octave: int = 4) -> List[Event]:
|
|
44
|
+
"""Gli eventi di un testo in notazione (una traccia)."""
|
|
45
|
+
return parse_track_text(text, patterns or {}, default_octave=default_octave)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate(text: str, patterns: Optional[Dict[str, Pattern]] = None) -> Tuple[bool, str]:
|
|
49
|
+
"""(True, '') se il testo e' valido, altrimenti (False, messaggio)."""
|
|
50
|
+
return validate_track_text(text, patterns or {})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check(text: str, patterns: Optional[Dict[str, Pattern]] = None, time_sig: str = "4/4",
|
|
54
|
+
metrica_changes=(), pickup=0) -> List[BarIssue]:
|
|
55
|
+
"""Gli avvisi che non impediscono di suonare: controlli di battuta e
|
|
56
|
+
testo cantato con troppe sillabe. 'pickup': quarti del levare."""
|
|
57
|
+
return notation_warnings(text, patterns or {}, time_sig, metrica_changes, pickup=pickup)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def to_midi(song: Song, path: str, only_audible: bool = True) -> str:
|
|
61
|
+
from .midi import song_to_midi
|
|
62
|
+
return song_to_midi(song, path, only_audible=only_audible)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def to_musicxml(song: Song, path: str, only_audible: bool = True) -> str:
|
|
66
|
+
from .musicxml import export_project_to_musicxml
|
|
67
|
+
return export_project_to_musicxml(song, path, only_audible=only_audible)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def to_abc(song: Song, path: str, only_audible: bool = True) -> str:
|
|
71
|
+
from .abc import export_project_to_abc
|
|
72
|
+
return export_project_to_abc(song, path, only_audible=only_audible)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_mtxt(song: Song, path: str, only_audible: bool = True) -> str:
|
|
76
|
+
"""Il brano come file MTXT 1.0 (vedi st_language.mtxt); ritorna path."""
|
|
77
|
+
from .mtxt import song_to_mtxt
|
|
78
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
79
|
+
f.write(song_to_mtxt(song, only_audible=only_audible))
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def mtxt_to_midi(text: str, path: str) -> str:
|
|
84
|
+
"""Un testo MTXT come file MIDI; ritorna path. Solleva
|
|
85
|
+
st_language.mtxt.MtxtError (un ValueError) se il testo non e' valido."""
|
|
86
|
+
from .mtxt import mtxt_to_midi as convert
|
|
87
|
+
return convert(text, path)
|
st_language/__main__.py
ADDED
st_language/_i18n.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messaggi della libreria (errori di sintassi, avvisi) nella lingua scelta.
|
|
3
|
+
|
|
4
|
+
I testi sono scritti in italiano nel codice, dentro tr(), come in
|
|
5
|
+
SoundText; i cataloghi locales/<lingua>.json ({testo italiano:
|
|
6
|
+
traduzione}) danno l'inglese, il francese e lo spagnolo. La lingua si
|
|
7
|
+
sceglie con set_language() o con la variabile d'ambiente ST_LANGUAGE
|
|
8
|
+
(altrimenti LANG); senza indicazioni, l'inglese.
|
|
9
|
+
|
|
10
|
+
Un programma che ha gia' le sue traduzioni (SoundText) puo' sostituire
|
|
11
|
+
tr con set_translator(): tutti i messaggi passano allora da li'.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from typing import Callable, Dict, Optional
|
|
17
|
+
|
|
18
|
+
LANGUAGES = ("it", "en", "fr", "es")
|
|
19
|
+
SOURCE_LANGUAGE = "it"
|
|
20
|
+
DEFAULT_LANGUAGE = "en"
|
|
21
|
+
|
|
22
|
+
_translator: Optional[Callable] = None
|
|
23
|
+
_language: Optional[str] = None
|
|
24
|
+
_catalog: Dict[str, str] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def set_translator(fn: Optional[Callable]) -> None:
|
|
28
|
+
"""fn(testo_italiano, *args, **kwargs) -> testo; None torna ai cataloghi."""
|
|
29
|
+
global _translator
|
|
30
|
+
_translator = fn
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env_language() -> str:
|
|
34
|
+
for var in ("ST_LANGUAGE", "LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
|
|
35
|
+
for part in os.environ.get(var, "").split(":"):
|
|
36
|
+
code = part.strip().lower()[:2]
|
|
37
|
+
if code in LANGUAGES:
|
|
38
|
+
return code
|
|
39
|
+
return DEFAULT_LANGUAGE
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_language(code: Optional[str] = None) -> str:
|
|
43
|
+
"""Sceglie la lingua dei messaggi (None: dall'ambiente); ritorna quella in uso."""
|
|
44
|
+
global _language, _catalog
|
|
45
|
+
code = (code or _env_language()).lower()[:2]
|
|
46
|
+
if code not in LANGUAGES:
|
|
47
|
+
code = DEFAULT_LANGUAGE
|
|
48
|
+
_language = code
|
|
49
|
+
_catalog = {}
|
|
50
|
+
if code != SOURCE_LANGUAGE:
|
|
51
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locales", f"{code}.json")
|
|
52
|
+
try:
|
|
53
|
+
with open(path, encoding="utf-8") as f:
|
|
54
|
+
_catalog = {k: v for k, v in json.load(f).items() if isinstance(v, str) and v}
|
|
55
|
+
except (OSError, ValueError):
|
|
56
|
+
_catalog = {}
|
|
57
|
+
return code
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def language() -> str:
|
|
61
|
+
if _language is None:
|
|
62
|
+
set_language()
|
|
63
|
+
return _language
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tr(text: str, *args, **kwargs) -> str:
|
|
67
|
+
if _translator is not None:
|
|
68
|
+
return _translator(text, *args, **kwargs)
|
|
69
|
+
if _language is None:
|
|
70
|
+
set_language()
|
|
71
|
+
out = _catalog.get(text, text)
|
|
72
|
+
if args or kwargs:
|
|
73
|
+
try:
|
|
74
|
+
return out.format(*args, **kwargs)
|
|
75
|
+
except (IndexError, KeyError, ValueError):
|
|
76
|
+
return text.format(*args, **kwargs)
|
|
77
|
+
return out
|
st_language/abc.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Esportazione del progetto in notazione ABC (standard 2.1), il formato di
|
|
3
|
+
testo per partiture usato da abcjs, EasyABC, abcm2ps, abc2midi e dalle
|
|
4
|
+
grandi raccolte di musica tradizionale.
|
|
5
|
+
|
|
6
|
+
Lavora sugli stessi attacchi dell'esportazione MusicXML (st_language.
|
|
7
|
+
musicxml): note, accordi gia' risolti dal motore di voicing, blocchi,
|
|
8
|
+
percussioni, battute, tuplet, legature fra battute. Ogni pentagramma di
|
|
9
|
+
ogni traccia diventa una voce ABC (V:), e le voci dei blocchi { ; } voci
|
|
10
|
+
in piu' sullo stesso pentagramma, raggruppate con %%score: pianoforti e
|
|
11
|
+
organi hanno due pentagrammi uniti da una graffa. Lo strumento passa con
|
|
12
|
+
%%MIDI program (la batteria sul canale 10, con le note dei suoni GM), la
|
|
13
|
+
tonalita' con K:, la metrica con M: e i cambi [M:] in tutte le voci, il
|
|
14
|
+
tempo con Q: e [Q:], le sigle degli accordi fra virgolette, le dinamiche
|
|
15
|
+
come decorazioni (!mf!), il testo cantato con w:. Il pedale del sustain
|
|
16
|
+
non c'e' (non e' nello standard).
|
|
17
|
+
Chitarre e bassi usano la chiave all'ottava bassa (treble-8, bass-8),
|
|
18
|
+
con le note scritte un'ottava sopra come vuole lo standard.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
from fractions import Fraction
|
|
23
|
+
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from .instruments import PERCUSSION_MAP
|
|
26
|
+
from .musicxml import (QUANT_GRID, _Direction, _Entry, _Item, _clefs, _drop_repeated_harmonies,
|
|
27
|
+
_dynamic_changes, _frac, _grid_divisions, _mark_lyrics, _measures, _monophonic,
|
|
28
|
+
_repeat_layout, _spell_midi, _staff_entries, _track_harmonies, _track_items,
|
|
29
|
+
_track_navigation, _track_repeats, _track_texts, _item_syllables, key_fifths,
|
|
30
|
+
song_key_changes, NAVIGATION_WORDS)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING: # solo per le annotazioni (nessun import circolare)
|
|
33
|
+
from .song import Part, Song # noqa: F401
|
|
34
|
+
|
|
35
|
+
# Unita' di nota (L:) in quarti: l'ottavo, la piu' comune.
|
|
36
|
+
UNIT = Fraction(1, 2)
|
|
37
|
+
BARS_PER_LINE = 4
|
|
38
|
+
DRUM_CHANNEL = 10
|
|
39
|
+
|
|
40
|
+
_SHARP_ORDER = "FCGDAEB"
|
|
41
|
+
_MAJOR_NAMES = {-7: "Cb", -6: "Gb", -5: "Db", -4: "Ab", -3: "Eb", -2: "Bb", -1: "F", 0: "C",
|
|
42
|
+
1: "G", 2: "D", 3: "A", 4: "E", 5: "B", 6: "F#", 7: "C#"}
|
|
43
|
+
_MINOR_NAMES = {-7: "Abm", -6: "Ebm", -5: "Bbm", -4: "Fm", -3: "Cm", -2: "Gm", -1: "Dm", 0: "Am",
|
|
44
|
+
1: "Em", 2: "Bm", 3: "F#m", 4: "C#m", 5: "G#m", 6: "D#m", 7: "A#m"}
|
|
45
|
+
_ARTICULATIONS = {"staccato": ".", "mute": "!wedge!", "legato": "!tenuto!"}
|
|
46
|
+
_DECORATIONS = {"accent": "!accent!", "marcato": "!marcato!", "tenuto": "!tenuto!", "fermata": "!fermata!",
|
|
47
|
+
"tr": "!trill!", "mordent": "!lowermordent!", "turn": "!turn!",
|
|
48
|
+
"arp": "!arpeggio!", "staccatissimo": "!wedge!", "sfz": "!sfz!", "fp": "!fp!",
|
|
49
|
+
"trem": "!///!", "harmonic": "!open!"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def key_name(key: str) -> str:
|
|
53
|
+
"""La tonalita' del progetto come valore di K: ('C', 'Am', 'Bb', 'F#m')."""
|
|
54
|
+
found = key_fifths(key)
|
|
55
|
+
if not found:
|
|
56
|
+
return "C"
|
|
57
|
+
fifths, mode = found
|
|
58
|
+
return (_MINOR_NAMES if mode == "minor" else _MAJOR_NAMES)[fifths]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def key_alters(fifths: int) -> Dict[str, int]:
|
|
62
|
+
"""Alterazione in chiave di ogni nota (lettera maiuscola -> -1/0/+1)."""
|
|
63
|
+
alters = {step: 0 for step in "ABCDEFG"}
|
|
64
|
+
for step in (_SHARP_ORDER[:fifths] if fifths > 0 else _SHARP_ORDER[::-1][:-fifths]):
|
|
65
|
+
alters[step] = 1 if fifths > 0 else -1
|
|
66
|
+
return alters
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def length_suffix(units: Fraction) -> str:
|
|
70
|
+
"""Il moltiplicatore di durata ABC ('', '2', '3/2', '/2', '/4')."""
|
|
71
|
+
if units == 1:
|
|
72
|
+
return ""
|
|
73
|
+
if units.denominator == 1:
|
|
74
|
+
return str(units.numerator)
|
|
75
|
+
if units.numerator == 1:
|
|
76
|
+
return "/" if units.denominator == 2 else f"/{units.denominator}"
|
|
77
|
+
return f"{units.numerator}/{units.denominator}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pitch_text(step: str, alter: int, octave: int, bar_alters: Dict[str, int],
|
|
81
|
+
signature: Dict[str, int]) -> str:
|
|
82
|
+
"""Una nota ABC: l'alterazione solo se diversa da quella in vigore
|
|
83
|
+
(armatura, o alterazione precedente nella battuta: per lo standard vale
|
|
84
|
+
per quella nota in tutte le ottave, %%propagate-accidentals pitch)."""
|
|
85
|
+
current = bar_alters.get(step, signature[step])
|
|
86
|
+
accidental = ""
|
|
87
|
+
if alter != current:
|
|
88
|
+
accidental = {2: "^^", 1: "^", 0: "=", -1: "_", -2: "__"}[alter]
|
|
89
|
+
bar_alters[step] = alter
|
|
90
|
+
if octave >= 5:
|
|
91
|
+
return accidental + step.lower() + "'" * (octave - 5)
|
|
92
|
+
return accidental + step + "," * (4 - octave)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def chord_symbol(symbol: str, bass: Optional[str]) -> str:
|
|
96
|
+
text = symbol.replace("♭", "b")
|
|
97
|
+
if len(text) > 1 and text[1] == "-":
|
|
98
|
+
text = text[0] + "b" + text[2:]
|
|
99
|
+
if bass:
|
|
100
|
+
text += "/" + bass.replace("♭", "b").replace("-", "b")
|
|
101
|
+
return '"' + text.replace('"', "") + '"'
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _drum_items(items: List[_Item]) -> List[_Item]:
|
|
105
|
+
"""I colpi di batteria come note: la nota GM del suono (canale 10)."""
|
|
106
|
+
out = []
|
|
107
|
+
for item in items:
|
|
108
|
+
pitches = sorted({PERCUSSION_MAP.get(d, 38) for d in item.drums})
|
|
109
|
+
graces = tuple((tuple(_spell_midi(PERCUSSION_MAP.get(d, 38), False) for d in drums), (), slash)
|
|
110
|
+
for _pitches, drums, slash in item.graces)
|
|
111
|
+
out.append(dataclasses.replace(item, pitches=[_spell_midi(p, False) for p in pitches], drums=[],
|
|
112
|
+
graces=graces))
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _clef_text(sign: str, octave_change: int) -> str:
|
|
117
|
+
if sign == "percussion":
|
|
118
|
+
return "perc"
|
|
119
|
+
name = "treble" if sign == "G" else "bass"
|
|
120
|
+
return name + ("-8" if octave_change < 0 else "+8" if octave_change > 0 else "")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _lyric_syllable(entry: _Entry, verse: int = 1) -> str:
|
|
124
|
+
"""La sillaba w: di una nota nella strofa 'verse': '*' senza testo, '_'
|
|
125
|
+
per la nota legata che prosegue (lo standard le conta come note distinte)."""
|
|
126
|
+
if not entry.first_of_item:
|
|
127
|
+
return "_ "
|
|
128
|
+
syllable = _item_syllables(entry.item).get(verse)
|
|
129
|
+
if not syllable:
|
|
130
|
+
return "* "
|
|
131
|
+
if syllable == "_":
|
|
132
|
+
return "_ "
|
|
133
|
+
text = syllable.replace(" ", "~").replace("*", "\\*")
|
|
134
|
+
return text if text.endswith("-") and len(text) > 1 else text + " "
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _Voice:
|
|
138
|
+
def __init__(self, vid: str, name: str, clef: str, program: Optional[int], drums: bool,
|
|
139
|
+
items: List[_Item], first: bool, transpose: int):
|
|
140
|
+
self.vid = vid
|
|
141
|
+
self.name = name
|
|
142
|
+
self.clef = clef
|
|
143
|
+
self.program = program
|
|
144
|
+
self.drums = drums
|
|
145
|
+
self.items = items
|
|
146
|
+
self.first = first # prima voce del pentagramma (pause visibili)
|
|
147
|
+
self.transpose = transpose # semitoni fra suono e scrittura (chiave all'ottava)
|
|
148
|
+
self.directions: List[_Direction] = []
|
|
149
|
+
self.verses = sorted({n for i in items for n in _item_syllables(i)})
|
|
150
|
+
self.has_lyrics = bool(self.verses)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _entry_text(entry: _Entry, voice: _Voice, bar_alters, signature) -> str:
|
|
154
|
+
units = entry.duration / UNIT
|
|
155
|
+
if entry.tuplet:
|
|
156
|
+
actual, normal = entry.tuplet
|
|
157
|
+
units = units * actual / normal
|
|
158
|
+
out = "".join(entry.directions)
|
|
159
|
+
if entry.item is None:
|
|
160
|
+
rest = "z" if voice.first else "x"
|
|
161
|
+
return out + rest + length_suffix(units)
|
|
162
|
+
if entry.first_of_item and entry.item.slur == "start":
|
|
163
|
+
out += "("
|
|
164
|
+
if entry.first_of_item and entry.item.articulation in _ARTICULATIONS:
|
|
165
|
+
out += _ARTICULATIONS[entry.item.articulation]
|
|
166
|
+
if entry.first_of_item:
|
|
167
|
+
out += "".join(_DECORATIONS[d] for d in entry.item.decorations if d in _DECORATIONS)
|
|
168
|
+
if entry.first_of_item and entry.item.graces:
|
|
169
|
+
# {/g} acciaccatura, {g} appoggiatura; un abbellimento di piu' note si
|
|
170
|
+
# scrive come sequenza (le note di un abbellimento ABC sono singole)
|
|
171
|
+
slash = "/" if entry.item.graces[0][2] else ""
|
|
172
|
+
out += "{" + slash + "".join(pitch_text(p.step, p.alter, p.octave + voice.transpose // 12, bar_alters,
|
|
173
|
+
signature)
|
|
174
|
+
for pitches, _drums, _slash in entry.item.graces for p in pitches) + "}"
|
|
175
|
+
notes = [pitch_text(p.step, p.alter, p.octave + voice.transpose // 12, bar_alters, signature)
|
|
176
|
+
for p in entry.item.pitches]
|
|
177
|
+
body = notes[0] if len(notes) == 1 else "[" + "".join(notes) + "]"
|
|
178
|
+
slur_end = ")" if not entry.tie_start and entry.item.slur == "stop" else ""
|
|
179
|
+
return out + body + length_suffix(units) + ("-" if entry.tie_start else "") + slur_end
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _measure_text(entries: List[_Entry], voice: _Voice, signature) -> Tuple[str, Dict[int, List[str]]]:
|
|
183
|
+
"""La battuta di una voce e le sue sillabe, per strofa."""
|
|
184
|
+
parts: List[str] = []
|
|
185
|
+
syllables: Dict[int, List[str]] = {n: [] for n in voice.verses}
|
|
186
|
+
bar_alters: Dict[str, int] = {}
|
|
187
|
+
in_tuplet = False
|
|
188
|
+
for i, entry in enumerate(entries):
|
|
189
|
+
if entry.tuplet_mark == "start":
|
|
190
|
+
count = next(k for k, e in enumerate(entries[i:]) if e.tuplet_mark == "stop") + 1
|
|
191
|
+
actual, normal = entry.tuplet
|
|
192
|
+
parts.append(f"({actual}:{normal}:{count}")
|
|
193
|
+
in_tuplet = True
|
|
194
|
+
elif entry.tuplet and not in_tuplet:
|
|
195
|
+
# tuplet isolata (una nota sola, es. una pausa di terzina)
|
|
196
|
+
actual, normal = entry.tuplet
|
|
197
|
+
parts.append(f"({actual}:{normal}:1")
|
|
198
|
+
text = _entry_text(entry, voice, bar_alters, signature)
|
|
199
|
+
if entry.tuplet_mark == "stop":
|
|
200
|
+
in_tuplet = False
|
|
201
|
+
joined = entry.beam in ("begin", "continue")
|
|
202
|
+
parts.append(text + ("" if joined else " "))
|
|
203
|
+
if entry.item is not None:
|
|
204
|
+
for n in voice.verses:
|
|
205
|
+
syllables[n].append(_lyric_syllable(entry, n))
|
|
206
|
+
return "".join(parts).rstrip(), syllables
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def project_to_abc(project: "Song", only_audible: bool = True,
|
|
210
|
+
tracks: Optional[List["Part"]] = None, midi_dir: Optional[str] = None) -> str:
|
|
211
|
+
"""Il progetto come brano ABC (X:1), una voce per pentagramma."""
|
|
212
|
+
from .timing import build_metrica_beat_map, build_tempo_beat_map
|
|
213
|
+
|
|
214
|
+
if tracks is None:
|
|
215
|
+
tracks = project.audible_tracks() if only_audible else project.tracks
|
|
216
|
+
tracks = [t for t in tracks if not t.is_audio]
|
|
217
|
+
events_by_track = {t.name: t.parsed_events(project.patterns, midi_dir=midi_dir, meter=project.meter()) for t in tracks}
|
|
218
|
+
key = key_fifths(project.key)
|
|
219
|
+
flats = bool(key and key[0] < 0)
|
|
220
|
+
signature = key_alters(key[0] if key else 0)
|
|
221
|
+
|
|
222
|
+
grid = None
|
|
223
|
+
for _attempt in range(2):
|
|
224
|
+
parsed = {t.name: _track_items(events_by_track[t.name], t.instrument, flats, grid) for t in tracks}
|
|
225
|
+
tempo_map = [(_frac(b, grid), bpm) for b, bpm in
|
|
226
|
+
build_tempo_beat_map(project, tracks=tracks, events_by_track=events_by_track)]
|
|
227
|
+
end = max([i.end for items, _ in parsed.values() for i in items] + [Fraction(0)])
|
|
228
|
+
measures = _measures(build_metrica_beat_map(project), end, getattr(project, "pickup", 0))
|
|
229
|
+
times = [m[0] for m in measures] + [t for t, _ in tempo_map]
|
|
230
|
+
for items, pedals in parsed.values():
|
|
231
|
+
times += [i.start for i in items] + [i.end for i in items] + [t for t, _ in pedals]
|
|
232
|
+
if _grid_divisions(times):
|
|
233
|
+
break
|
|
234
|
+
grid = QUANT_GRID
|
|
235
|
+
|
|
236
|
+
layout = _repeat_layout(measures, [r for t in tracks for r in _track_repeats(events_by_track[t.name], grid)],
|
|
237
|
+
[parsed[t.name][0] for t in tracks])
|
|
238
|
+
voices: List[_Voice] = []
|
|
239
|
+
score: List[str] = []
|
|
240
|
+
for n, track in enumerate(tracks, 1):
|
|
241
|
+
items, _pedals = parsed[track.name] # il pedale non e' nello standard ABC 2.1
|
|
242
|
+
_drop_repeated_harmonies(items)
|
|
243
|
+
instrument = track.instrument
|
|
244
|
+
drums = instrument.is_percussion
|
|
245
|
+
clefs = _clefs(track, items)
|
|
246
|
+
if drums:
|
|
247
|
+
items = _drum_items(items)
|
|
248
|
+
if len(clefs) == 2:
|
|
249
|
+
upper = [dataclasses.replace(i, pitches=[p for p in i.pitches if p.midi >= 60])
|
|
250
|
+
for i in items if any(p.midi >= 60 for p in i.pitches)]
|
|
251
|
+
lower = [dataclasses.replace(i, pitches=[p for p in i.pitches if p.midi < 60], harmony=None,
|
|
252
|
+
lyric=i.lyric if not any(p.midi >= 60 for p in i.pitches) else None)
|
|
253
|
+
for i in items if any(p.midi < 60 for p in i.pitches)]
|
|
254
|
+
staff_items = [upper, lower]
|
|
255
|
+
else:
|
|
256
|
+
staff_items = [items]
|
|
257
|
+
|
|
258
|
+
directions: List[_Direction] = []
|
|
259
|
+
if n == 1:
|
|
260
|
+
directions += [_Direction(t, f"[Q:1/4={bpm}]") for t, bpm in tempo_map if t > 0]
|
|
261
|
+
directions += [_Direction(i.start, chord_symbol(*i.harmony)) for i in items if i.harmony]
|
|
262
|
+
directions += [_Direction(t, chord_symbol(symbol, bass))
|
|
263
|
+
for t, symbol, bass in _track_harmonies(events_by_track[track.name], grid)]
|
|
264
|
+
directions += [_Direction(t, '"^' + words.replace('"', "'") + '"')
|
|
265
|
+
for t, words in _track_texts(events_by_track[track.name], grid)]
|
|
266
|
+
directions += [_Direction(t, {"segno": "!segno!", "coda": "!coda!"}.get(name)
|
|
267
|
+
or '"^' + NAVIGATION_WORDS.get(name, name) + '"')
|
|
268
|
+
for t, name in _track_navigation(events_by_track[track.name], grid)]
|
|
269
|
+
if not drums:
|
|
270
|
+
first_voice = min((i.voice for i in items), default=1)
|
|
271
|
+
directions += [_Direction(t, f"!{mark}!") for t, mark in
|
|
272
|
+
_dynamic_changes(_monophonic([i for i in items if i.voice == first_voice]))]
|
|
273
|
+
merged: Dict[Fraction, str] = {}
|
|
274
|
+
for d in sorted(directions, key=lambda d: (d.time, not d.xml.startswith("["))):
|
|
275
|
+
merged[d.time] = merged.get(d.time, "") + d.xml
|
|
276
|
+
directions = [_Direction(t, text) for t, text in sorted(merged.items())]
|
|
277
|
+
|
|
278
|
+
staves = []
|
|
279
|
+
for s, s_items in enumerate(staff_items):
|
|
280
|
+
by_voice: Dict[int, List[_Item]] = {}
|
|
281
|
+
for item in s_items:
|
|
282
|
+
by_voice.setdefault(item.voice, []).append(item)
|
|
283
|
+
sign, _line, octave_change = clefs[s]
|
|
284
|
+
ids = []
|
|
285
|
+
for k, (_v, v_items) in enumerate(sorted(by_voice.items()) or [(1, [])]):
|
|
286
|
+
v_items = _monophonic(v_items)
|
|
287
|
+
_mark_lyrics(v_items)
|
|
288
|
+
vid = f"T{n}" + ("" if s == 0 else "b") + ("" if k == 0 else f"v{k + 1}")
|
|
289
|
+
name = track.name if s == 0 and k == 0 else ""
|
|
290
|
+
voice = _Voice(vid, name, _clef_text(sign, octave_change), instrument.gm_program,
|
|
291
|
+
drums, v_items, k == 0, -12 * octave_change)
|
|
292
|
+
if s == 0 and k == 0:
|
|
293
|
+
voice.directions = directions
|
|
294
|
+
voices.append(voice)
|
|
295
|
+
ids.append(vid)
|
|
296
|
+
staves.append(ids[0] if len(ids) == 1 else "(" + " ".join(ids) + ")")
|
|
297
|
+
score.append(staves[0] if len(staves) == 1 else "{" + " | ".join(staves) + "}")
|
|
298
|
+
|
|
299
|
+
first_sig = f"{measures[0][2]}/{measures[0][3]}"
|
|
300
|
+
bpm = tempo_map[0][1] if tempo_map and tempo_map[0][0] == 0 else round(project.tempo_bpm)
|
|
301
|
+
out = ["X:1", f"T:{getattr(project, 'title', '') or project.name}"]
|
|
302
|
+
if getattr(project, "composer", ""):
|
|
303
|
+
out.append(f"C:{project.composer}")
|
|
304
|
+
if getattr(project, "lyricist", ""):
|
|
305
|
+
out.append(f"C:{project.lyricist} (lyrics)")
|
|
306
|
+
out += [f"M:{first_sig}", "L:1/8", f"Q:1/4={bpm}"]
|
|
307
|
+
key_changes = dict(song_key_changes(project))
|
|
308
|
+
if len(voices) > 1:
|
|
309
|
+
out.append("%%score " + " ".join(score))
|
|
310
|
+
for voice in voices:
|
|
311
|
+
line = f"V:{voice.vid} clef={voice.clef}"
|
|
312
|
+
if voice.name:
|
|
313
|
+
line += ' name="' + voice.name.replace('"', "'") + '"'
|
|
314
|
+
out.append(line)
|
|
315
|
+
out.append(f"K:{key_name(project.key)}")
|
|
316
|
+
|
|
317
|
+
for voice in voices:
|
|
318
|
+
out.append(f"V:{voice.vid}")
|
|
319
|
+
out.append(f"%%MIDI channel {DRUM_CHANNEL}" if voice.drums else f"%%MIDI program {voice.program}")
|
|
320
|
+
line: List[str] = []
|
|
321
|
+
syllables: Dict[int, List[str]] = {n: [] for n in voice.verses}
|
|
322
|
+
previous_sig = (measures[0][2], measures[0][3])
|
|
323
|
+
voice_signature = signature
|
|
324
|
+
for number, (m_index, marks) in enumerate(layout, 1):
|
|
325
|
+
m_start, m_len, num, den = measures[m_index]
|
|
326
|
+
changes_key = m_start in key_changes and number > 1 and not voice.drums
|
|
327
|
+
if changes_key:
|
|
328
|
+
voice_signature = key_alters(key_fifths(key_changes[m_start])[0])
|
|
329
|
+
entries = _staff_entries(voice.items, voice.directions, m_start, m_len, num, den)
|
|
330
|
+
text, bar_syllables = _measure_text(entries, voice, voice_signature)
|
|
331
|
+
if (num, den) != previous_sig:
|
|
332
|
+
text = f"[M:{num}/{den}] " + text
|
|
333
|
+
previous_sig = (num, den)
|
|
334
|
+
if changes_key:
|
|
335
|
+
text = f"[K:{key_name(key_changes[m_start])}] " + text
|
|
336
|
+
if marks.ending_start:
|
|
337
|
+
text = f"[{marks.ending_start} " + text
|
|
338
|
+
if number == 1 and marks.forward:
|
|
339
|
+
text = "|: " + text
|
|
340
|
+
last = number == len(layout)
|
|
341
|
+
following = layout[number][1] if not last else None
|
|
342
|
+
if marks.backward:
|
|
343
|
+
bar = "::" if following is not None and following.forward else ":|"
|
|
344
|
+
elif following is not None and following.forward:
|
|
345
|
+
bar = "|:"
|
|
346
|
+
else:
|
|
347
|
+
bar = "|]" if last else "|"
|
|
348
|
+
line.append(text + " " + bar)
|
|
349
|
+
for n in voice.verses:
|
|
350
|
+
syllables[n] += bar_syllables[n]
|
|
351
|
+
if number % BARS_PER_LINE == 0 or last:
|
|
352
|
+
out.append(" ".join(line))
|
|
353
|
+
# una riga w: per strofa, sempre (anche vuota): per lo standard
|
|
354
|
+
# la successiva si allinea dopo le note di questa
|
|
355
|
+
for n in voice.verses:
|
|
356
|
+
words = "".join(syllables[n]).split(" ")
|
|
357
|
+
while words and words[-1] in ("*", ""):
|
|
358
|
+
words.pop()
|
|
359
|
+
out.append(("w: " + " ".join(words)).rstrip())
|
|
360
|
+
line, syllables = [], {n: [] for n in voice.verses}
|
|
361
|
+
return "\n".join(out) + "\n"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def export_project_to_abc(project: "Song", path: str, only_audible: bool = True,
|
|
365
|
+
tracks: Optional[List["Part"]] = None, midi_dir: Optional[str] = None) -> str:
|
|
366
|
+
"""Scrive in path il brano ABC del progetto (vedi project_to_abc): le
|
|
367
|
+
stesse tracce dell'esportazione MIDI e MusicXML."""
|
|
368
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
369
|
+
f.write(project_to_abc(project, only_audible=only_audible, tracks=tracks, midi_dir=midi_dir))
|
|
370
|
+
return path
|