+5
-10
@@ -6,6 +6,8 @@ import shutil
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import modulus.filesystem
|
||||
|
||||
import generator.arrange.generate
|
||||
import generator.catalog
|
||||
import generator.engraving.cache
|
||||
@@ -71,7 +73,7 @@ def generate_assets(args: argparse.Namespace) -> None:
|
||||
report('loading solo scale patterns')
|
||||
scales = generator.scales.library.load_library(data_dir / 'scales')
|
||||
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='.vamp-assets-', dir=output_dir.parent) as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='.vamp_assets_', dir=output_dir.parent) as temporary:
|
||||
staged = pathlib.Path(temporary) / 'assets'
|
||||
staged.mkdir(parents=True, exist_ok=True)
|
||||
copy_runtime_fonts(static_assets_dir, staged)
|
||||
@@ -90,15 +92,8 @@ def generate_assets(args: argparse.Namespace) -> None:
|
||||
generator.score.validate_generated_tree(staged, validated_pages)
|
||||
report('building catalog database')
|
||||
generator.catalog.build_catalog(staged)
|
||||
backup = pathlib.Path(temporary) / 'previous'
|
||||
if output_dir.exists():
|
||||
output_dir.rename(backup)
|
||||
try:
|
||||
staged.rename(output_dir)
|
||||
except OSError:
|
||||
if backup.exists():
|
||||
backup.rename(output_dir)
|
||||
raise
|
||||
with modulus.filesystem.publish_directories([(staged, output_dir)]):
|
||||
pass
|
||||
report('publishing complete')
|
||||
print(f'Generated Product Vamp assets: {performance_count} performance scores.')
|
||||
if args.check:
|
||||
|
||||
@@ -205,7 +205,7 @@ def realize(measure: dict, bar: generator.rhythm.Bar, previous: dict | None, *,
|
||||
written = {'type': source.written_type, 'dots': source.dots} if unchanged else generator.model.written_for(duration)
|
||||
if not unchanged and (lane == 'electric' or not chord):
|
||||
duration, written = next(written_durations(duration))
|
||||
event = generator.model.make_event(f'{measure["id"]}-rhythm-{lane}-{len(events)}', start, duration, 'note' if chord else 'rest', written=written)
|
||||
event = generator.model.make_event(f'{measure["id"]}_rhythm_{lane}_{len(events)}', start, duration, 'note' if chord else 'rest', written=written)
|
||||
if chord:
|
||||
event['_harmony'] = chord
|
||||
if lane:
|
||||
@@ -215,7 +215,7 @@ def realize(measure: dict, bar: generator.rhythm.Bar, previous: dict | None, *,
|
||||
events.append(event)
|
||||
cursor = start + duration
|
||||
for duration, written in written_durations(end - cursor):
|
||||
events.append(generator.model.make_event(f'{measure["id"]}-rhythm-{lane}-{len(events)}', cursor, duration, 'rest', written=written))
|
||||
events.append(generator.model.make_event(f'{measure["id"]}_rhythm_{lane}_{len(events)}', cursor, duration, 'rest', written=written))
|
||||
cursor += duration
|
||||
normalize_beams(events)
|
||||
return events
|
||||
|
||||
@@ -67,11 +67,11 @@ def prepare(document: dict) -> None:
|
||||
for note in voice['notes']:
|
||||
start = generator.model.fraction(note['onset'])
|
||||
if start > cursor:
|
||||
notes.append(generator.model.make_event(f'{note["id"]}-release', cursor, start - cursor, 'rest') | {'voice': voice['id']})
|
||||
notes.append(generator.model.make_event(f'{note["id"]}_release', cursor, start - cursor, 'rest') | {'voice': voice['id']})
|
||||
notes.append(note)
|
||||
cursor = start + generator.model.fraction(note['duration'])
|
||||
if cursor < length:
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-{voice["id"]}-release', cursor, length - cursor, 'rest') | {'voice': voice['id']})
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}_{voice["id"]}_release', cursor, length - cursor, 'rest') | {'voice': voice['id']})
|
||||
voice['notes'] = notes
|
||||
generator.accompaniment.normalize_beams(notes)
|
||||
|
||||
@@ -85,7 +85,7 @@ def combine(document: dict) -> None:
|
||||
def rest(end):
|
||||
nonlocal cursor
|
||||
for duration, written in generator.accompaniment.written_durations(end - cursor):
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-acoustic-{len(notes) + 1}', cursor, duration, 'rest', written=written) | {'voice': 'voice-arranged'})
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}_acoustic_{len(notes) + 1}', cursor, duration, 'rest', written=written) | {'voice': 'voice_arranged'})
|
||||
cursor += duration
|
||||
|
||||
for start, group in itertools.groupby(attacks, key=lambda n: generator.model.fraction(n['onset'])):
|
||||
@@ -94,9 +94,9 @@ def combine(document: dict) -> None:
|
||||
pitches = sorted((p for n in group for p in n['pitches']), key=lambda p: (generator.model.pitch_number(p), p['tablature']['string']))
|
||||
pitches = [{k: v for k, v in p.items() if k != 'stem'} for p in pitches]
|
||||
duration = generator.model.fraction(group[0]['duration'])
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}-acoustic-{len(notes) + 1}', start, duration, 'note', pitches, group[0]['written']) | {'voice': 'voice-arranged'})
|
||||
notes.append(generator.model.make_event(f'{measure["id"]}_acoustic_{len(notes) + 1}', start, duration, 'note', pitches, group[0]['written']) | {'voice': 'voice_arranged'})
|
||||
cursor = start + duration
|
||||
rest(length)
|
||||
if not attacks and length == generator.model.meter_length(measure['meter']):
|
||||
notes = [generator.model.make_event(f'{measure["id"]}-acoustic-1', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'voice': 'voice-arranged', 'measure_rest': True}]
|
||||
measure['voices'] = [{'id': 'voice-arranged', 'notes': notes}]
|
||||
notes = [generator.model.make_event(f'{measure["id"]}_acoustic_1', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'voice': 'voice_arranged', 'measure_rest': True}]
|
||||
measure['voices'] = [{'id': 'voice_arranged', 'notes': notes}]
|
||||
|
||||
@@ -90,7 +90,7 @@ def place(document: dict) -> None:
|
||||
for note in span.notes:
|
||||
positions = (voicing.bass,) if note['_guitar_lane'] == 'bass' else voicing[1:]
|
||||
for pitch in generator.fretboard.chord_pitches(span.harmony, positions):
|
||||
note['pitches'].append({**pitch, 'id': f'{note["id"]}-s{pitch["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']})
|
||||
note['pitches'].append({**pitch, 'id': f'{note["id"]}_s{pitch["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']})
|
||||
for field in ('_harmony', '_guitar_profile', '_guitar_lane'):
|
||||
note.pop(field, None)
|
||||
document['staves'][0]['tablature'] = True
|
||||
|
||||
@@ -76,7 +76,7 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
|
||||
if section.role == 'accompaniment':
|
||||
plan = plans[section.section_index]
|
||||
rhythm = (plan.acoustic_bass[len(segment)], plan.acoustic_upper[len(segment)]) if part_type == 'acoustic' else plan.electric[len(segment)]
|
||||
result = generator.parts.build_measure(m, f'measure-{len(output) + len(segment) + 1}', section.role, part_type, previous[si], section.label if not segment else '', rhythm)
|
||||
result = generator.parts.build_measure(m, f'measure_{len(output) + len(segment) + 1}', section.role, part_type, previous[si], section.label if not segment else '', rhythm)
|
||||
segment.append(result)
|
||||
if part_type == 'electric' and section.role in ('melody', 'accompaniment'):
|
||||
voicing_origins.append(generator.electric.MeasureOrigin(source_starts[si], section.role, result))
|
||||
@@ -88,10 +88,10 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
|
||||
for shift in staff.get('octave_shifts', []):
|
||||
low, high = max(start, generator.model.fraction(shift['start'])), min(start + segment_length, generator.model.fraction(shift['end']))
|
||||
if low < high:
|
||||
octave_shifts.append({**shift, 'id': f'section-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(absolute + low - start), 'end': generator.model.rational(absolute + high - start)})
|
||||
octave_shifts.append({**shift, 'id': f'section_{section.section_index}_{shift["id"]}', 'start': generator.model.rational(absolute + low - start), 'end': generator.model.rational(absolute + high - start)})
|
||||
pickup = None
|
||||
if section.role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and output:
|
||||
pickup = generator.parts.build_measure(source[0], f'pickup-{section.section_index}', section.role, part_type, None)
|
||||
pickup = generator.parts.build_measure(source[0], f'pickup_{section.section_index}', section.role, part_type, None)
|
||||
tuplets.extend(generator.score.reidentify(([pickup] if pickup else []) + segment, staff.get('tuplets', [])))
|
||||
if pickup:
|
||||
destination = output[-1]
|
||||
@@ -104,11 +104,11 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
|
||||
low, high = max(onset - pad, generator.model.fraction(shift['start'])), min(generator.model.fraction(pickup['duration']), generator.model.fraction(shift['end']))
|
||||
if low < high:
|
||||
offset = absolute - generator.model.fraction(pickup['duration'])
|
||||
octave_shifts.append({**shift, 'id': f'pickup-{section.section_index}-{shift["id"]}', 'start': generator.model.rational(offset + low), 'end': generator.model.rational(offset + high)})
|
||||
octave_shifts.append({**shift, 'id': f'pickup_{section.section_index}_{shift["id"]}', 'start': generator.model.rational(offset + low), 'end': generator.model.rational(offset + high)})
|
||||
output.extend(segment)
|
||||
absolute += segment_length
|
||||
tuplets = generator.arrange.pickup.retain_links(output, tuplets)
|
||||
result = generator.score.make_arranged_document(document, staff, staff_id='staff-arranged-' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
||||
result = generator.score.make_arranged_document(document, staff, staff_id='staff_arranged_' + part_type, staff_name=part_type.capitalize(), role='vocal' if part_type == 'vocal' else 'guitar', arrangement_part_type=part_type, measures=output, tuplets=tuplets, octave_shifts=octave_shifts)
|
||||
result['metadata'].setdefault('tempo', 124)
|
||||
if part_type == 'acoustic':
|
||||
generator.acoustic.prepare(result)
|
||||
@@ -138,7 +138,7 @@ def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Pa
|
||||
reports = []
|
||||
for part in generator.parts.PART_TYPES:
|
||||
try:
|
||||
generator.score.write_json(output_dir / f'{generator.score.slugify(path.stem)}-{part}.json', _arrange_document(document, part, plans, scales=scales, reports=reports, source_name=path.name))
|
||||
generator.score.write_json(output_dir / f'{generator.score.slugify(path.stem)}_{part}.json', _arrange_document(document, part, plans, scales=scales, reports=reports, source_name=path.name))
|
||||
except ValueError as error:
|
||||
raise ValueError(f'{path}, part {part}: {error}') from error
|
||||
count += 1
|
||||
|
||||
@@ -72,7 +72,7 @@ def build_catalog(root: pathlib.Path, destination: pathlib.Path | None = None) -
|
||||
raise ValueError(f'{root}: score asset directory not found')
|
||||
destination = destination or root / 'catalog.db'
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='.vamp-catalog-', dir=destination.parent) as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='.vamp_catalog_', dir=destination.parent) as temporary:
|
||||
staged = pathlib.Path(temporary) / 'catalog.db'
|
||||
connection = sqlite3.connect(staged)
|
||||
try:
|
||||
|
||||
@@ -61,5 +61,5 @@ def reuse_voicings(links: list[VoicingLink]) -> None:
|
||||
if strings not in generator.guitar.ELECTRIC_STRINGS:
|
||||
continue
|
||||
note = link.target
|
||||
note['pitches'] = [copy.deepcopy({**{k: p[k] for k in ('letter', 'alter', 'octave', 'tablature')}, 'id': f'{note["id"]}-s{p["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']}) for p in reference['pitches']]
|
||||
note['pitches'] = [copy.deepcopy({**{k: p[k] for k in ('letter', 'alter', 'octave', 'tablature')}, 'id': f'{note["id"]}_s{p["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']}) for p in reference['pitches']]
|
||||
break
|
||||
|
||||
@@ -31,7 +31,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
raise generator.engraving.types.EngravingError('music systems overlap their page rows')
|
||||
audit_primitives(page.primitives, page.width, page.height, fonts)
|
||||
header = generator.engraving.types.bounds([p for p in page.primitives if p.role in ('title', 'subtitle')])
|
||||
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page-number'])
|
||||
footer = generator.engraving.types.bounds([p for p in page.primitives if p.role == 'page_number'])
|
||||
if page.systems[0].box.top < header.bottom + layout.style.page_clearance - 0.01 or page.systems[-1].box.bottom > footer.top - layout.style.page_clearance + 0.01:
|
||||
raise generator.engraving.types.EngravingError('music overlaps the page header or footer')
|
||||
for item in page.primitives:
|
||||
@@ -40,7 +40,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
for system in page.systems:
|
||||
if system.scale != scale:
|
||||
raise generator.engraving.types.EngravingError('notation size must remain consistent throughout a part')
|
||||
for role in ('staff', 'tab-staff'):
|
||||
for role in ('staff', 'tab_staff'):
|
||||
lines = [p for p in system.primitives if p.role == role]
|
||||
if lines:
|
||||
left = min(x for p in lines for x, _ in p.points)
|
||||
@@ -68,7 +68,7 @@ def audit_layout(document: dict, layout: generator.engraving.types.LayoutDocumen
|
||||
if index.staff.get('tablature') and anchor.fragment.kind == 'tone':
|
||||
tone = index.tones[anchor.source][1]
|
||||
tab = tone['tablature']
|
||||
tab_baseline = max(p.points[0][1] for p in system.primitives if p.role == 'tab-staff')
|
||||
tab_baseline = max(p.points[0][1] for p in system.primitives if p.role == 'tab_staff')
|
||||
expected_y = tab_baseline + (4 - (6 - tab['string']) * layout.style.tab_space) * system.scale
|
||||
expected_x = min(a.x for a in system.anchors if a.event == anchor.event and a.time == anchor.time)
|
||||
tabs = [p for p in system.primitives if p.role == 'tab' and anchor.source in p.sources]
|
||||
|
||||
@@ -16,12 +16,13 @@ import PIL.ImageChops
|
||||
import PIL.features
|
||||
|
||||
import generator.engraving.validation
|
||||
import generator.theme
|
||||
|
||||
def renderer_identity(fonts, style) -> dict:
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
versions = {name: importlib.metadata.version(name) for name in ('Pillow', 'fonttools', 'aggdraw')}
|
||||
versions.update({name: PIL.features.version(name) for name in PIL.features.get_supported()})
|
||||
sources = [*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'requirements.txt']
|
||||
sources = [*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'theme.py', root / 'palette.json']
|
||||
return {'fonts_style': fonts.fingerprint(style), 'python': {'implementation': platform.python_implementation(), 'version': platform.python_version()}, 'dependencies': versions,
|
||||
'sources': {path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(sources)}}
|
||||
|
||||
@@ -81,7 +82,7 @@ def store_pages(entry: pathlib.Path, destination: pathlib.Path, pages: list[dict
|
||||
if entry.is_symlink() or entry.is_file():
|
||||
entry.unlink(missing_ok=True)
|
||||
entry.mkdir(exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='.engraving-', dir=entry.parent) as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='.engraving_', dir=entry.parent) as temporary:
|
||||
staged = pathlib.Path(temporary)
|
||||
for page in pages:
|
||||
shutil.copyfile(destination / page['path'], staged / page['path'])
|
||||
@@ -113,7 +114,7 @@ def restore_band(entry: pathlib.Path, key: str, width: int, height: int) -> PIL.
|
||||
if image.format != 'PNG' or image.size != (width, height) or image.mode != 'RGB':
|
||||
return None
|
||||
image.load()
|
||||
ink = PIL.ImageChops.difference(image, PIL.Image.new('RGB', image.size, 'white')).getbbox()
|
||||
ink = PIL.ImageChops.difference(image, PIL.Image.new('RGB', image.size, generator.theme.rgb('background'))).getbbox()
|
||||
if not ink or ink[0] < 3 or ink[1] < 3 or ink[2] > width - 3 or ink[3] > height - 3:
|
||||
return None
|
||||
return image.copy()
|
||||
@@ -127,7 +128,7 @@ def store_band(entry: pathlib.Path, key: str, image: PIL.Image.Image) -> None:
|
||||
if entry.is_symlink() or entry.is_file():
|
||||
entry.unlink()
|
||||
entry.mkdir(exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='.engraving-', dir=entry.parent) as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='.engraving_', dir=entry.parent) as temporary:
|
||||
staged = pathlib.Path(temporary)
|
||||
image.save(staged / 'band.png', format='PNG')
|
||||
metadata = {'context': key, 'width': image.width, 'height': image.height, 'sha256': hashlib.sha256((staged / 'band.png').read_bytes()).hexdigest()}
|
||||
|
||||
@@ -57,7 +57,7 @@ def column(index: generator.engraving.notation.ScoreIndex, time: fractions.Fract
|
||||
items.append(fonts.text(generator.harmony.symbol(item), 0, 0, 18 if index.staff.get('tablature') else 21, 'harmony', (item['id'],)))
|
||||
items.extend(generator.engraving.scales.column(index, time, start, fonts))
|
||||
if time == measure_start:
|
||||
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure-number', (measure['id'],)))
|
||||
items.append(fonts.text(str(measure['number']), 0, -style.space * (index.state(time)['staff_details']['lines'] + 0.5), 12, 'measure_number', (measure['id'],)))
|
||||
if label := measure.get('section_label'):
|
||||
items.append(fonts.text(label, 0, 0, 16 if index.staff.get('tablature') else 19, 'section', (measure['id'] + ':section',)))
|
||||
return items, anchors, fragments
|
||||
@@ -219,7 +219,7 @@ def paginate(systems: list[generator.engraving.types.System], document: dict, fo
|
||||
index = index or generator.engraving.notation.ScoreIndex(document)
|
||||
header = part_header(document, fonts, style)
|
||||
top = generator.engraving.types.bounds(header).bottom + style.page_clearance
|
||||
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page-number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
|
||||
footers = [fonts.text(str(number), style.width / 2, style.height - style.margin_y, 12, 'page_number', (), True) for number in range(1, (len(systems) + 1) // 2 + 1)]
|
||||
bottom = min(p.box.top for p in footers) - style.page_clearance
|
||||
if bottom - top <= style.system_gap:
|
||||
raise generator.engraving.types.EngravingError('page header leaves no room for two music systems')
|
||||
|
||||
@@ -104,7 +104,7 @@ def draw_event(index: generator.engraving.notation.ScoreIndex, event: generator.
|
||||
head = fonts.glyph(glyph, x, y, tone_size, 'head', sources)
|
||||
group_items.append(head)
|
||||
if tone.get('notehead', {}).get('parentheses') == 'yes':
|
||||
group_items.extend((fonts.text('(', head.box.left - 6, y + 5, 18, 'head-parentheses', sources), fonts.text(')', head.box.right + 2, y + 5, 18, 'head-parentheses', sources)))
|
||||
group_items.extend((fonts.text('(', head.box.left - 6, y + 5, 18, 'head_parentheses', sources), fonts.text(')', head.box.right + 2, y + 5, 18, 'head_parentheses', sources)))
|
||||
ledger = -2
|
||||
while position <= ledger:
|
||||
group_items.append(generator.engraving.types.line('ledger', sources, ((head.box.left - 5, -ledger * style.space / 2), (head.box.right + 5, -ledger * style.space / 2)), 1.8))
|
||||
@@ -149,7 +149,7 @@ def draw_event(index: generator.engraving.notation.ScoreIndex, event: generator.
|
||||
flag = 'flag' + ('8th' if written['type'] == 'eighth' else written['type']) + ('Up' if direction == -1 else 'Down')
|
||||
group_items.append(fonts.glyph(flag, anchor.stem_x, stem_y, size, 'flag', references))
|
||||
if note.get('grace', {}).get('slash'):
|
||||
group_items.append(generator.engraving.types.line('grace-slash', references, ((anchor.stem_x - 5, stem_y + 14), (anchor.stem_x + 9, stem_y + 4)), 1.5))
|
||||
group_items.append(generator.engraving.types.line('grace_slash', references, ((anchor.stem_x - 5, stem_y + 14), (anchor.stem_x + 9, stem_y + 4)), 1.5))
|
||||
if note.get('tremolo', {}).get('type') == 'single':
|
||||
for mark in range(note['tremolo']['marks']):
|
||||
ty = stem_y - direction * (10 + mark * 5)
|
||||
|
||||
@@ -116,7 +116,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
|
||||
scores = {}
|
||||
audited = set()
|
||||
result = GroupResult()
|
||||
with tempfile.TemporaryDirectory(prefix='vamp-notation-') as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='vamp_notation_') as temporary:
|
||||
bodies = BodyBands(pathlib.Path(temporary), cache_dir, result)
|
||||
try:
|
||||
for path, score_key in paths:
|
||||
@@ -147,7 +147,7 @@ def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, prog
|
||||
audited.add(placement)
|
||||
else:
|
||||
for page in pages_layout:
|
||||
decoration = tuple(p for p in page.primitives if p.role in ('title', 'subtitle', 'page-number'))
|
||||
decoration = tuple(p for p in page.primitives if p.role in ('title', 'subtitle', 'page_number'))
|
||||
generator.engraving.audit.audit_primitives(decoration, page.width, page.height, family)
|
||||
result.timings['pagination'] += time.monotonic() - started
|
||||
pages = []
|
||||
|
||||
@@ -10,6 +10,7 @@ import fontTools.pens.basePen
|
||||
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.types
|
||||
import generator.theme
|
||||
|
||||
class OutlinePen(fontTools.pens.basePen.BasePen):
|
||||
def __init__(self, glyphs, scale: float, x: float, y: float):
|
||||
@@ -34,7 +35,7 @@ def rasterize_page(page: generator.engraving.types.Page, fonts: generator.engrav
|
||||
|
||||
def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[generator.engraving.types.Primitive, ...], top: int, bottom: int, fonts: generator.engraving.fonts.Fonts) -> PIL.Image.Image:
|
||||
scale = page.supersampling
|
||||
image = PIL.Image.new('RGB', (page.width * scale, (bottom - top) * scale), 'white')
|
||||
image = PIL.Image.new('RGB', (page.width * scale, (bottom - top) * scale), generator.theme.rgb('background'))
|
||||
context = aggdraw.Draw(image)
|
||||
for item in primitives:
|
||||
points = [(x * scale, y * scale - top * scale) for x, y in item.points]
|
||||
@@ -55,15 +56,15 @@ def rasterize_band(page: generator.engraving.types.Page, primitives: tuple[gener
|
||||
if item.kind == 'polygon':
|
||||
path.close()
|
||||
if item.kind in ('glyph', 'polygon'):
|
||||
context.path(path, None, aggdraw.Brush('black'))
|
||||
context.path(path, None, aggdraw.Brush(generator.theme.rgb('ink')))
|
||||
else:
|
||||
context.path(path, aggdraw.Pen('black', item.thickness * scale))
|
||||
context.path(path, aggdraw.Pen(generator.theme.rgb('ink'), item.thickness * scale))
|
||||
context.flush()
|
||||
drawing = PIL.ImageDraw.Draw(image)
|
||||
for item in primitives:
|
||||
if item.kind == 'text':
|
||||
x, y = item.points[0]
|
||||
drawing.text((x * scale, y * scale - top * scale), item.value, font=fonts.text_font(item.size * scale), anchor='ls', fill='black')
|
||||
drawing.text((x * scale, y * scale - top * scale), item.value, font=fonts.text_font(item.size * scale), anchor='ls', fill=generator.theme.rgb('ink'))
|
||||
return image.resize((page.width, bottom - top), PIL.Image.Resampling.LANCZOS)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -75,7 +76,7 @@ class Band:
|
||||
def page_bands(page: generator.engraving.types.Page) -> tuple[Band, Band, Band] | None:
|
||||
body = tuple(p for system in page.systems for p in system.primitives)
|
||||
header = tuple(p for p in page.primitives if p.role in ('title', 'subtitle'))
|
||||
footer = tuple(p for p in page.primitives if p.role == 'page-number')
|
||||
footer = tuple(p for p in page.primitives if p.role == 'page_number')
|
||||
if not body or not header or not footer or page.primitives != header + body + footer:
|
||||
return None
|
||||
bands = []
|
||||
@@ -91,7 +92,7 @@ def page_bands(page: generator.engraving.types.Page) -> tuple[Band, Band, Band]
|
||||
return tuple(bands)
|
||||
|
||||
def compose_page(page: generator.engraving.types.Page, fonts: generator.engraving.fonts.Fonts, bands: tuple[Band, Band, Band], body: PIL.Image.Image) -> PIL.Image.Image:
|
||||
image = PIL.Image.new('RGB', (page.width, page.height), 'white')
|
||||
image = PIL.Image.new('RGB', (page.width, page.height), generator.theme.rgb('background'))
|
||||
if body.mode != 'RGB' or body.size != (page.width, bands[1].bottom - bands[1].top):
|
||||
raise generator.engraving.types.EngravingError('notation band has incompatible geometry')
|
||||
image.paste(body, (0, bands[1].top))
|
||||
|
||||
@@ -65,7 +65,7 @@ def draw_beams(index: generator.engraving.notation.ScoreIndex, anchors: list[gen
|
||||
x1 = left if index.by_id[group[0]].start < start else first.stem_x
|
||||
x2 = right if index.by_id[group[-1]].start >= end else last.stem_x
|
||||
for anchor in selected:
|
||||
additions.append(generator.engraving.types.line('beam-stem', (anchor.event,), ((anchor.stem_x, anchor.stem_y), (anchor.stem_x, beam_y)), 1.4))
|
||||
additions.append(generator.engraving.types.line('beam_stem', (anchor.event,), ((anchor.stem_x, anchor.stem_y), (anchor.stem_x, beam_y)), 1.4))
|
||||
for level in range(1, max(a.flags for a in selected) + 1):
|
||||
y = beam_y - direction * (level - 1) * style.space * 0.75
|
||||
for i, anchor in enumerate(selected):
|
||||
@@ -262,9 +262,9 @@ def draw_lyric_spans(index: generator.engraving.notation.ScoreIndex, items: list
|
||||
if x2 <= x1:
|
||||
continue
|
||||
if extend in ('start', 'continue'):
|
||||
result.append(generator.engraving.types.line('lyric-extension', (identifier,), ((x1, y + 3), (x2, y + 3)), 1))
|
||||
result.append(generator.engraving.types.line('lyric_extension', (identifier,), ((x1, y + 3), (x2, y + 3)), 1))
|
||||
elif following:
|
||||
count = max(1, int((x2 - x1) / 90))
|
||||
for dash in range(count):
|
||||
result.append(fonts.text('-', x1 + (x2 - x1) * (dash + 1) / (count + 1), y, 18, 'lyric-hyphen', (identifier,), True))
|
||||
result.append(fonts.text('-', x1 + (x2 - x1) * (dash + 1) / (count + 1), y, 18, 'lyric_hyphen', (identifier,), True))
|
||||
return result
|
||||
|
||||
@@ -10,7 +10,7 @@ def column(index, event, anchors, fonts, style):
|
||||
sources = (event.value['id'],)
|
||||
if not event.value['pitches']:
|
||||
anchor = anchors[0]
|
||||
return [fonts.glyph('noteheadSlashHorizontalEnds', x, -2 * style.tab_space, style.space, 'tab-slash', sources)] if anchor.fragment.kind == 'slash' else []
|
||||
return [fonts.glyph('noteheadSlashHorizontalEnds', x, -2 * style.tab_space, style.space, 'tab_slash', sources)] if anchor.fragment.kind == 'slash' else []
|
||||
result = []
|
||||
for anchor in anchors:
|
||||
tone = index.tones[anchor.source][1]
|
||||
@@ -27,23 +27,23 @@ def staff(index, items, start, end, left, right, fonts, style):
|
||||
offset = generator.engraving.types.bounds(notation).bottom + style.tab_gap + 5 * style.tab_space
|
||||
numbers = [p.moved(0, offset) for p in numbers]
|
||||
drawing = list(numbers)
|
||||
drawing.append(fonts.text('TAB', 0, offset - 2 * style.tab_space, style.tab_font_size, 'tab-label', ()))
|
||||
drawing.append(fonts.text('TAB', 0, offset - 2 * style.tab_space, style.tab_font_size, 'tab_label', ()))
|
||||
for string in range(1, 7):
|
||||
y = offset - (6 - string) * style.tab_space
|
||||
cuts = sorted((max(0, p.box.left - 2), min(right, p.box.right + 2)) for p in numbers if p.box.top <= y <= p.box.bottom)
|
||||
cursor = 0
|
||||
for a, b in cuts + [(right, right)]:
|
||||
if a > cursor:
|
||||
drawing.append(generator.engraving.types.line('tab-staff', (), ((cursor, y), (a, y)), style.space * 0.13))
|
||||
drawing.append(generator.engraving.types.line('tab_staff', (), ((cursor, y), (a, y)), style.space * 0.13))
|
||||
cursor = max(cursor, b)
|
||||
for bar in (p for p in notation if p.role == 'barline'):
|
||||
drawing.append(generator.engraving.types.line('tab-barline', bar.sources, tuple((x, offset + y * 5 * style.tab_space / (4 * style.space)) for x, y in bar.points), bar.thickness))
|
||||
drawing.append(generator.engraving.types.line('tab_barline', bar.sources, tuple((x, offset + y * 5 * style.tab_space / (4 * style.space)) for x, y in bar.points), bar.thickness))
|
||||
by_source = {source: p for p in numbers if p.role == 'tab' for source in p.sources[1:]}
|
||||
|
||||
def tie(identifier, tone, x1, x2):
|
||||
y = offset - (6 - tone['tablature']['string']) * style.tab_space + 6
|
||||
if x2 > x1:
|
||||
drawing.append(generator.engraving.types.curve('tab-tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y + 5), (x2 - (x2 - x1) / 3, y + 5), (x2, y))))
|
||||
drawing.append(generator.engraving.types.curve('tab_tie', (identifier,), ((x1, y), (x1 + (x2 - x1) / 3, y + 5), (x2 - (x2 - x1) / 3, y + 5), (x2, y))))
|
||||
|
||||
for identifier, (event, tone) in index.tones.items():
|
||||
first = by_source.get(identifier)
|
||||
|
||||
@@ -9,6 +9,7 @@ import PIL.Image
|
||||
import PIL.ImageChops
|
||||
|
||||
import generator.engraving.types
|
||||
import generator.theme
|
||||
|
||||
ENGINE_NAME = 'Vamp Python'
|
||||
PAGE_WIDTH = 976
|
||||
@@ -19,9 +20,10 @@ def check_content(data: bytes, name: str) -> None:
|
||||
if image.format != 'PNG' or image.size != (PAGE_WIDTH, PAGE_HEIGHT):
|
||||
raise generator.engraving.types.EngravingError(f'{name}: expected a {PAGE_WIDTH}x{PAGE_HEIGHT} PNG page')
|
||||
image.load()
|
||||
white = PIL.Image.new('RGBA', image.size, 'white')
|
||||
white.alpha_composite(image.convert('RGBA'))
|
||||
ink = PIL.ImageChops.difference(white.convert('RGB'), PIL.Image.new('RGB', image.size, 'white')).convert('L').point(lambda x: 255 if x > 32 else 0)
|
||||
background = PIL.Image.new('RGB', image.size, generator.theme.rgb('background'))
|
||||
composited = background.convert('RGBA')
|
||||
composited.alpha_composite(image.convert('RGBA'))
|
||||
ink = PIL.ImageChops.difference(composited.convert('RGB'), background).convert('L').point(lambda x: 255 if x > 32 else 0)
|
||||
bounds = ink.getbbox()
|
||||
if not bounds:
|
||||
raise generator.engraving.types.EngravingError(f'{name}: engraver produced a blank page')
|
||||
|
||||
+7
-7
@@ -166,7 +166,7 @@ class EventPlacement:
|
||||
|
||||
def continued_sources(notes):
|
||||
result = {p['tie_from'] for n in notes for p in n['pitches'] if 'tie_from' in p}
|
||||
result.update(f'{n["_continue_from"]}-s{s}' for n in notes if '_continue_from' in n for s, _ in generator.fretboard.STANDARD_TUNING)
|
||||
result.update(f'{n["_continue_from"]}_s{s}' for n in notes if '_continue_from' in n for s, _ in generator.fretboard.STANDARD_TUNING)
|
||||
return result
|
||||
|
||||
def has_attack(notes: list[dict]) -> bool:
|
||||
@@ -206,11 +206,11 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
compatible = False
|
||||
break
|
||||
if '_continue_from' in note:
|
||||
previous = {p for i, p in active.items() if i.startswith(note['_continue_from'] + '-s')}
|
||||
previous = {p for i, p in active.items() if i.startswith(note['_continue_from'] + '_s')}
|
||||
if previous != set(added):
|
||||
compatible = False
|
||||
break
|
||||
assignment.update((f'{note["id"]}-s{p.string_number}', p) for p in added)
|
||||
assignment.update((f'{note["id"]}_s{p.string_number}', p) for p in added)
|
||||
choices.append(EventPlacement(note, tuple((p, exact_positions[p['id']]) for p in note['pitches']), added))
|
||||
if not compatible:
|
||||
continue
|
||||
@@ -220,7 +220,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d
|
||||
shape = tuple(p for _, p in exact) + positions
|
||||
if len({p.string_number for p in shape}) != len(shape) or not generator.fretboard.within_fret_span(shape):
|
||||
continue
|
||||
assignment = tuple(sorted((*exact, *((f'{chord["id"]}-s{p.string_number}', p) for p in positions)))) if chord else exact
|
||||
assignment = tuple(sorted((*exact, *((f'{chord["id"]}_s{p.string_number}', p) for p in positions)))) if chord else exact
|
||||
choices = tuple(EventPlacement(n, tuple((p, exact_positions[p['id']]) for p in n['pitches']), positions if n is chord else ()) for n in notes)
|
||||
cost = voicing_cost(shape, profile, requirements.allowed, melody) if fresh else VoicingCost()
|
||||
yield AttackCandidate(assignment, choices, shape, cost)
|
||||
@@ -244,7 +244,7 @@ def guitar_events(document: dict) -> tuple[list, dict[str, GuitarTone]]:
|
||||
if source and '_harmony' in source:
|
||||
note['_continue_from'] = source['id']
|
||||
for string, _ in generator.fretboard.STANDARD_TUNING:
|
||||
tones[f'{source["id"]}-s{string}'].pitch['tie_to'] = f'{note["id"]}-s{string}'
|
||||
tones[f'{source["id"]}_s{string}'].pitch['tie_to'] = f'{note["id"]}_s{string}'
|
||||
elif not note['pitches']:
|
||||
note.pop('_harmony', None)
|
||||
note['kind'] = 'rest'
|
||||
@@ -264,7 +264,7 @@ def guitar_events(document: dict) -> tuple[list, dict[str, GuitarTone]]:
|
||||
tones[tone['id']] = GuitarTone(tone, absolute + onset + generator.model.fraction(tone['duration']), options)
|
||||
if '_harmony' in note:
|
||||
for string, _ in generator.fretboard.STANDARD_TUNING:
|
||||
tones[f'{note["id"]}-s{string}'] = GuitarTone({}, absolute + onset + generator.model.fraction(note['duration']), ())
|
||||
tones[f'{note["id"]}_s{string}'] = GuitarTone({}, absolute + onset + generator.model.fraction(note['duration']), ())
|
||||
if note['pitches'] or '_harmony' in note:
|
||||
events.append((absolute + onset, not bool(note.get('grace')), order if note.get('grace') else -1, note))
|
||||
absolute += generator.model.fraction(measure['duration'])
|
||||
@@ -328,7 +328,7 @@ def place_guitar(document: dict) -> None:
|
||||
tone['tablature'] = {'string': position.string_number, 'fret': position.fret}
|
||||
if definition := note.pop('_harmony', None):
|
||||
for pitch in generator.fretboard.chord_pitches(definition, choice.added):
|
||||
note['pitches'].append({**pitch, 'id': f'{note["id"]}-s{pitch["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']})
|
||||
note['pitches'].append({**pitch, 'id': f'{note["id"]}_s{pitch["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']})
|
||||
if note.get('_harmonize') and not choice.added and not note.get('_rhythm_continuation'):
|
||||
skipped.append(note['id'])
|
||||
selected = selected.previous
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ def harmonize(document: dict) -> None:
|
||||
pieces = []
|
||||
for index, (a, b) in enumerate(zip(cuts, cuts[1:])):
|
||||
fragment = copy.deepcopy(note)
|
||||
fragment['id'] = note['id'] if index == 0 else f'{note["id"]}-h{index}'
|
||||
fragment['id'] = note['id'] if index == 0 else f'{note["id"]}_h{index}'
|
||||
fragment['onset'] = generator.model.rational(generator.model.fraction(note['onset']) + a - start)
|
||||
fragment['duration'] = generator.model.rational(b - a)
|
||||
if len(cuts) > 2:
|
||||
@@ -69,7 +69,7 @@ def harmonize(document: dict) -> None:
|
||||
fragment.pop('beams', None)
|
||||
for tone in fragment['pitches']:
|
||||
original = tone['id']
|
||||
tone['id'] = original if index == 0 else f'{original}-h{index}'
|
||||
tone['id'] = original if index == 0 else f'{original}_h{index}'
|
||||
tone['duration'], tone['written'] = fragment['duration'], copy.deepcopy(fragment['written'])
|
||||
tone.pop('tie_from', None)
|
||||
tone.pop('tie_to', None)
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ def make_event(identifier: str, onset: fractions.Fraction, duration: fractions.F
|
||||
notation = written or written_for(duration)
|
||||
event = {'id': identifier, 'kind': kind, 'onset': rational(onset), 'duration': rational(duration), 'written': notation, 'hidden': False, 'pitches': []}
|
||||
for i, p in enumerate(pitches or []):
|
||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}-p{i + 1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||
event['pitches'].append({**copy.deepcopy(p), 'id': f'{identifier}_p{i + 1}', 'duration': rational(duration), 'written': copy.deepcopy(notation)})
|
||||
if 'alter' in event['pitches'][-1]:
|
||||
event['pitches'][-1]['alter'] = rational(alteration(p['alter']))
|
||||
return event
|
||||
|
||||
@@ -176,7 +176,7 @@ class Importer:
|
||||
self.identity(node)
|
||||
kind = node.find('kind')
|
||||
self.harmony_sequence += 1
|
||||
result = {'id': f'harmony-{self.harmony_sequence}', 'onset': generator.model.rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||
result = {'id': f'harmony_{self.harmony_sequence}', 'onset': generator.model.rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
|
||||
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
|
||||
for tag in ('root', 'bass'):
|
||||
item = node.find(tag)
|
||||
@@ -247,7 +247,7 @@ class Importer:
|
||||
if action in ('up', 'down'):
|
||||
if number in self.open_octaves:
|
||||
self.fail('duplicate octave-shift start')
|
||||
span = {'id': f'octave-{len(self.octaves) + 1}', 'start': position, 'octaves': (size - 1) // 7 * (1 if action == 'up' else -1)}
|
||||
span = {'id': f'octave_{len(self.octaves) + 1}', 'start': position, 'octaves': (size - 1) // 7 * (1 if action == 'up' else -1)}
|
||||
self.open_octaves[number] = span
|
||||
self.octaves.append(span)
|
||||
elif action == 'stop':
|
||||
@@ -313,7 +313,7 @@ class Importer:
|
||||
if duration != expected:
|
||||
self.fail(f'<note> duration {duration} disagrees with written duration/time-modification {expected}')
|
||||
self.sequence += 1
|
||||
note = {'id': f'note-{self.sequence}', 'voice': voice, 'kind': 'rest' if node.find('rest') is not None else 'note', 'onset': generator.model.rational(onset), 'duration': generator.model.rational(duration), 'written': written, 'hidden': node.get('print-object') == 'no', 'pitches': []}
|
||||
note = {'id': f'note_{self.sequence}', 'voice': voice, 'kind': 'rest' if node.find('rest') is not None else 'note', 'onset': generator.model.rational(onset), 'duration': generator.model.rational(duration), 'written': written, 'hidden': node.get('print-object') == 'no', 'pitches': []}
|
||||
if is_chord:
|
||||
note = previous
|
||||
if grace is not None and not is_chord:
|
||||
@@ -333,7 +333,7 @@ class Importer:
|
||||
note['rest_position'] = {'letter': node.findtext('rest/display-step').lower(), 'octave': self.number(node.findtext('rest/display-octave'), integer=True)}
|
||||
else:
|
||||
pitch_node = node.find('pitch')
|
||||
p = {'id': f'{note["id"]}-p{len(note["pitches"]) + 1}', 'duration': generator.model.rational(duration), 'written': written}
|
||||
p = {'id': f'{note["id"]}_p{len(note["pitches"]) + 1}', 'duration': generator.model.rational(duration), 'written': written}
|
||||
if pitch_node is not None:
|
||||
self.children(pitch_node, {'step', 'alter', 'octave'})
|
||||
p.update({'letter': pitch_node.findtext('step', '').lower(), 'alter': generator.model.rational(self.number(pitch_node.findtext('alter', '0'))), 'octave': self.number(pitch_node.findtext('octave'), integer=True)})
|
||||
@@ -448,7 +448,7 @@ class Importer:
|
||||
active = [t for (v, _), t in self.active_tuplets.items() if v == voice]
|
||||
if active and (item.find('tuplet-actual') is None or item.find('tuplet-normal') is None):
|
||||
self.fail('nested tuplets require explicit tuplet-actual and tuplet-normal numbers')
|
||||
span = {'id': f'tuplet-{self.tuplet_sequence}', 'voice': voice, 'start': note['id'], 'note_count': self.number(actual, integer=True, positive=True), 'space_count': self.number(normal, integer=True, positive=True), 'depth': len(active), 'bracket': item.get('bracket', 'auto'), 'show_number': item.get('show-number', 'actual'), 'show_type': item.get('show-type', 'none'), 'line_shape': item.get('line-shape', 'straight')}
|
||||
span = {'id': f'tuplet_{self.tuplet_sequence}', 'voice': voice, 'start': note['id'], 'note_count': self.number(actual, integer=True, positive=True), 'space_count': self.number(normal, integer=True, positive=True), 'depth': len(active), 'bracket': item.get('bracket', 'auto'), 'show_number': item.get('show-number', 'actual'), 'show_type': item.get('show-type', 'none'), 'line_shape': item.get('line-shape', 'straight')}
|
||||
for tag in ('tuplet-actual', 'tuplet-normal'):
|
||||
detail = item.find(tag)
|
||||
if detail is not None:
|
||||
@@ -532,7 +532,7 @@ class Importer:
|
||||
voices = {}
|
||||
previous = None
|
||||
optional = []
|
||||
measure = {'id': f'measure-{index + 1}', 'number': bar.get('number', str(index + 1)), 'implicit': bar.get('implicit') == 'yes', 'harmonies': [], 'attributes': [], 'key': copy.deepcopy(self.key), 'meter': copy.deepcopy(self.meter), 'clef': copy.deepcopy(self.clef), 'transpose': copy.deepcopy(self.transpose), 'staff_details': copy.deepcopy(self.staff_details)}
|
||||
measure = {'id': f'measure_{index + 1}', 'number': bar.get('number', str(index + 1)), 'implicit': bar.get('implicit') == 'yes', 'harmonies': [], 'attributes': [], 'key': copy.deepcopy(self.key), 'meter': copy.deepcopy(self.meter), 'clef': copy.deepcopy(self.clef), 'transpose': copy.deepcopy(self.transpose), 'staff_details': copy.deepcopy(self.staff_details)}
|
||||
for item in bar:
|
||||
if item.tag == 'attributes':
|
||||
state = self.attributes(item, cursor)
|
||||
@@ -564,7 +564,7 @@ class Importer:
|
||||
duration = self.number(item.findtext('duration'), positive=True) / self.divisions
|
||||
if item.find('voice') is not None:
|
||||
self.sequence += 1
|
||||
voices.setdefault(self.voice_id, []).append(generator.model.make_event(f'spacer-{self.sequence}', cursor, duration, 'spacer') | {'voice': self.voice_id})
|
||||
voices.setdefault(self.voice_id, []).append(generator.model.make_event(f'spacer_{self.sequence}', cursor, duration, 'spacer') | {'voice': self.voice_id})
|
||||
cursor += duration
|
||||
extent = max(extent, cursor)
|
||||
previous = None
|
||||
@@ -619,7 +619,7 @@ class Importer:
|
||||
for p in n['pitches']:
|
||||
p.pop('_absolute', None)
|
||||
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
|
||||
result = {'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
|
||||
result = {'metadata': metadata, 'staves': [{'id': 'staff_source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
|
||||
result['staves'][0]['instruments'] = instruments
|
||||
generator.model.validate_document(result)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"background": "#111513",
|
||||
"panel": "#1c221f",
|
||||
"ink": "#dce5df",
|
||||
"muted": "#a5b3aa",
|
||||
"rule": "#48574f",
|
||||
"accent": "#80c9b5",
|
||||
"selection": "#293d34",
|
||||
"connecting": "#9fb9cc",
|
||||
"retrying": "#d6b879",
|
||||
"connected": "#80c9b5",
|
||||
"danger": "#e79891"
|
||||
}
|
||||
+2
-2
@@ -22,7 +22,7 @@ def source_paths(directory: pathlib.Path) -> list[pathlib.Path]:
|
||||
raise ValueError(f'{path}: performance sources belong directly under data/songs')
|
||||
identity = generator.score.slugify(path.stem)
|
||||
if identity in identities:
|
||||
raise ValueError(f'{identities[identity]} and {path}: conflicting output path {identity}-acoustic.json')
|
||||
raise ValueError(f'{identities[identity]} and {path}: conflicting output path {identity}_acoustic.json')
|
||||
identities[identity] = path
|
||||
return paths
|
||||
|
||||
@@ -45,7 +45,7 @@ def generated_voice(measure: dict, role: str) -> list[dict]:
|
||||
beat_index = 0
|
||||
while onset < end:
|
||||
duration = min(beats[beat_index % len(beats)] if beats else end - onset, end - onset)
|
||||
event = generator.model.make_event(f'slash-{len(events)}', onset, duration, 'note')
|
||||
event = generator.model.make_event(f'slash_{len(events)}', onset, duration, 'note')
|
||||
event['notehead'] = 'slash'
|
||||
events.append(event)
|
||||
onset += duration
|
||||
|
||||
+1
-1
@@ -89,6 +89,6 @@ def concert_document(document: dict, *, octave_transposing: bool = False) -> dic
|
||||
|
||||
def sounding_pitch(pitch: dict, state: dict):
|
||||
if 'unpitched' in pitch:
|
||||
raise ValueError('unpitched notes have no fretted or generated-tone interpretation')
|
||||
raise ValueError('unpitched notes have no fretted or generated_tone interpretation')
|
||||
transpose = state['transpose']
|
||||
return generator.model.pitch_number(pitch) + transpose['chromatic'] + 12 * transpose['octave_change']
|
||||
|
||||
@@ -13,7 +13,7 @@ import generator.model
|
||||
import generator.pitch
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
PROFILE = 'soft-struck'
|
||||
PROFILE = 'soft_struck'
|
||||
MAX_EVENTS = 1000000
|
||||
|
||||
class PlaybackUnavailable(ValueError):
|
||||
@@ -21,11 +21,11 @@ class PlaybackUnavailable(ValueError):
|
||||
|
||||
def sounding_pitch(tone: dict, state: dict) -> fractions.Fraction:
|
||||
if 'unpitched' in tone:
|
||||
raise PlaybackUnavailable('unpitched notation has no generated-tone interpretation')
|
||||
raise PlaybackUnavailable('unpitched notation has no generated_tone interpretation')
|
||||
value = generator.pitch.sounding_pitch(tone, state)
|
||||
frequency = 440 * 2 ** (float(value - 69) / 12)
|
||||
if not 20 <= frequency <= 18000:
|
||||
raise PlaybackUnavailable(f'sounding pitch {value} is outside the generated-tone range (20-18000 Hz)')
|
||||
raise PlaybackUnavailable(f'sounding pitch {value} is outside the generated_tone range (20-18000 Hz)')
|
||||
return value
|
||||
|
||||
def compile_timeline(document: dict) -> dict:
|
||||
@@ -133,7 +133,7 @@ def compile_timeline(document: dict) -> dict:
|
||||
if tremolo['type'] == 'start' and attack % 2:
|
||||
other = partner[0]['pitches'][note['pitches'].index(pitch)]
|
||||
amount = sounding_pitch(other, partner[2])
|
||||
events.append({'id': f'{pitch["id"]}-{attack}', 'pitch': generator.model.rational(amount), 'onset': generator.model.rational(performed + cursor), 'duration': generator.model.rational(min(step, length - cursor))})
|
||||
events.append({'id': f'{pitch["id"]}_{attack}', 'pitch': generator.model.rational(amount), 'onset': generator.model.rational(performed + cursor), 'duration': generator.model.rational(min(step, length - cursor))})
|
||||
cursor += step
|
||||
attack += 1
|
||||
if len(events) > MAX_EVENTS:
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
Pillow==12.3.0
|
||||
defusedxml==0.7.1
|
||||
fonttools==4.65.0
|
||||
aggdraw==1.4.1
|
||||
+2
-2
@@ -84,10 +84,10 @@ def load_library(root: pathlib.Path) -> Library:
|
||||
snippets = []
|
||||
for path in sorted(root.rglob('*.xml')):
|
||||
relative = path.relative_to(root)
|
||||
if len(relative.parts) != 3 or relative.parts[0] not in ('2-4', '3-4', '4-4', '5-4') or relative.parts[1] not in ('1', '2', '3'):
|
||||
if len(relative.parts) != 3 or relative.parts[0] not in ('2_4', '3_4', '4_4', '5_4') or relative.parts[1] not in ('1', '2', '3'):
|
||||
raise ValueError(f'{path}: rhythm sources belong under meter/series')
|
||||
snippet = read_snippet(path, relative.as_posix())
|
||||
if snippet.meter != relative.parts[0].replace('-', '/'):
|
||||
if snippet.meter != relative.parts[0].replace('_', '/'):
|
||||
raise ValueError(f'{path}: rhythm meter disagrees with its directory')
|
||||
snippets.append(snippet)
|
||||
if not snippets:
|
||||
|
||||
@@ -9,8 +9,8 @@ import generator.pitch
|
||||
|
||||
NAMES = {
|
||||
'major': ('Ionian', 'Dorian', 'Phrygian', 'Lydian', 'Mixolydian', 'Aeolian', 'Locrian'),
|
||||
'melodic-minor': ('Melodic Minor', 'Dorian b2', 'Lydian Augmented', 'Lydian Dominant', 'Mixolydian b6', 'Locrian natural 2', 'Altered'),
|
||||
'harmonic-minor': ('Harmonic Minor', 'Locrian natural 6', 'Ionian #5', 'Dorian #4', 'Phrygian Dominant', 'Lydian #2', 'Ultra Locrian'),
|
||||
'melodic_minor': ('Melodic Minor', 'Dorian b2', 'Lydian Augmented', 'Lydian Dominant', 'Mixolydian b6', 'Locrian natural 2', 'Altered'),
|
||||
'harmonic_minor': ('Harmonic Minor', 'Locrian natural 6', 'Ionian #5', 'Dorian #4', 'Phrygian Dominant', 'Lydian #2', 'Ultra Locrian'),
|
||||
}
|
||||
ROOTS = ('C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B')
|
||||
|
||||
@@ -76,17 +76,17 @@ def read_pattern(path: pathlib.Path, family: str, mode: int) -> Pattern:
|
||||
return Pattern(family, mode, tuple(int(n) for n in intervals[:-1]))
|
||||
|
||||
def load_library(root: pathlib.Path) -> Library:
|
||||
expected = {root / f'{family}-scale-modes' / f'{mode:04}.xml' for family in NAMES for mode in range(1, 8)}
|
||||
expected = {root / f'{family}_scale_modes' / f'{mode:04}.xml' for family in NAMES for mode in range(1, 8)}
|
||||
actual = set(root.rglob('*.xml'))
|
||||
if actual != expected:
|
||||
missing, unexpected = sorted(expected - actual), sorted(actual - expected)
|
||||
raise ValueError(f'{root}: expected all 21 scale patterns; missing: {missing}; unexpected: {unexpected}')
|
||||
patterns = tuple(read_pattern(root / f'{family}-scale-modes' / f'{mode:04}.xml', family, mode) for family in NAMES for mode in range(1, 8))
|
||||
patterns = tuple(read_pattern(root / f'{family}_scale_modes' / f'{mode:04}.xml', family, mode) for family in NAMES for mode in range(1, 8))
|
||||
for family in NAMES:
|
||||
modes = [pattern for pattern in patterns if pattern.family == family]
|
||||
parent = modes[0].intervals
|
||||
for index, pattern in enumerate(modes):
|
||||
rotation = tuple((parent[(index + n) % 7] - parent[index]) % 12 for n in range(7))
|
||||
if pattern.intervals != rotation:
|
||||
raise ValueError(f'{root / (family + "-scale-modes") / f"{pattern.mode:04}.xml"}: pattern is not mode {pattern.mode} of its family parent')
|
||||
raise ValueError(f'{root / (family + "_scale_modes") / f"{pattern.mode:04}.xml"}: pattern is not mode {pattern.mode} of its family parent')
|
||||
return Library(patterns)
|
||||
|
||||
@@ -22,7 +22,7 @@ def annotate(document: dict, solos: list[generator.scales.timeline.Solo], librar
|
||||
if previous is not None and previous['label'] == label and generator.model.fraction(previous['end']) == choice.span.start:
|
||||
previous['end'] = generator.model.rational(choice.span.end)
|
||||
else:
|
||||
previous = {'id': f'scale-{len(spans) + 1}', 'start': generator.model.rational(choice.span.start), 'end': generator.model.rational(choice.span.end), 'label': label, 'matched': choice.scale is not None}
|
||||
previous = {'id': f'scale_{len(spans) + 1}', 'start': generator.model.rational(choice.span.start), 'end': generator.model.rational(choice.span.end), 'label': label, 'matched': choice.scale is not None}
|
||||
spans.append(previous)
|
||||
if choice.scale is None:
|
||||
print(f'{source}, {staff["name"]}, source measure {choice.span.source_measure}, beat {choice.span.beat}: {choice.reason} Chord: {generator.harmony.symbol(choice.span.chord)}', flush=True)
|
||||
|
||||
@@ -27,22 +27,22 @@ def destination(chord: dict, following: dict | None, library: generator.scales.l
|
||||
if following and dominant(chord) and (root_number(chord) - root_number(following)) % 12 == 7:
|
||||
third = generator.harmony.degrees(following).get(3)
|
||||
if third in (-1, 0):
|
||||
return library.scale('harmonic-minor' if third == -1 else 'major', 1, generator.model.pitch_name(following['root']))
|
||||
return library.scale('harmonic_minor' if third == -1 else 'major', 1, generator.model.pitch_name(following['root']))
|
||||
return None
|
||||
|
||||
def preference(chord: dict, following: dict | None, after: dict | None, home: generator.scales.library.Scale, library: generator.scales.library.Library) -> tuple[generator.scales.library.Scale, str]:
|
||||
if any(root_number(item).denominator != 1 for item in (chord, following, after) if item):
|
||||
return home, 'non-semitone harmony cannot establish a destination in the permitted catalog'
|
||||
if target := destination(chord, following, library):
|
||||
return target, 'dominant approaching ' + generator.model.pitch_name(following['root']) + (' minor' if target.pattern.family == 'harmonic-minor' else ' major')
|
||||
return target, 'dominant approaching ' + generator.model.pitch_name(following['root']) + (' minor' if target.pattern.family == 'harmonic_minor' else ' major')
|
||||
degrees = generator.harmony.degrees(chord)
|
||||
if following and degrees.get(3) == -1 and degrees.get(7) == -1 and dominant(following) and (root_number(following) - root_number(chord)) % 12 == 5:
|
||||
tonic = generator.pitch.shift_pitch({**chord['root'], 'octave': 4}, -1, -2)
|
||||
target = destination(following, after, library) or library.scale('harmonic-minor' if degrees.get(5) == -1 else 'major', 1, generator.model.pitch_name(tonic))
|
||||
target = destination(following, after, library) or library.scale('harmonic_minor' if degrees.get(5) == -1 else 'major', 1, generator.model.pitch_name(tonic))
|
||||
if set(generator.harmony.pitch_classes(chord)) | set(generator.harmony.pitch_classes(following)) <= target.pitches:
|
||||
return target, 'shared ' + ('minor ii-V' if target.pattern.family == 'harmonic-minor' else 'major ii-V') + ' destination ' + target.root
|
||||
return target, 'shared ' + ('minor ii-V' if target.pattern.family == 'harmonic_minor' else 'major ii-V') + ' destination ' + target.root
|
||||
if following and degrees.get(3) == -1 and degrees.get(5) == -1 and degrees.get(7) == -2 and (root_number(following) - root_number(chord)) % 12 == 1:
|
||||
return library.scale('harmonic-minor', 1, generator.model.pitch_name(following['root'])), 'leading-tone diminished resolution to ' + generator.model.pitch_name(following['root'])
|
||||
return library.scale('harmonic_minor', 1, generator.model.pitch_name(following['root'])), 'leading-tone diminished resolution to ' + generator.model.pitch_name(following['root'])
|
||||
return home, 'declared tonal center ' + home.label()
|
||||
|
||||
def following_indices(timeline: tuple[generator.scales.timeline.ChordSpan, ...]) -> list[int | None]:
|
||||
|
||||
+7
-7
@@ -44,7 +44,7 @@ def write_json(path: pathlib.Path, document: dict) -> None:
|
||||
path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
return re.sub(r'[^a-z0-9]+', '-', value.strip().lower()).strip('-') or 'score'
|
||||
return re.sub(r'[^a-z0-9]+', '_', value.strip().lower()).strip('_') or 'score'
|
||||
|
||||
def source_staff(document: dict) -> dict:
|
||||
if len(document.get('staves', [])) != 1:
|
||||
@@ -71,9 +71,9 @@ def copy_source_voices(measure: dict, *, strip_lyrics: bool) -> list[dict]:
|
||||
|
||||
def make_measure(measure_id: str, source: dict, notes: list[dict], *, section_label: str = '') -> dict:
|
||||
result = copy.deepcopy(source)
|
||||
result.update({'id': measure_id, 'section_label': section_label, 'voices': [{'id': 'voice-arranged', 'notes': notes}]})
|
||||
result.update({'id': measure_id, 'section_label': section_label, 'voices': [{'id': 'voice_arranged', 'notes': notes}]})
|
||||
for note in notes:
|
||||
note['voice'] = 'voice-arranged'
|
||||
note['voice'] = 'voice_arranged'
|
||||
return result
|
||||
|
||||
def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||
@@ -82,14 +82,14 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||
tuplets = []
|
||||
for measure in measures:
|
||||
for hi, harmony in enumerate(measure.get('harmonies', [])):
|
||||
harmony['id'] = f'{measure["id"]}-h{hi + 1}'
|
||||
harmony['id'] = f'{measure["id"]}_h{hi + 1}'
|
||||
for ni, note in enumerate(measure_notes(measure)):
|
||||
old = note['id']
|
||||
note['id'] = f'{measure["id"]}-n{ni + 1}'
|
||||
note['id'] = f'{measure["id"]}_n{ni + 1}'
|
||||
event_map[old] = note['id']
|
||||
for pi, pitch in enumerate(note['pitches']):
|
||||
old_pitch = pitch['id']
|
||||
pitch['id'] = f'{note["id"]}-p{pi + 1}'
|
||||
pitch['id'] = f'{note["id"]}_p{pi + 1}'
|
||||
tone_map[old_pitch] = pitch['id']
|
||||
for measure in measures:
|
||||
for note in measure_notes(measure):
|
||||
@@ -111,7 +111,7 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
|
||||
if not members:
|
||||
continue
|
||||
t = copy.deepcopy(source)
|
||||
t.update({'id': f'{measures[0]["id"]}-{source["id"]}', 'start': members[0]['id'], 'end': members[-1]['id'], 'continued_start': source['start'] not in event_map, 'continued_end': source['end'] not in event_map})
|
||||
t.update({'id': f'{measures[0]["id"]}_{source["id"]}', 'start': members[0]['id'], 'end': members[-1]['id'], 'continued_start': source['start'] not in event_map, 'continued_end': source['end'] not in event_map})
|
||||
for n in members:
|
||||
n['tuplet_ids'] = [t['id'] if v == source['id'] else v for v in n['tuplet_ids']]
|
||||
tuplets.append(t)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""The product palette shared by build-time rasterization and both devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
PALETTE_PATH = pathlib.Path(__file__).with_name('palette.json')
|
||||
COLORS = json.loads(PALETTE_PATH.read_text(encoding='utf-8'))
|
||||
|
||||
def rgb(name: str) -> tuple[int, int, int]:
|
||||
value = COLORS[name]
|
||||
if not isinstance(value, str) or not re.fullmatch(r'#[0-9a-fA-F]{6}', value):
|
||||
raise ValueError(f'invalid palette color: {name}')
|
||||
return tuple(int(value[index:index + 2], 16) for index in (1, 3, 5))
|
||||
|
||||
def generate_header(destination: pathlib.Path) -> None:
|
||||
lines = ['#pragma once', '', 'namespace vamp_style {', '']
|
||||
for name in COLORS:
|
||||
if not re.fullmatch(r'[a-z][a-z_]*', name):
|
||||
raise ValueError(f'invalid palette name: {name}')
|
||||
r, g, b = rgb(name)
|
||||
lines.append(f'constexpr auto {name} = vamp_style::color{{{r}, {g}, {b}, 255}};')
|
||||
lines.extend(['', '}', ''])
|
||||
content = '\n'.join(lines)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not destination.exists() or destination.read_text() != content:
|
||||
destination.write_text(content, encoding='utf-8')
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--header', required=True, type=pathlib.Path)
|
||||
generate_header(parser.parse_args().header)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user