From bc7baea7c43a19992e1d9945773888f8ef81d3ce Mon Sep 17 00:00:00 2001 From: khodowany Date: Thu, 8 Oct 2026 02:13:45 -0500 Subject: [PATCH] Cleanup --- TECHNICAL_GUIDE.md | 10 + generator/__main__.py | 22 +- generator/arrange/accompaniment.py | 11 +- generator/arrange/assembly.py | 100 + generator/arrange/ending.py | 32 +- generator/arrange/ending_plan.py | 22 + generator/arrange/form.py | 58 + generator/arrange/generate.py | 40 +- generator/audio_assets.py | 68 + generator/cache.py | 82 + generator/catalog.py | 6 +- generator/chord_families.py | 14 +- generator/comping.py | 68 +- generator/comping_rhythm.py | 23 +- generator/compose.py | 13 +- generator/composition.py | 158 + generator/composition_types.py | 28 + generator/engraving/cache.py | 44 +- generator/engraving/layout.py | 6 +- generator/engraving/pipeline.py | 196 +- generator/engraving/worker.py | 190 + generator/guitar.py | 44 +- generator/model.py | 156 - generator/musicxml.py | 3 +- generator/musicxml_writer.py | 3 +- generator/parts.py | 1 - generator/performance.py | 62 +- generator/printing.py | 22 +- generator/production.py | 345 +- generator/role_cache.py | 29 + generator/score.py | 59 +- generator/score_io.py | 59 + generator/solo/authored.py | 27 +- generator/solo/composer.py | 31 +- generator/solo/decoder.py | 36 +- generator/solo/expressions.py | 39 +- generator/solo/harmonic.py | 17 +- generator/solo/profiles.py | 3 +- generator/solo/recognition.py | 26 +- generator/solo/report.py | 19 +- generator/{solo/analysis.py => source.py} | 40 +- generator/summary.py | 3 +- generator/train.py | 21 +- generator/validation.py | 161 + generator/vocabulary.py | 48 +- include/vamp_zero/score_loader.hpp | 3 +- include/vamp_zero/ui.hpp | 3 +- src/vamp_zero/app.cpp | 7 +- src/vamp_zero/score_loader.cpp | 20 +- src/vamp_zero/ui.cpp | 6 +- tests/CMakeLists.txt | 5 + tests/asset_contract.py | 17 + tests/fixtures/assets_manifest.json | 411 + tests/fixtures/role_outputs.json | 16725 ++++++++++++++++++++ tests/production_fixtures.py | 5 +- tests/solo_fixtures.py | 4 +- tests/test_acoustic.py | 10 +- tests/test_acoustic_pairs.py | 3 +- tests/test_acoustic_voicing.py | 7 +- tests/test_arrangement_ending.py | 12 +- tests/test_arrangement_pickup.py | 12 +- tests/test_catalog.py | 4 +- tests/test_chord_families.py | 11 +- tests/test_comping_cycles.py | 22 +- tests/test_comping_meters.py | 19 +- tests/test_comping_rhythm.py | 23 +- tests/test_comping_voicings.py | 33 +- tests/test_electric_voicing.py | 4 +- tests/test_engraving_cache.py | 9 +- tests/test_fret_policy.py | 21 +- tests/test_generator_boundaries.py | 64 + tests/test_guitar.py | 16 +- tests/test_guitar_parts.py | 11 +- tests/test_harmonic_recognition.py | 6 +- tests/test_harmony.py | 1 - tests/test_independent_generation.py | 54 +- tests/test_key_metadata.py | 3 +- tests/test_learned_solos.py | 24 +- tests/test_music_model.py | 13 +- tests/test_output_contract.py | 20 + tests/test_parts.py | 17 +- tests/test_performance.py | 13 +- tests/test_printing.py | 7 +- tests/test_publish_package.py | 12 +- tests/test_score_bundle.py | 30 +- tests/test_score_loader.cpp | 33 + tests/test_six_parts.py | 11 +- tests/test_voicing.py | 3 +- 88 files changed, 18844 insertions(+), 1335 deletions(-) create mode 100644 generator/arrange/assembly.py create mode 100644 generator/arrange/ending_plan.py create mode 100644 generator/audio_assets.py create mode 100644 generator/cache.py create mode 100644 generator/composition.py create mode 100644 generator/composition_types.py create mode 100644 generator/engraving/worker.py create mode 100644 generator/role_cache.py create mode 100644 generator/score_io.py rename generator/{solo/analysis.py => source.py} (56%) create mode 100644 generator/validation.py create mode 100644 tests/asset_contract.py create mode 100644 tests/fixtures/assets_manifest.json create mode 100644 tests/fixtures/role_outputs.json create mode 100644 tests/test_generator_boundaries.py create mode 100644 tests/test_output_contract.py diff --git a/TECHNICAL_GUIDE.md b/TECHNICAL_GUIDE.md index b4bcd12..0a5c3f0 100644 --- a/TECHNICAL_GUIDE.md +++ b/TECHNICAL_GUIDE.md @@ -49,6 +49,8 @@ The Zero menus use direct taps: release on a row to select it, tap switches to t In score view, Zero and Pico share one gesture state machine: a second release within 220 ms of the first release produces previous, a hold of at least 700 ms produces select once, and other taps produce next. A late second release preserves both single taps; holding consumes a pending first tap. Zero schedules pending-tap deadlines without polling and discards canceled or obsolete timer callbacks. +Score loading uses a dedicated single-worker executor for SQLite reads and JSON parsing. Completion returns to the control executor. The loader retains one in-flight request, coalesces pending selections, and discards stale results, keeping database work off HTTP and playback polling. + ## Score Generation The build runs `./modulus.py python -m generator` against `data/` and writes build-only assets under the CMake binary tree. Every song produces acoustic, electric, and vocal parts in its authored musical key. Source counts are discovered, not fixed in code. Generator-only optional chord markers are consumed before publishing. @@ -59,6 +61,14 @@ The pipeline is Soundslice/MuseScore -> uncompressed `.xml` -> musical model and ### Musical Data +Generator dependencies run from pure musical values and source analysis toward composition, assembly, and publication. `model` contains primitive values; `validation` checks score documents; `score` contains pure score operations; and `score_io` owns bundle/file validation. `source` owns canonical source identity and immutable source events. `arrange.form.FormPlan` supplies source-derived sections, ownership windows, and pickup stops to both accompaniment and role assembly. + +`composition` generates one role from an explicit immutable `vocabulary.CompositionContext`; `production` schedules typed worker requests and publishes the assembled parts. Lower musical functions receive that context explicitly. The scoped vocabulary API remains available for standalone callers and tests. Cached vocabulary loads return owned copies so one caller cannot alter another caller's resources. + +Role cache identities include the transitive generator imports of the composition entrypoint. Engraving cache identities similarly include renderer dependencies. Cache identities never seed musical choices. `cache` provides shared content identity and staged-file publication mechanics, while role, PNG, and PDF adapters retain their own validation. `engraving.worker` owns rendering and its bounded image cache; `engraving.pipeline` groups and schedules work. `performance` contains pure timing contracts, and `audio_assets` validates and copies supplied recordings. + +The reviewed output contract is checked by `tests.test_output_contract` for complete synthetic role scores, including learned solos, and by `python -m tests.asset_contract ` for every non-diagnostic asset. CTest runs the asset check against the normal build output. Musical output, JSON/catalog serialization, PNGs, and PDFs must remain unchanged during structural refactors; diagnostic text and cache identities may change. + `data/songs/` contains authoritative source scores. `data/training/manifest.json` pairs each authored solo with a source and a slot (1 or 2), and declares its pitch convention. Both slots train **one pooled model**. Slot 1 maps to acoustic playback and slot 2 to electric playback; slot and instrument names are not model features. The four XML files retain their notes, harmony, timing, ties, rests, Autumn Leaves pickup and All of Me's full-bar opening. Explanatory labels such as `Lead in`, `b9` and `6` were removed; they were not model features. Written chord extensions remain intact. `data/models/solo.json` is an explicitly trained, versioned statistical model. Its short-context counts, rhythm and register statistics are learned from the paired examples. Provenance records input hashes and the training version. This is supervised use of the supplied solos, not independence from them and not a neural network pretrained on an external corpus. diff --git a/generator/__main__.py b/generator/__main__.py index d950ba4..3416cc7 100644 --- a/generator/__main__.py +++ b/generator/__main__.py @@ -2,14 +2,14 @@ from __future__ import annotations import argparse import json +import os import pathlib +import platform import shutil import tempfile import time -import modulus.filesystem - -import generator.arrange.generate +import generator.audio_assets import generator.catalog import generator.engraving.cache import generator.engraving.fonts @@ -17,12 +17,12 @@ import generator.engraving.pipeline import generator.engraving.types import generator.parts import generator.pico_artwork -import generator.performance import generator.printing +import generator.production import generator.score +import generator.score_io import generator.summary -import platform -import os +import modulus.filesystem RUNTIME_FONT_FILES = ("PetalumaScript.otf", "OFL.txt") @@ -87,7 +87,7 @@ def generate_assets(args: argparse.Namespace) -> None: staged.mkdir(parents=True, exist_ok=True) copy_runtime_fonts(static_assets_dir, staged) report('generating performance parts') - performance_count = generator.arrange.generate.generate_performance_scores(data_dir / 'songs', staged / 'songs', composition_jobs=getattr(args, 'composition_jobs', 2), cache_dir=getattr(args, 'composition_cache', None), progress=progress, timings=timings) + performance_count = generator.production.generate_tree(data_dir / 'songs', staged / 'songs', jobs=getattr(args, 'composition_jobs', 2), cache_dir=getattr(args, 'composition_cache', None), progress=progress, timings=timings) if getattr(args, 'trace_level', 'summary') == 'summary': for path in (staged / 'analysis').glob('*.roles'): roles = json.loads(path.read_text()) @@ -95,13 +95,13 @@ def generate_assets(args: argparse.Namespace) -> None: trace = role.get('details', {}).get('selection_trace', {}) trace.pop('decisions', None) path.write_text(json.dumps(roles, sort_keys=True, indent=2)) - generator.score.validate_generated_tree(staged) + generator.score_io.validate_generated_tree(staged) if args.check: generator.summary.validate_expected_performance_summary(pathlib.Path(__file__).resolve().parents[1] / 'tests' / 'fixtures' / 'performance_summary.json', staged / 'songs') report('copying supplied recordings') song_ids = {generator.score.slugify(path.stem) for path in (data_dir / 'songs').glob('*.xml')} - audio_assets = generator.performance.copy_audio(static_assets_dir, staged, song_ids) - generator.performance.validate_recording_lengths(staged, audio_assets) + audio_assets = generator.audio_assets.copy_audio(static_assets_dir, staged, song_ids) + generator.audio_assets.validate_recording_lengths(staged, audio_assets) report('engraving pages') validated_pages = generator.engraving.pipeline.engrave_tree(staged, static_assets_dir / 'fonts' / 'petaluma', cache_dir=cache_dir, jobs=getattr(args, 'jobs', 1), progress=progress) report('generating printable parts') @@ -109,7 +109,7 @@ def generate_assets(args: argparse.Namespace) -> None: report('rendering Pico titles') generator.pico_artwork.generate_titles(staged, static_assets_dir / 'fonts' / 'petaluma') report('validating score, page, and audio assets') - generator.score.validate_generated_tree(staged, validated_pages) + generator.score_io.validate_generated_tree(staged, validated_pages) report('building catalog database') generator.catalog.build_catalog(staged, audio_assets=audio_assets) with modulus.filesystem.publish_directories([(staged, output_dir)]): diff --git a/generator/arrange/accompaniment.py b/generator/arrange/accompaniment.py index b47e2ff..21bc2c4 100644 --- a/generator/arrange/accompaniment.py +++ b/generator/arrange/accompaniment.py @@ -1,20 +1,13 @@ """One learned accompaniment per instrument, reused at authored form positions.""" from __future__ import annotations -import copy import fractions import generator.accompaniment -import generator.acoustic -import generator.acoustic_voicing -import generator.electric -import generator.guitar -import generator.fretboard -import generator.model -import generator.parts -import generator.score import generator.arrange.form import generator.arrange.pickup +import generator.model +import generator.score def harmonic_key(measure, previous): diff --git a/generator/arrange/assembly.py b/generator/arrange/assembly.py new file mode 100644 index 0000000..7c8c367 --- /dev/null +++ b/generator/arrange/assembly.py @@ -0,0 +1,100 @@ +"""Assemble source-defined role windows without composing or respelling.""" +from __future__ import annotations +import copy +import fractions + +import generator.accompaniment +import generator.arrange.pickup +import generator.composition_types +import generator.fretboard +import generator.model +import generator.parts +import generator.validation + +def mask(document, allowed): + """Apply source-defined role exclusions, without observing another part.""" + absolute = fractions.Fraction() + for measure in document['staves'][0]['measures']: + length = generator.model.fraction(measure['duration']) + for voice in measure['voices']: + selected = [] + for n in voice['notes']: + start = absolute + generator.model.fraction(n['onset']) + end = start + generator.model.fraction(n['duration']) + intervals = [(max(start, a), min(end, b)) for a, b in allowed if a < end and b > start] + for a, b in intervals: + note = copy.deepcopy(n) + if a != start: + note['onset'] = generator.model.rational(a - absolute) + for pitch in note['pitches']: + pitch.pop('tie_from', None) + generator.arrange.pickup.truncate(note, b - absolute) + if a < b: + selected.append(note) + voice['notes'] = fill(selected, length, measure['id'] + '_' + voice['id'], voice['id']) + absolute += length + staff = document['staves'][0] + staff['tuplets'] = generator.arrange.pickup.retain_links(staff['measures'], staff.get('tuplets', [])) + +def fill(notes, length, prefix, voice='voice_arranged'): + notes = sorted(notes, key=lambda n: generator.model.fraction(n['onset'])) + result = [] + cursor = fractions.Fraction() + + def rests(end): + nonlocal cursor + for duration, written in generator.accompaniment.written_durations(end - cursor): + result.append(generator.model.make_event(prefix + '_rest_' + str(len(result)), cursor, duration, 'rest', written=written) | {'voice': voice}) + cursor += duration + + for n in notes: + start = generator.model.fraction(n['onset']) + if start < cursor: + raise ValueError(prefix + ': overlapping independently generated roles') + rests(start) + n['voice'] = voice + result.append(n) + cursor = start + generator.model.fraction(n['duration']) + rests(length) + return result + +def assemble(source, results): + """Union prevalidated role windows; no selection or pitch/fingering rewrite.""" + documents = {} + for part in generator.parts.PART_TYPES: + selected = [results[r]['document'] for r in generator.composition_types.ROLES if r.startswith(part + '_')] + document = copy.deepcopy(selected[0]) + staff = document['staves'][0] + if part == 'vocal': + documents[part] = document + continue + staff['tuplets'] = [] + staff['octave_shifts'] = [] + staff['scale_spans'] = [] + for other in selected: + for field in ('tuplets', 'octave_shifts', 'scale_spans'): + staff[field].extend(copy.deepcopy(other['staves'][0].get(field, []))) + for i, m in enumerate(staff['measures']): + # Tuplet rests belong to the source/role rhythm and must survive union. + lanes = {} + for d in selected: + for voice in d['staves'][0]['measures'][i]['voices']: + lane = voice['id'] if part == 'acoustic' and voice['id'] in ('upper', 'bass') else 'voice_arranged' + sounding = [copy.deepcopy(n) for n in voice['notes'] if n['pitches'] or n.get('tuplet_ids')] + if sounding: + lanes.setdefault(lane, []).extend(sounding) + lanes = lanes or {'voice_arranged': []} + m['voices'] = [{'id': lane, 'notes': fill(notes, generator.model.fraction(m['duration']), m['id'] + '_' + lane, lane)} for lane, notes in lanes.items()] + # Attribute changes belong to source melody pickups and are assembly metadata. + for d in selected: + other = d['staves'][0]['measures'][i] + if other.get('attributes'): + m['attributes'] = copy.deepcopy(other['attributes']) + staff['tuplets'] = generator.arrange.pickup.retain_links(staff['measures'], staff['tuplets']) + owners = {n['id']: v['id'] for m in staff['measures'] for v in m['voices'] for n in v['notes']} + for span in staff['tuplets']: + span['voice'] = owners[span['start']] + generator.validation.validate_document(document) + generator.fretboard.validate_document(document) + documents[part] = document + return documents diff --git a/generator/arrange/ending.py b/generator/arrange/ending.py index b24aa3e..4951379 100644 --- a/generator/arrange/ending.py +++ b/generator/arrange/ending.py @@ -4,44 +4,30 @@ import fractions import generator.accompaniment import generator.acoustic_voicing +import generator.arrange.ending_plan import generator.comping import generator.fretboard import generator.model import generator.parts import generator.score - -BARS = 3 - -def template(source): - result = copy.deepcopy(source[-1]) - # An ending is three full bars in the final active meter, never a pickup. - for change in result.get('attributes', []): - for key in ('meter', 'key', 'clef', 'transpose', 'staff_details'): - if key in change: - result[key] = copy.deepcopy(change[key]) - result.update(duration=generator.model.rational(generator.model.meter_length(result['meter'])), implicit=False, - attributes=[], harmonies=[], voices=[], tempos=[], section_label='') - result['time_signature'] = generator.model.meter_label(result['meter']) - return result - -def duration(source): - return BARS * generator.model.fraction(template(source)['duration']) if source else fractions.Fraction() +import generator.vocabulary def chord(source): effective = [{**m, 'harmonies': [h for h in m['harmonies'] if not h.get('chord_optional')]} for m in source] prior = generator.accompaniment.carried_chords(effective)[-1] return generator.accompaniment.active_chord(effective[-1], generator.model.fraction(effective[-1]['duration']), prior) -def append(source, output, part, *, sounding=False, center=55): +def append(source, output, part, *, sounding=False, center=55, composition=None): + composition = composition or generator.vocabulary.current() if not source or not output: return - model = template(source) + model = generator.arrange.ending_plan.template(source) harmony = chord(source) grip = () if sounding: if harmony is None: raise ValueError('Ending: final performed harmony is missing or N.C.; cannot invent a closing chord') - options = generator.comping.voicings(generator.comping.semantic_key(harmony), part) + options = generator.comping.voicings(generator.comping.semantic_key(harmony), part, composition=composition) recent = {} for m in reversed(output): for n in sorted(generator.score.measure_notes(m), key=lambda n: generator.model.fraction(n['onset']), reverse=True): @@ -55,10 +41,10 @@ def append(source, output, part, *, sounding=False, center=55): if part == 'acoustic': bass = next((recent[s] for s in (5, 6) if s in recent), None) previous = generator.acoustic_voicing.Voicing(bass, *(recent[s] for s in (4, 3, 2))) if bass and all(s in recent for s in (4, 3, 2)) else () - grip = min(options, key=lambda v: (generator.comping.local_cost(part, generator.comping.semantic_key(harmony), v, center).moved(generator.comping.movement(part, previous, v)), v)) + grip = min(options, key=lambda v: (generator.comping.local_cost(part, generator.comping.semantic_key(harmony), v, center, composition=composition).moved(generator.comping.movement(part, previous, v)), v)) length = generator.model.fraction(model['duration']) previous = None - for index in range(BARS): + for index in range(generator.arrange.ending_plan.BARS): measure = generator.parts.build_measure(model, f'measure_{len(output) + 1}', 'rest', part, None, 'Ending' if index == 0 else '') measure['ending_sustain'] = True if index == 0 and harmony: @@ -77,4 +63,4 @@ def append(source, output, part, *, sounding=False, center=55): cursor += duration measure['voices'] = [{'id': 'voice_arranged', 'notes': notes}] output.append(measure) - generator.score.reidentify(output[-BARS:], []) + generator.score.reidentify(output[-generator.arrange.ending_plan.BARS:], []) diff --git a/generator/arrange/ending_plan.py b/generator/arrange/ending_plan.py new file mode 100644 index 0000000..6d40af7 --- /dev/null +++ b/generator/arrange/ending_plan.py @@ -0,0 +1,22 @@ +"""Source-derived ending geometry, independent of instrument composition.""" +import copy +import fractions + +import generator.model + +BARS = 3 + +def template(source): + result = copy.deepcopy(source[-1]) + # An ending is three full bars in the final active meter, never a pickup. + for change in result.get('attributes', []): + for key in ('meter', 'key', 'clef', 'transpose', 'staff_details'): + if key in change: + result[key] = copy.deepcopy(change[key]) + result.update(duration=generator.model.rational(generator.model.meter_length(result['meter'])), implicit=False, + attributes=[], harmonies=[], voices=[], tempos=[], section_label='') + result['time_signature'] = generator.model.meter_label(result['meter']) + return result + +def duration(source): + return BARS * generator.model.fraction(template(source)['duration']) if source else fractions.Fraction() diff --git a/generator/arrange/form.py b/generator/arrange/form.py index 0f34b13..743e219 100644 --- a/generator/arrange/form.py +++ b/generator/arrange/form.py @@ -1,8 +1,12 @@ from __future__ import annotations import dataclasses +import fractions import typing +import generator.arrange.ending_plan +import generator.arrange.pickup +import generator.model import generator.score @dataclasses.dataclass(frozen=True) @@ -55,3 +59,57 @@ def full_arrangement_sections(measures: list[dict], part_type: str) -> list[Arra for label, start, count, index in specs if count > 0 ] + +@dataclasses.dataclass(frozen=True) +class FormPlan: + sections: tuple[ArrangementSection, ...] + windows: tuple[tuple[fractions.Fraction, fractions.Fraction, str], ...] + stops: tuple[tuple[int, fractions.Fraction] | None, ...] + @classmethod + def read(cls, document, part): + source = generator.score.source_staff(document)['measures'] + sections = tuple(full_arrangement_sections(source, part)) + stops = tuple(section_stop(source, section, sections[i + 1] if i + 1 < len(sections) else None) for i, section in enumerate(sections)) + return cls(sections, _windows(document, part, sections), stops) + +def section_stop(source, section, following): + if following is None or not generator.score.body_start_measure_index(source): + return None + last = section.source_measure_index + section.measure_count - 1 + pad = generator.arrange.pickup.offset(source[last], source[0]) + if following.role == 'slash': + return last, pad + if following.role == 'melody' and generator.arrange.pickup.enters(following, source): + attacks = [generator.model.fraction(note['onset']) for note in generator.score.measure_notes(source[0]) if note['pitches']] + if attacks: + return last, pad + min(attacks) + return None + +def windows(document, part): + return FormPlan.read(document, part).windows + +def _windows(document, part, sections): + source = generator.score.source_staff(document)['measures'] + result, cursor = [], fractions.Fraction() + for section in sections: + duration = sum((generator.model.fraction(m['duration']) for m in source[section.source_measure_index:section.source_measure_index + section.measure_count]), fractions.Fraction()) + role = {'slash': 'solo', 'vocal': 'melody'}.get(section.role, section.role) + if section.role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and result: + attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(source[0]) if n['pitches']] + if attacks: + start = cursor - generator.model.fraction(source[0]['duration']) + min(attacks) + a, _, old = result[-1] + result[-1] = (a, start, old) + result.append((start, cursor, part + '_' + role)) + end = cursor + duration + start = cursor + if section.role == 'slash' and generator.score.body_start_measure_index(source) and result: + # Allocate the source's complete pickup bar before composition. This + # does not inspect either generated solo; a role may leave it silent. + start = cursor - generator.model.fraction(source[0]['duration']) + a, _, old = result[-1] + result[-1] = (a, start, old) + result.append((start, end, part + '_' + role)) + cursor = end + result.append((cursor, cursor + generator.arrange.ending_plan.duration(source), part + ('_accompaniment' if part != 'vocal' else '_rest'))) + return tuple(result) diff --git a/generator/arrange/generate.py b/generator/arrange/generate.py index 80e82ef..cf3490e 100644 --- a/generator/arrange/generate.py +++ b/generator/arrange/generate.py @@ -2,36 +2,31 @@ from __future__ import annotations import copy import fractions -import pathlib import generator.accompaniment -import generator.arrange.accompaniment import generator.acoustic import generator.acoustic_voicing import generator.arrange.ending import generator.arrange.form import generator.arrange.pickup +import generator.comping import generator.electric import generator.fretboard import generator.guitar import generator.melody import generator.model -import generator.musicxml import generator.parts import generator.score -import generator.solo.arrangement -import generator.comping import generator.vocabulary -import generator.production FINAL_SECTION_INDEX = 5 -def arrange_document(document: dict, part_type: str) -> dict: +def arrange_document(document: dict, part_type: str, *, composition=None) -> dict: """Canonical source-only template; solos are inserted by production roles.""" + composition = composition or generator.vocabulary.CompositionContext.create(generator.vocabulary.load(), part_type + '_accompaniment') document = generator.parts.prepare_document(document) - with generator.vocabulary.using(generator.vocabulary.load(), part_type + '_accompaniment'): - backing = generator.comping.Canonical(document, part_type, 'canonical') if part_type != 'vocal' else None - return _arrange_document(document, part_type, accompaniment=backing) + backing = generator.comping.Canonical(document, part_type, 'canonical', composition=composition) if part_type != 'vocal' else None + return _arrange_document(document, part_type, accompaniment=backing, composition=composition) def section_source(source: list[dict], section, next_section=None) -> list[dict]: result = copy.deepcopy(source) @@ -44,12 +39,14 @@ def section_source(source: list[dict], section, next_section=None) -> list[dict] generator.arrange.pickup.harmonies(destination, result[0]) return result -def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, accompaniment=None) -> dict: +def _arrange_document(document: dict, part_type: str, plan=None, *, owned=None, accompaniment=None, composition=None) -> dict: + composition = composition or generator.vocabulary.current() if part_type not in generator.parts.PART_TYPES: raise ValueError(f'unknown arrangement part: {part_type}') staff = generator.score.source_staff(document) source = staff['measures'] - sections = generator.arrange.form.full_arrangement_sections(source, part_type) + plan = plan or generator.arrange.form.FormPlan.read(document, part_type) + sections = plan.sections output, tuplets, octave_shifts, voicing_origins = [], [], [], [] absolute = fractions.Fraction() source_starts = [] @@ -62,16 +59,8 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, section_measures = section_source(source, section, sections[index + 1] if index + 1 < len(sections) else None) previous = generator.accompaniment.carried_chords(section_measures) segment = [] - stop = None + stop = plan.stops[index] next_section = sections[index + 1] if index + 1 < len(sections) else None - if next_section and generator.score.body_start_measure_index(source): - last = section.source_measure_index + section.measure_count - 1 - if next_section.role == 'slash': - stop = (last, generator.model.fraction(section_measures[last]['duration']) - generator.model.fraction(source[0]['duration'])) - elif next_section.role == 'melody' and generator.arrange.pickup.enters(next_section, source): - attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(source[0]) if n['pitches']] - if attacks: - stop = (last, generator.model.fraction(section_measures[last]['duration']) - generator.model.fraction(source[0]['duration']) + min(attacks)) for si in range(section.source_measure_index, section.source_measure_index + section.measure_count): m = section_measures[si] if role == 'accompaniment' and accompaniment is not None: @@ -111,7 +100,7 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, if sections and sections[-1].section_index == FINAL_SECTION_INDEX: generator.arrange.ending.append(source, output, part_type, sounding=part_type != 'vocal' and (owned is None or 'accompaniment' in owned), - center=generator.vocabulary.role(part_type + '_accompaniment').get('center', 55)) + center=composition.role(part_type + '_accompaniment').get('center', 55), composition=composition) 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['metadata'].setdefault('tempo', 124) @@ -123,15 +112,12 @@ def _arrange_document(document: dict, part_type: str, plans=None, *, owned=None, generator.acoustic.combine(result) elif part_type == 'electric' and (owned is None or 'melody' in owned): generator.melody.harmonize(result) - generator.guitar.place_guitar(result) + generator.guitar.place_guitar(result, composition=composition) if generator.accompaniment.connect_continuations(result): - generator.guitar.place_guitar(result) + generator.guitar.place_guitar(result, composition=composition) # Source labels identify the head while arranging; published labels identify # the complete performance, including its lead-in and repeated choruses. for number, measure in enumerate(output, 1): measure['number'] = str(number) generator.fretboard.validate_document(result) return result - -def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, composition_jobs=2, cache_dir=None, progress=False, timings=None) -> int: - return generator.production.generate_tree(source_dir, output_dir, jobs=composition_jobs, cache_dir=cache_dir, progress=progress, timings=timings) diff --git a/generator/audio_assets.py b/generator/audio_assets.py new file mode 100644 index 0000000..cac441f --- /dev/null +++ b/generator/audio_assets.py @@ -0,0 +1,68 @@ +"""Validate and copy supplied recordings without synthesis or transformation.""" +from __future__ import annotations + +import fractions +import hashlib +import json +import re +import shutil +import wave + +import generator.model +import generator.performance + +TEMPOS = tuple(range(60, 121, 10)) + +def describe_wav(path, root, *, song_id='', bpm=0): + if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(root.resolve()): + raise ValueError(f'{path}: expected a contained regular WAV') + try: + with wave.open(str(path), 'rb') as audio: + channels, rate, frames = audio.getnchannels(), audio.getframerate(), audio.getnframes() + if audio.getcomptype() != 'NONE' or audio.getsampwidth() != 2 or channels not in (1, 2) or not 8000 <= rate <= 192000 or frames <= 0: + raise ValueError('expected nonempty mono/stereo PCM16 WAV') + remaining = frames + while remaining: + count = min(remaining, 65536) + if len(audio.readframes(count)) != count * channels * 2: + raise ValueError('truncated WAV data') + remaining -= count + except (wave.Error, EOFError) as error: + raise ValueError(f'{path}: invalid WAV: {error}') from error + with path.open('rb') as source: + digest = hashlib.file_digest(source, 'sha256').hexdigest() + return dict(path=path.relative_to(root).as_posix(), sha256=digest, channels=channels, sample_rate=rate, frames=frames, song_id=song_id, bpm=bpm) + +def copy_audio(static, destination, song_ids): + entries = [] + inputs = [(static / 'sounds' / name, '', 0) for name in ('beat1.wav', 'beat2.wav')] + for path in sorted((static / 'backing').glob('*.wav')): + match = re.fullmatch(r'([a-z0-9]+(?:_[a-z0-9]+)*)_(60|70|80|90|100|110|120)\.wav', path.name) + if not match: + raise ValueError(f'{path}: unsupported backing filename or tempo') + if match[1] not in song_ids: + continue + inputs.append((path, match[1], int(match[2]))) + for source, song_id, bpm in inputs: + description = describe_wav(source, static, song_id=song_id, bpm=bpm) + target = destination / description['path'] + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + entries.append(description) + return entries + +def validate_recording_lengths(root, entries): + durations = {} + for path in sorted((root / 'songs').glob('*.json')): + document = json.loads(path.read_text()) + performance = generator.performance.timing(document) + song_id = performance['song_id'] + duration = generator.model.fraction(performance['duration']) + performance['count_in_beats'] + if song_id in durations and durations[song_id] != duration: + raise ValueError(f'{song_id}: parts disagree about performance duration') + durations[song_id] = duration + for entry in entries: + if entry['song_id']: + seconds = fractions.Fraction(entry['frames'], entry['sample_rate']) + if seconds < durations[entry['song_id']] * 60 / entry['bpm']: + raise ValueError(f"{entry['path']}: backing recording is shorter than count-in and score") diff --git a/generator/cache.py b/generator/cache.py new file mode 100644 index 0000000..3e0f5a2 --- /dev/null +++ b/generator/cache.py @@ -0,0 +1,82 @@ +"""Content identities and atomic storage shared by generator cache adapters.""" +from __future__ import annotations +import ast +import contextlib +import functools +import hashlib +import json +import os +import pathlib +import shutil +import tempfile + +ROOT = pathlib.Path(__file__).resolve().parent + +@functools.lru_cache(maxsize=256) +def imports(content): + modules = set() + for node in ast.walk(ast.parse(content)): + if isinstance(node, ast.Import): + modules.update(alias.name for alias in node.names if alias.name.startswith('generator.')) + elif isinstance(node, ast.ImportFrom) and node.module and node.module.startswith('generator'): + modules.add(node.module) + modules.update(node.module + '.' + alias.name for alias in node.names) + return tuple(sorted(modules)) + +def source_identity(entrypoints): + """Conservatively include the transitive imports of explicit stage roots. + + Read bytes on every call, so edits in a long-lived build process invalidate + both code content and newly introduced dependencies. Only parsing is cached. + """ + pending, sources = list(entrypoints), {} + while pending: + module = pending.pop() + relative = module.split('.')[1:] + path = ROOT.joinpath(*relative).with_suffix('.py') if relative else ROOT / '__init__.py' + if not path.is_file(): + path = ROOT.joinpath(*relative, '__init__.py') + if path in sources or not path.is_file(): + continue + content = path.read_bytes() + sources[path] = hashlib.sha256(content).hexdigest() + pending.extend(imports(content)) + pending.extend('.'.join(module.split('.')[:i]) for i in range(1, len(module.split('.')))) + return {path.relative_to(ROOT).as_posix(): sources[path] for path in sorted(sources)} + +def fingerprint(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest() + +def write_json(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile('w', dir=path.parent, delete=False) as output: + temporary = pathlib.Path(output.name) + json.dump(value, output, sort_keys=True) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + +def has_symlink(path): + return any(parent.is_symlink() for parent in (path, *path.parents)) + +@contextlib.contextmanager +def stage_entry(entry, prefix): + if has_symlink(entry.parent): + raise ValueError('cache directories must not be symlinks') + entry.parent.mkdir(parents=True, exist_ok=True) + if entry.is_symlink() or entry.is_file(): + entry.unlink(missing_ok=True) + entry.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix=prefix, dir=entry.parent) as temporary: + yield pathlib.Path(temporary) + +def publish_files(entry, staged, names): + """Publish complete files in order, with the artifact manifest last.""" + for name in names: + target = entry / name + if target.is_dir() and not target.is_symlink(): + shutil.rmtree(target) + (staged / name).replace(target) diff --git a/generator/catalog.py b/generator/catalog.py index 29990b6..96d4c33 100644 --- a/generator/catalog.py +++ b/generator/catalog.py @@ -10,7 +10,7 @@ import sys if __name__ == '__main__' and __package__ is None: sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -import generator.performance +import generator.audio_assets APPLICATION_ID = 0x56414D50 SONG_COLUMNS = """ @@ -58,12 +58,12 @@ def populate(connection: sqlite3.Connection, root: pathlib.Path, audio_assets=No for path in sorted(root.glob('sounds/*.wav')): if path.name not in ('beat1.wav', 'beat2.wav'): raise ValueError(f'{path}: unknown metronome sample') - audio_assets.append(generator.performance.describe_wav(path, root)) + audio_assets.append(generator.audio_assets.describe_wav(path, root)) for path in sorted(root.glob('backing/*.wav')): match = re.fullmatch(r'([a-z0-9]+(?:_[a-z0-9]+)*)_(60|70|80|90|100|110|120)\.wav', path.name) if not match: raise ValueError(f'{path}: invalid backing filename') - audio_assets.append(generator.performance.describe_wav(path, root, song_id=match[1], bpm=int(match[2]))) + audio_assets.append(generator.audio_assets.describe_wav(path, root, song_id=match[1], bpm=int(match[2]))) folder = root / 'songs' if (folder.exists() or folder.is_symlink()) and not folder.is_dir(): raise ValueError(f'{folder}: score asset directory not found') diff --git a/generator/chord_families.py b/generator/chord_families.py index 7368aaa..61ffc14 100644 --- a/generator/chord_families.py +++ b/generator/chord_families.py @@ -1,10 +1,11 @@ """Transposable guitar families, derived from the written chord and legal grips.""" -import generator.harmony import generator.fretboard +import generator.harmony import generator.model import generator.vocabulary -def acoustic(chord, grip): +def acoustic(chord, grip, *, composition=None): + composition = composition or generator.vocabulary.current() degrees = generator.harmony.degrees(chord) root = int(generator.model.NATURAL[chord['root']['letter']] + generator.model.alteration(chord['root']['alter'])) classes = {d: (root + generator.harmony.degree_semitones(d, a)) % 12 for d, a in degrees.items()} @@ -14,7 +15,7 @@ def acoustic(chord, grip): kind = 'extended' if terminal and color else 'tetrad' if terminal else 'triad' bass = next((d for d in (1, 5, 3) if classes.get(d) == grip[0].midi % 12), None) slots = {'root': 1, 'fifth': 5, 'guide': guide, 'terminal': terminal, 'color': color} - policy = generator.vocabulary.accompaniment('acoustic')['voicing_families'] + policy = composition.accompaniment('acoustic')['voicing_families'] orders = policy[kind].get(f'{grip[0].string_number}/{bass}', ()) upper = tuple(p.midi % 12 for p in grip[1:]) preferred = any(all(slots[s] in classes for s in order) and tuple(classes[slots[s]] for s in order) == upper for order in orders) @@ -37,10 +38,11 @@ def electric(grip): return 'drop-two' return 'close-position' if pitches and pitches[-1] - pitches[0] < 12 else 'compact-open' -def coverage(chord, options, part): +def coverage(chord, options, part, *, composition=None): + composition = composition or generator.vocabulary.current() if part == 'electric': - return {name: sum(electric(v) == name for v in options) for name in generator.vocabulary.accompaniment('electric')['voicing_families']['rank']} - families = [acoustic(chord, v) for v in options] + return {name: sum(electric(v) == name for v in options) for name in composition.accompaniment('electric')['voicing_families']['rank']} + families = [acoustic(chord, v, composition=composition) for v in options] result = [] for string in (5, 6): for degree in (1, 5): diff --git a/generator/comping.py b/generator/comping.py index 6da5eeb..ef271bd 100644 --- a/generator/comping.py +++ b/generator/comping.py @@ -2,26 +2,25 @@ from __future__ import annotations import copy import dataclasses +import fractions import functools import json -import fractions import typing import generator.accompaniment import generator.acoustic -import generator.comping_rhythm -import generator.chord_families import generator.acoustic_voicing import generator.arrange.form -import generator.arrange.pickup +import generator.chord_families +import generator.comping_rhythm import generator.fretboard import generator.guitar import generator.harmony import generator.model import generator.parts import generator.score -import generator.vocabulary import generator.solo.policy +import generator.vocabulary def semantic_key(chord): return json.dumps({k: chord[k] for k in ('root', 'kind', 'degrees', 'bass', 'inversion') if k in chord}, sort_keys=True) @@ -38,12 +37,15 @@ def requirements(chord, part): raise ValueError('acoustic accompaniment requires a written bass, root or fifth') return dataclasses.replace(result, basses=basses) +def voicings(key, part, *, composition=None): + return _voicings(key, part, composition or generator.vocabulary.current()) + @functools.lru_cache(maxsize=2048) -def voicings(key, part): +def _voicings(key, part, composition): chord = json.loads(key) required = requirements(chord, part) if part == 'electric': - options = generator.guitar.chord_positions(required, prune=False, profile='electric') + options = generator.guitar.chord_positions(required, prune=False, profile='electric', composition=composition) elif part == 'acoustic': degrees = generator.harmony.degrees(chord) plain = degrees in ({1: 0, 3: 0, 5: 0}, {1: 0, 3: -1, 5: 0}) @@ -77,19 +79,21 @@ class VoicingCost(typing.NamedTuple): def moved(self, distance): return self._replace(effort=self.effort + distance) -def voicing_preferences(part, grip, center): - return generator.guitar.voicing_preferences(grip, generator.guitar.voicing_preference(part + '_accompaniment', center)) +def voicing_preferences(part, grip, center, *, composition=None): + composition = composition or generator.vocabulary.current() + return generator.guitar.voicing_preferences(grip, generator.guitar.voicing_preference(part + '_accompaniment', center, composition=composition)) -def local_cost(part, key, grip, center): +def local_cost(part, key, grip, center, *, composition=None): + composition = composition or generator.vocabulary.current() required = requirements(json.loads(key), part) register = abs(fractions.Fraction(sum(p.midi for p in grip), len(grip)) - center) - preference = sum(voicing_preferences(part, grip, center)) + preference = sum(voicing_preferences(part, grip, center, composition=composition)) if part == 'electric': - value = generator.guitar.voicing_cost(grip, 'electric', required.allowed) + value = generator.guitar.voicing_cost(grip, 'electric', required.allowed, composition=composition) return VoicingCost(value.extended_grips, 3 * value.string_shortfall + value.family_penalty, value.effort + preference, value.missing_tones, register) # Familiar degree orders on either bass string, then balanced whole-form # movement, sounding register and hand position. Neither is an absolute ban. - family = generator.chord_families.acoustic(json.loads(key), grip) + family = generator.chord_families.acoustic(json.loads(key), grip, composition=composition) return VoicingCost(generator.fretboard.span_penalty(grip), int(not family['preferred']), preference, len(set(required.allowed) - {p.midi % 12 for p in grip}), register) def select_grips(layers, costs, part, seed, *, left=(), right=(), close=False): @@ -184,9 +188,10 @@ def split_measures(flat, inputs, part): return tuple(output) class Canonical: - def __init__(self, document, part, seed): + def __init__(self, document, part, seed, *, composition=None, plan=None): + self.composition = composition or generator.vocabulary.current() self.part, self.seed = part, seed - self.policy = generator.vocabulary.role(part + '_accompaniment') + self.policy = self.composition.role(part + '_accompaniment') self.cache, self._reports, self._rhythm_reports = {}, {}, {} self.measures = generator.score.source_staff(document)['measures'] # A held accompaniment chord may continue into the bar containing the @@ -194,7 +199,8 @@ class Canonical: self.voice_id = self.measures[0]['voices'][0]['id'] self.melody = generator.comping_rhythm.melody_contexts(document) self.first = generator.score.body_start_measure_index(self.measures) - sections = generator.arrange.form.full_arrangement_sections(self.measures, part) + plan = plan or generator.arrange.form.FormPlan.read(document, part) + sections = plan.sections self.closed = any(a.role == b.role == 'accompaniment' and a.source_measure_index + a.measure_count == len(self.measures) and b.source_measure_index == self.first for a, b in zip(sections, sections[1:])) carried = generator.accompaniment.carried_chords(self.measures) @@ -204,7 +210,7 @@ class Canonical: size = 1 if index + 1 < len(self.measures): lengths = [generator.model.fraction(m['duration']) for m in self.measures[index:index + 2]] - if self.measures[index]['time_signature'] == self.measures[index + 1]['time_signature'] and any(tuple(map(fractions.Fraction, p['lengths'])) == tuple(lengths) for p in generator.vocabulary.accompaniment(part)['patterns']): + if self.measures[index]['time_signature'] == self.measures[index + 1]['time_signature'] and any(tuple(map(fractions.Fraction, p['lengths'])) == tuple(lengths) for p in self.composition.accompaniment(part)['patterns']): size = 2 self.groups.append(tuple((self.measures[i], carried[i], i) for i in range(index, index + size))) index += size @@ -214,19 +220,9 @@ class Canonical: for section in sections: if section.role == 'accompaniment': self.pair_breaks.update((section.source_measure_index, section.source_measure_index + section.measure_count)) - for section, following in zip(sections, sections[1:]): - if section.role != 'accompaniment' or not self.first: - continue - last = section.source_measure_index + section.measure_count - 1 - pad = generator.arrange.pickup.offset(self.measures[last], self.measures[0]) - if following.role == 'slash': - self.pair_stops.append((last, pad)) - elif following.role == 'melody' and generator.arrange.pickup.enters(following, self.measures): - attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(self.measures[0]) if n['pitches']] - if attacks: - self.pair_stops.append((last, pad + min(attacks))) + self.pair_stops = [stop for section, stop in zip(sections, plan.stops) if section.role == 'accompaniment' and stop is not None] breaks = [(i, fractions.Fraction()) for i in self.pair_breaks] + self.pair_stops - choices, locations, self.groove_report = generator.comping_rhythm.choose_cycles(self.groups, part, self.melody, seed, breaks=breaks) + choices, locations, self.groove_report = generator.comping_rhythm.choose_cycles(self.groups, part, self.melody, seed, breaks=breaks, composition=self.composition) self.patterns = {g[0][2]: (p, a) for g, (p, a, _) in zip(self.groups, choices)} self.locations = {g[0][2]: location for g, location in zip(self.groups, locations)} selected = [r for _, _, r in choices] @@ -266,7 +262,7 @@ class Canonical: melody.extend((a + cursor, b + cursor) for a, b in self.melody[i]) lengths.append(generator.model.fraction(m['duration'])) cursor += lengths[-1] - return generator.comping_rhythm.candidates(flat, None, self.part, tuple(melody), self.seed + '/' + str(inputs[0][2]), lengths=tuple(lengths), limit=limit) + return generator.comping_rhythm.candidates(flat, None, self.part, tuple(melody), self.seed + '/' + str(inputs[0][2]), lengths=tuple(lengths), limit=limit, composition=self.composition) def acoustic_realizations(self, sources, bounds=None, stop=None): """Assign the canonical attack stream once, before splitting it into blocks. @@ -313,14 +309,14 @@ class Canonical: offset, index = next((a, i) for a, i in reversed(offsets) if a <= start) events = tuple(e for e in rhythm.events if start <= (e.harmonic_time if e.harmonic_time is not None else e.start) < end) try: - options = voicings(semantic_key(chord), self.part) + options = voicings(semantic_key(chord), self.part, composition=self.composition) except ValueError as error: raise ValueError(f'source measure {inputs[index - inputs[0][2]][0]["number"]}, beat {start - offset + 1}: {error}') from error spans.append(HarmonyChoice(index, start, end, chord, events, dict(rhythm.families), options)) return tuple(spans) def select(self, spans, *, left=(), right=(), close=False): layers = tuple(s.options for s in spans) - costs = tuple({v: local_cost(self.part, semantic_key(s.chord), v, self.policy.get('center', 55)) for v in layer} for s, layer in zip(spans, layers)) + costs = tuple({v: local_cost(self.part, semantic_key(s.chord), v, self.policy.get('center', 55), composition=self.composition) for v in layer} for s, layer in zip(spans, layers)) return select_grips(layers, costs, self.part, self.seed, left=left, right=right, close=close) def __call__(self, source, prior, index, *, sources=None, bounds=None, stop=None): group = next((g for g in self.groups if any(i == index for _, _, i in g)), ((source, prior, index),)) @@ -397,8 +393,8 @@ class Canonical: root = int(generator.model.NATURAL[span.chord['root']['letter']] + generator.model.alteration(span.chord['root']['alter'])) rows.append({'source_index': span.index, 'onset': str(span.start), 'chord': generator.harmony.symbol(span.chord), 'figure': span.figure, 'pattern_id': rhythm.pattern_id, 'tones': len(grip), 'bass': bool(bass), 'variant': variant, - 'family': (generator.chord_families.acoustic(span.chord, grip) if self.part == 'acoustic' else generator.chord_families.electric(grip)) if grip else None, - 'family_coverage': generator.chord_families.coverage(span.chord, span.options, self.part), + 'family': (generator.chord_families.acoustic(span.chord, grip, composition=self.composition) if self.part == 'acoustic' else generator.chord_families.electric(grip)) if grip else None, + 'family_coverage': generator.chord_families.coverage(span.chord, span.options, self.part, composition=self.composition), 'fret_span': generator.fretboard.fret_span(grip), 'span_group': ('fallback' if generator.fretboard.span_penalty(grip) else 'primary') if grip else None, 'span_candidates': generator.chord_families.span_coverage(span.options), @@ -407,9 +403,9 @@ class Canonical: 'bass_string': bass.string_number if bass else None, 'movement': movement(self.part, previous, grip), 'voicing_candidates': len(span.options)}) if grip: - register, position = voicing_preferences(self.part, grip, self.policy.get('center', 55)) + register, position = voicing_preferences(self.part, grip, self.policy.get('center', 55), composition=self.composition) travel = movement(self.part, previous, grip) - effort = generator.guitar.voicing_cost(grip, 'electric').effort if self.part == 'electric' else 0 + effort = generator.guitar.voicing_cost(grip, 'electric', composition=self.composition).effort if self.part == 'electric' else 0 rows[-1]['voicing_cost'] = {'movement': travel, 'grip_effort': str(effort), 'register': str(register), 'position': str(position), 'combined': str(travel + effort + register + position)} for event in span.events: lanes = ['bass', 'upper'] if event.lane == 'combined' else [event.lane] diff --git a/generator/comping_rhythm.py b/generator/comping_rhythm.py index dcf26c9..5ceb6dd 100644 --- a/generator/comping_rhythm.py +++ b/generator/comping_rhythm.py @@ -5,8 +5,8 @@ import itertools import generator.accompaniment import generator.model -import generator.solo.analysis import generator.solo.policy +import generator.source import generator.vocabulary def meter_context(measure): @@ -90,7 +90,7 @@ class Rhythm: return tuple((e.lane, str(e.start), str(e.end - e.start), str(e.harmonic_time)) for e in self.events) def melody_contexts(document): - melody = generator.solo.analysis.attacks(document) + melody = generator.source.attacks(document) result, cursor = [], fractions.Fraction() for m in document['staves'][0]['measures']: length = generator.model.fraction(m['duration']) @@ -247,9 +247,10 @@ def evaluate(events, part, melody, harmony, lengths): divisor = max(1, len(lengths)) return tuple((k, v / divisor) for k, v in (('activity', activity), ('melody_support', support), ('harmonic_support', coverage), ('articulation', articulation))) -def candidates(measure, prior, part, melody, seed, *, lengths=None, limit=None, grouping=()): +def candidates(measure, prior, part, melody, seed, *, lengths=None, limit=None, grouping=(), composition=None): + composition = composition or generator.vocabulary.current() lengths = lengths or (generator.model.fraction(measure['duration']),) - policy = generator.vocabulary.accompaniment(part) + policy = composition.accompaniment(part) result = [] for pattern in patterns_for(policy, measure, lengths, grouping): for anticipate in (False, True): @@ -371,13 +372,14 @@ def realize_acoustic_form(rhythms, context): def form_cost(rhythms, groups): return sum(r.cost * len(g) for r, g in zip(rhythms, groups)) -def choose_cycles(groups, part, melody, seed, *, breaks=()): +def choose_cycles(groups, part, melody, seed, *, breaks=(), composition=None): """Select a small recurring vocabulary against every occurrence in the form. No local shortlist precedes whole-form evaluation. A pattern/mode is retained as one decision even where a chord boundary changes its legal release. """ - policy = generator.vocabulary.accompaniment(part) + composition = composition or generator.vocabulary.current() + policy = composition.accompaniment(part) settings = policy['groove'] selected, locations, reports = [], [], [] runs = [] @@ -463,7 +465,7 @@ def choose_cycles(groups, part, melody, seed, *, breaks=()): selected.append((p, anticipate, cache[i, p['id'], anticipate])) locations.append({'cycle_start': run[0][0][2], 'cycle_slot': i % len(cycle), 'base_pattern': p['id'], 'fill': None}) context = acoustic_context(groups, melody, breaks) if part == 'acoustic' else None - add_fills(groups, selected, locations, part, melody, seed, context=context) + add_fills(groups, selected, locations, part, melody, seed, context=context, composition=composition) if context: realized = realize_acoustic_form([r for _, _, r in selected], context) selected = [(p, a, r) for (p, a, _), r in zip(selected, realized)] @@ -472,9 +474,10 @@ def choose_cycles(groups, part, melody, seed, *, breaks=()): report['attack_assignment'] = 'sequential_combined_except_eighth_pairs' return selected, locations, reports -def add_fills(groups, selected, locations, part, melody, seed, *, context=None): +def add_fills(groups, selected, locations, part, melody, seed, *, context=None, composition=None): """Fixed source-position answers; no iteration seed or generated lead input.""" - policy = generator.vocabulary.accompaniment(part) + composition = composition or generator.vocabulary.current() + policy = composition.accompaniment(part) spacing = policy['groove']['fill_spacing_measures'] if not groups: return @@ -519,7 +522,7 @@ def add_fills(groups, selected, locations, part, melody, seed, *, context=None): if any(e.start < middle < e.end for e in current.events): continue for variation in policy['patterns']: - if variation['family'] != base['family'] or variation['lengths'] != base['lengths'] or variation.get('meter') != base.get('meter') or variation.get('grouping') != base.get('grouping'): + if variation['family'] != base['family'] or tuple(variation['lengths']) != tuple(base['lengths']) or variation.get('meter') != base.get('meter') or tuple(variation.get('grouping', ())) != tuple(base.get('grouping', ())): continue hybrid = {**base, 'id': base['id'] + '.fill.' + variation['id'], 'events': [r for r in base['events'] if fractions.Fraction(r['onset']) < middle] + diff --git a/generator/compose.py b/generator/compose.py index 2363e57..ba6197b 100644 --- a/generator/compose.py +++ b/generator/compose.py @@ -1,24 +1,27 @@ """Explicit single-role build. Does not publish or consume an existing ensemble.""" import argparse import json -import time import pathlib +import time + +import generator.composition +import generator.composition_types import generator.musicxml import generator.parts -import generator.production +import generator.source def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--song', type=pathlib.Path, required=True) - parser.add_argument('--role', choices=generator.production.ROLES, required=True) + parser.add_argument('--role', choices=generator.composition_types.ROLES, required=True) parser.add_argument('--data-dir', type=pathlib.Path, default=pathlib.Path('data')) parser.add_argument('--output', type=pathlib.Path, required=True) parser.add_argument('--cache-dir', type=pathlib.Path) parser.add_argument('--settings', type=pathlib.Path) args = parser.parse_args() - source = generator.production.SourceAnalysis.read(generator.parts.prepare_document(generator.musicxml.load_musicxml(args.song))) + source = generator.source.SourceAnalysis.read(generator.parts.prepare_document(generator.musicxml.load_musicxml(args.song))) start = time.monotonic() - result = generator.production.generate_role(source, args.role, args.data_dir, json.loads(args.settings.read_text()) if args.settings else {}, args.cache_dir) + result = generator.composition.generate_role(source, args.role, args.data_dir, json.loads(args.settings.read_text()) if args.settings else {}, args.cache_dir) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, indent=2)) print(f'{args.role}: {time.monotonic() - start:.2f}s wall; cached={result["cache_hit"]}; role generation/cache lookup {result["seconds"]:.2f}s') diff --git a/generator/composition.py b/generator/composition.py new file mode 100644 index 0000000..b1a2fa0 --- /dev/null +++ b/generator/composition.py @@ -0,0 +1,158 @@ +"""Independent, reproducible role generation from source and policy inputs. + +Only source documents and production resources enter generate_role. Completed +roles and published assets are downstream values, never composition inputs. +""" +from __future__ import annotations +import copy +import dataclasses +import functools +import hashlib +import json +import pathlib +import time + +import generator.accompaniment +import generator.acoustic +import generator.arrange.assembly +import generator.arrange.form +import generator.arrange.generate +import generator.comping +import generator.comping_rhythm +import generator.composition_types +import generator.fretboard +import generator.input_audit +import generator.role_cache +import generator.score +import generator.solo.arrangement +import generator.solo.authored +import generator.solo.composer +import generator.solo.learned +import generator.solo.notation +import generator.solo.report +import generator.source +import generator.validation +import generator.vocabulary + +VERSION = 'independent-roles-1' + +def dependencies(role, resources, measures=None): + names = ['harmony', 'lead_ins'] if role.endswith('_solo') else [] + accompaniment = {} + if role.endswith('_accompaniment'): + policy = generator.vocabulary.accompaniment(role.split('_')[0], resources) + accompaniment = {'accompaniment': generator.comping_rhythm.relevant_policy(policy, measures) if measures is not None else policy} + return {'roles': {'role': resources['roles']['roles'][role], 'difficulty': resources['roles']['difficulty']}, + **({'voicing_families': generator.vocabulary.accompaniment('electric', resources)['voicing_families']} if role == 'electric_melody' else {}), + **accompaniment, + **{name: resources[name] for name in names}} + +def _resource_key(directory, role): + return {'model': hashlib.sha256((directory / 'models' / 'solo.json').read_bytes()).hexdigest()} if role.endswith('_solo') else {} + +@functools.lru_cache(maxsize=4) +def _solo_model(directory, fingerprint): + return generator.solo.learned.Model.load(pathlib.Path(directory) / 'models' / 'solo.json') + +def generate_role(source, role, data_dir, settings=None, cache_dir=None) -> generator.composition_types.RoleResult: + started = time.monotonic() + with generator.input_audit.composition(data_dir, role, cache_dir) as reads: + result = _generate_role(source, role, data_dir, settings, cache_dir) + # Observed reads are diagnostics, never cache identity or musical seeds. + result['trace'] = {**result['trace'], 'input_boundary': 'production data (including explicit authored repertoire), generator code, Python runtime, own role cache'} + result['observed_reads'] = sorted(reads) + result['seconds'] = time.monotonic() - started + return result + +def _generate_role(source, role, data_dir, settings=None, cache_dir=None): + if role not in generator.composition_types.ROLES: + raise ValueError('Unknown generation role: ' + role) + data_dir = pathlib.Path(data_dir) + resources = generator.vocabulary.load(data_dir / 'composition') + settings = settings or {} + registered = generator.solo.authored.lookup(source, role.removesuffix('_solo'), data_dir) if role.endswith('_solo') and not settings.get('predict', False) else None + algorithm = VERSION + '/' + generator.solo.learned.VERSION if role.endswith('_solo') else VERSION + inputs = {'version': algorithm, 'source': source.musical_key, 'role': role, 'settings': settings, + 'vocabulary': dependencies(role, resources, generator.score.source_staff(source.document())['measures']), 'catalogs': ({'authored': registered['sha256'], 'id': registered['id'], 'pitch_convention': registered['pitch_convention']} if registered else _resource_key(data_dir, role)), 'code': generator.role_cache.code_key()} + # Source notation/lyrics also invalidate cached score representation, never the musical seed. + key = generator.vocabulary.fingerprint({'inputs': inputs, 'notation': source.document_json}) + path = pathlib.Path(cache_dir) / role / (key + '.json') if cache_dir else None + cached = generator.role_cache.restore(path, key) + if cached is not None: + return cached + started = time.monotonic() + document = source.document() + part = role.split('_')[0] + plan = generator.arrange.form.FormPlan.read(document, part) + trace = {'source': source.musical_key, 'role': role, 'production_resources': inputs['catalogs'], + 'vocabulary': generator.vocabulary.fingerprint(inputs['vocabulary']), 'settings': settings, 'algorithm': algorithm, 'code': inputs['code'], 'cache_key': key} + choice_source = source.accompaniment_key() if role.endswith('_accompaniment') else source.musical_key + choice_data, choice_settings = inputs['vocabulary'], settings + if role.endswith('_accompaniment'): + # Voicing weights invalidate the concrete cache, but must not reshuffle + # rhythm patterns. Removing this new field preserves the pre-policy seed. + choice_data = copy.deepcopy(choice_data) + choice_data['roles']['role'].pop('voicing', None) + for field in ('provenance', 'license', 'research'): + choice_data['accompaniment'].pop(field, None) + choice_settings = {k: v for k, v in settings.items() if k != 'voicing'} + seed = generator.vocabulary.fingerprint({'source': choice_source, 'role': role, 'settings': choice_settings, 'data': choice_data, 'version': algorithm}) + details = {} + solo_report = '' + composition_timings = {} + composition = generator.vocabulary.CompositionContext.create(resources, role, settings | {'choice_seed': seed}) + if role.endswith('_solo'): + result = generator.arrange.generate._arrange_document(document, part, plan, owned={'slash'}, composition=composition) + si = 3 if part == 'acoustic' else 4 + first, count, offset, length = generator.solo.arrangement.regions(document)[si] + # The template/harmony comes directly from source form, not another generated part. + authored = generator.score.source_staff(document)['measures'] + section = plan.sections[si] + effective = generator.arrange.generate.section_source(authored, section, plan.sections[si + 1]) + template = copy.deepcopy(effective[section.source_measure_index:section.source_measure_index + count]) + initial = generator.accompaniment.carried_chords(effective)[section.source_measure_index] + start, end = next((a - offset, b - offset) for a, b, r in plan.windows if r == role) + notation_first = first + if start < 0: + # The preceding arrangement harmony comes from the source form. Do + # not invent harmony from a comparison score's unlabelled lead-in. + template.insert(0, copy.deepcopy(result['staves'][0]['measures'][first - 1])) + notation_first -= 1 + initial = generator.accompaniment.carried_chords(result['staves'][0]['measures'])[first - 1] + if registered: + solo = generator.solo.authored.realize(registered, generator.source.analyze(document), template, part, start, end, initial, composition=composition) + else: + solo_model = _solo_model(str(data_dir), inputs['catalogs']['model']) + solo = generator.solo.composer.compose(generator.source.analyze(document), template, part, end, initial=initial, learned_model=solo_model, expressions_enabled=composition.role().get('expressions', True), start=start, composition=composition) + composition_timings = {k: v for k, v in solo.diagnostics.items() if k.endswith('_seconds')} + relative = tuple(dataclasses.replace(n, start=n.start - start, end=n.end - start) for n in solo.notes()) + output = generator.solo.notation.measures(result['staves'][0]['measures'][notation_first:first + count], relative, 'solo_' + part) + generator.solo.notation.replace_region(result, notation_first, output) + details = generator.solo.report.structure(solo, composition=composition) + details['allocation'] = {'start': str(start), 'end': str(end), 'lead_in_measures': first - notation_first} + solo_report = generator.solo.report.describe(solo, first, offset, detail=details, composition=composition) + elif role.endswith('_accompaniment'): + canonical = generator.comping.Canonical(document, part, seed, composition=composition, plan=plan) + result = generator.arrange.generate._arrange_document(document, part, plan, owned={'accompaniment'}, accompaniment=canonical, composition=composition) + details = {'canonical_comping': canonical.report, 'accompaniment_rhythm': canonical.rhythm_report, 'groove_cycles': canonical.groove_report, 'rhythm_source': choice_source} + else: + result = generator.arrange.generate._arrange_document(document, part, plan, owned={'vocal' if part == 'vocal' else 'melody'}, composition=composition) + allowed = tuple((a, b) for a, b, r in plan.windows if r == role) + generator.arrange.assembly.mask(result, allowed) + if role == "acoustic_accompaniment": + generator.acoustic.validate_comping(result) + staff = result['staves'][0] + for measure in staff['measures']: + measure['id'] = role + '_' + measure['id'] + staff['tuplets'] = generator.score.reidentify(staff['measures'], staff.get('tuplets', [])) + if part != 'vocal': + result['staves'][0]['tablature'] = True + generator.validation.validate_document(result) + generator.fretboard.validate_document(result) + value = {'role': role, 'document': result, 'trace': trace, 'details': details, 'report': solo_report, 'seconds': time.monotonic() - started, 'composition_timings': composition_timings, 'cache_hit': False} + # Sidecar diagnostics use the same rational-safe representation as reports. + # Identical insertion order after cold generation and cache loading also + # keeps serialized scores and the catalog byte-for-byte reproducible. + value = json.loads(json.dumps(value, default=str, sort_keys=True)) + generator.role_cache.store(path, key, value) + return value diff --git a/generator/composition_types.py b/generator/composition_types.py new file mode 100644 index 0000000..10fea3c --- /dev/null +++ b/generator/composition_types.py @@ -0,0 +1,28 @@ +"""Worker requests and role results; score serialization stays unchanged.""" +from __future__ import annotations +import dataclasses +import pathlib +import typing + +import generator.source + +ROLES = ('acoustic_accompaniment', 'electric_accompaniment', 'electric_melody', 'vocal_melody', 'acoustic_solo', 'electric_solo') + +@dataclasses.dataclass(frozen=True) +class RoleRequest: + source: generator.source.SourceAnalysis + role: str + directory: pathlib.Path + settings_json: str = '{}' + cache: pathlib.Path | None = None + +class RoleResult(typing.TypedDict): + role: str + document: dict + trace: dict + details: dict + report: str + seconds: float + composition_timings: dict + cache_hit: bool + observed_reads: typing.NotRequired[list[str]] diff --git a/generator/engraving/cache.py b/generator/engraving/cache.py index 8d64dd0..436e291 100644 --- a/generator/engraving/cache.py +++ b/generator/engraving/cache.py @@ -9,12 +9,12 @@ import pathlib import platform import re import shutil -import tempfile import PIL.Image import PIL.ImageChops import PIL.features +import generator.cache import generator.engraving.validation import generator.theme @@ -22,9 +22,10 @@ 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 / 'performance.py', root / 'harmony.py', root / 'theme.py', root / 'palette.json'] + sources = generator.cache.source_identity(('generator.engraving.pipeline', 'generator.printing')) + sources['palette.json'] = hashlib.sha256((root / 'palette.json').read_bytes()).hexdigest() 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)}} + 'sources': sources} def renderer_fingerprint(fonts, style) -> str: return hashlib.sha256(json.dumps(renderer_identity(fonts, style), sort_keys=True).encode()).hexdigest() @@ -46,12 +47,9 @@ def band_fingerprint(notation: str, page, band) -> str: context = [notation, page.width, page.height, page.supersampling, dataclasses.asdict(band)] return hashlib.sha256(json.dumps(context, sort_keys=True, ensure_ascii=False).encode()).hexdigest() -def has_symlink(path: pathlib.Path) -> bool: - return any(parent.is_symlink() for parent in (path, *path.parents)) - def restore_pages(entry: pathlib.Path, destination: pathlib.Path, validated_pages: set[str] | None = None) -> list[dict] | None: try: - if has_symlink(entry) or has_symlink(destination): + if generator.cache.has_symlink(entry) or generator.cache.has_symlink(destination): return None manifest = entry / 'pages.json' if manifest.is_symlink(): @@ -76,28 +74,17 @@ def restore_pages(entry: pathlib.Path, destination: pathlib.Path, validated_page return None def store_pages(entry: pathlib.Path, destination: pathlib.Path, pages: list[dict]) -> None: - if has_symlink(entry.parent): - raise ValueError('engraving cache directories must not be symlinks') - entry.parent.mkdir(parents=True, exist_ok=True) - 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: - staged = pathlib.Path(temporary) + with generator.cache.stage_entry(entry, '.engraving_') as staged: for page in pages: shutil.copyfile(destination / page['path'], staged / page['path']) (staged / 'pages.json').write_text(json.dumps(pages), encoding='utf-8') # Identical keys have identical pages; publish complete files, then the manifest. - for name in list(dict.fromkeys(page['path'] for page in pages)) + ['pages.json']: - target = entry / name - if target.is_dir() and not target.is_symlink(): - shutil.rmtree(target, ignore_errors=True) - (staged / name).replace(target) + generator.cache.publish_files(entry, staged, list(dict.fromkeys(page['path'] for page in pages)) + ['pages.json']) def restore_band(entry: pathlib.Path, key: str, width: int, height: int) -> PIL.Image.Image | None: try: manifest, asset = entry / 'band.json', entry / 'band.png' - if has_symlink(entry) or manifest.is_symlink() or asset.is_symlink(): + if generator.cache.has_symlink(entry) or manifest.is_symlink() or asset.is_symlink(): return None metadata = json.loads(manifest.read_text(encoding='utf-8')) if not isinstance(metadata, dict) or set(metadata) != {'context', 'width', 'height', 'sha256'}: @@ -122,19 +109,8 @@ def restore_band(entry: pathlib.Path, key: str, width: int, height: int) -> PIL. return None def store_band(entry: pathlib.Path, key: str, image: PIL.Image.Image) -> None: - if has_symlink(entry.parent): - raise ValueError('engraving cache directories must not be symlinks') - entry.parent.mkdir(parents=True, exist_ok=True) - 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: - staged = pathlib.Path(temporary) + with generator.cache.stage_entry(entry, '.engraving_') as staged: 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()} (staged / 'band.json').write_text(json.dumps(metadata), encoding='utf-8') - for name in ('band.png', 'band.json'): - target = entry / name - if target.is_dir() and not target.is_symlink(): - shutil.rmtree(target) - (staged / name).replace(target) + generator.cache.publish_files(entry, staged, ('band.png', 'band.json')) diff --git a/generator/engraving/layout.py b/generator/engraving/layout.py index c21271c..0983331 100644 --- a/generator/engraving/layout.py +++ b/generator/engraving/layout.py @@ -16,7 +16,7 @@ import generator.engraving.spans import generator.engraving.tablature import generator.engraving.types import generator.harmony -import generator.model +import generator.validation def lyric_text(verse: dict) -> str: return ''.join(s['text'] for s in verse['syllables'] if s['kind'] in ('text', 'elision')) @@ -250,7 +250,7 @@ def paginate(systems, document, fonts, style, *, index=None): def layout_systems(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts, *, index=None) -> tuple[generator.engraving.types.System, ...]: if index is None: - generator.model.validate_document(document) + generator.validation.validate_document(document) index = generator.engraving.notation.ScoreIndex(document) if not index.measures: raise generator.engraving.types.EngravingError('score has no measures') @@ -276,7 +276,7 @@ def layout_systems(document: dict, style: generator.engraving.types.Style, fonts return tuple(systems) def layout_score(document: dict, style: generator.engraving.types.Style, fonts: generator.engraving.fonts.Fonts) -> generator.engraving.types.LayoutDocument: - generator.model.validate_document(document) + generator.validation.validate_document(document) style = generator.engraving.types.score_style(document, style) index = generator.engraving.notation.ScoreIndex(document) systems = layout_systems(document, style, fonts, index=index) diff --git a/generator/engraving/pipeline.py b/generator/engraving/pipeline.py index 6b01b00..5c8845c 100644 --- a/generator/engraving/pipeline.py +++ b/generator/engraving/pipeline.py @@ -1,195 +1,17 @@ from __future__ import annotations -import collections import concurrent.futures -import dataclasses -import hashlib import json import multiprocessing import pathlib -import shutil -import tempfile import time -import PIL.Image - -import generator.engraving.audit import generator.engraving.cache import generator.engraving.fonts -import generator.engraving.layout -import generator.engraving.notation -import generator.engraving.raster import generator.engraving.types import generator.engraving.validation -import generator.model -import generator.performance - -@dataclasses.dataclass -class GroupResult: - scores: list = dataclasses.field(default_factory=list) - fullscore: int = 0 - bodyhits: int = 0 - bodies: int = 0 - renderedpages: int = 0 - rejected: int = 0 - timings: dict = dataclasses.field(default_factory=lambda: dict.fromkeys(('layout', 'pagination', 'raster', 'cache'), 0.0)) - -class BodyBands: - def __init__(self, root, persistent, result, limit=16 * 1024 * 1024): - self.root, self.persistent, self.result, self.limit = root, persistent, result, limit - self.images = collections.OrderedDict() - self.size = 0 - self.known = {} - self.root.mkdir(parents=True, exist_ok=True) - def get(self, notation, page, band, fonts): - key = generator.engraving.cache.band_fingerprint(notation, page, band) - if key in self.images: - self.images.move_to_end(key) - self.result.bodyhits += 1 - return self.images[key] - started = time.monotonic() - entry = self.root / (key + '.rgb') - image = None - if key in self.known: - if entry.is_symlink() or entry.stat().st_size != page.width * (band.bottom - band.top) * 3: - raise generator.engraving.types.EngravingError('temporary notation band was corrupted') - data = entry.read_bytes() - if hashlib.sha256(data).hexdigest() != self.known[key]: - raise generator.engraving.types.EngravingError('temporary notation band was corrupted') - image = PIL.Image.frombytes('RGB', (page.width, band.bottom - band.top), data) - elif self.persistent: - cached = self.persistent / 'notation' / key - image = generator.engraving.cache.restore_band(cached, key, page.width, band.bottom - band.top) - self.result.rejected += int(image is None and (cached.exists() or cached.is_symlink())) - self.result.timings['cache'] += time.monotonic() - started - if image is None: - started = time.monotonic() - image = generator.engraving.raster.rasterize_band(page, band.primitives, band.top, band.bottom, fonts) - self.result.timings['raster'] += time.monotonic() - started - self.result.bodies += 1 - if self.persistent: - started = time.monotonic() - generator.engraving.cache.store_band(self.persistent / 'notation' / key, key, image) - self.result.timings['cache'] += time.monotonic() - started - else: - self.result.bodyhits += 1 - if key not in self.known: - started = time.monotonic() - # Spill finalized pixels, so eviction never repeats rasterization or PNG decoding. - data = image.tobytes() - entry.write_bytes(data) - self.result.timings['cache'] += time.monotonic() - started - self.known[key] = hashlib.sha256(data).hexdigest() - size = image.width * image.height * 3 - while self.images and self.size + size > self.limit: - _, old = self.images.popitem(last=False) - self.size -= old.width * old.height * 3 - old.close() - self.images[key] = image - self.size += size - return image - def close(self): - for image in self.images.values(): - image.close() - self.images.clear() - self.size = 0 - -def clear_pages(destination): - if destination.is_symlink() or destination.is_file(): - destination.unlink() - elif destination.exists(): - shutil.rmtree(destination) - -def write_bundle(path, document, pages, fingerprint): - entries = [{k: v for k, v in {**page, 'path': path.with_suffix('.pages').name + '/' + page['path']}.items() if k != 'playback'} for page in pages] - normal = [entry for entry, page in zip(entries, pages) if not page['playback']] - playback = [entry for entry, page in zip(entries, pages) if page['playback']] - music = document - bundle = {'music': music, 'presentation': {'engine': generator.engraving.validation.ENGINE_NAME, 'style_sha256': fingerprint, - 'width': generator.engraving.validation.PAGE_WIDTH, 'height': generator.engraving.validation.PAGE_HEIGHT, 'pages': normal, 'playback_pages': playback}} - validate_cached_views(document, pages) - bundle['performance'] = generator.performance.timing(document) - path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') - -def validate_cached_views(document, pages): - generator.performance.validate_views(document, [p for p in pages if not p['playback']], [p for p in pages if p['playback']]) - -def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, progress=False): - family = generator.engraving.fonts.Fonts(font_path) - style = generator.engraving.types.Style() - systems = None - index = None - scores = {} - audited = set() - result = GroupResult() - with tempfile.TemporaryDirectory(prefix='vamp_notation_') as temporary: - bodies = BodyBands(pathlib.Path(temporary), cache_dir, result) - try: - for path, score_key in paths: - try: - document = json.loads(path.read_text(encoding='utf-8')) - style = generator.engraving.types.score_style(document, generator.engraving.types.Style()) - destination = path.with_suffix('.pages') - clear_pages(destination) - destination.mkdir() - if score_key in scores: - source, pages = scores[score_key] - for page in pages: - shutil.copyfile(source / page['path'], destination / page['path']) - result.fullscore += 1 - else: - if systems is None: - started = time.monotonic() - generator.model.validate_document(document) - index = generator.engraving.notation.ScoreIndex(document) - systems = generator.engraving.layout.layout_systems(document, style, family, index=index) - result.timings['layout'] += time.monotonic() - started - started = time.monotonic() - pages_layout, playback_layout = generator.engraving.layout.page_views(systems, document, family, style, index=index) - placement = tuple((page.width, page.height, tuple((system.start, system.end, system.box) for system in page.systems)) for page in pages_layout) - if placement not in audited: - layout = generator.engraving.types.LayoutDocument(pages_layout, style, fingerprint) - generator.engraving.audit.audit_layout(document, layout, family) - generator.engraving.audit.audit_playback(pages_layout, playback_layout, style, family) - audited.add(placement) - else: - for page in pages_layout + playback_layout: - 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 = [] - rendered = {} - for is_playback, page in [(False, p) for p in pages_layout] + [(True, p) for p in playback_layout]: - if page.primitives in rendered: - name, digest = rendered[page.primitives] - pages.append(dict(path=name, sha256=digest, playback=is_playback, **generator.performance.page_timing(document, page))) - continue - bands = generator.engraving.raster.page_bands(page) - if bands is None: - started = time.monotonic() - image = generator.engraving.raster.rasterize_page(page, family) - else: - body = bodies.get(notation, page, bands[1], family) - started = time.monotonic() - image = generator.engraving.raster.compose_page(page, family, bands, body) - target = destination / (('playback-' if is_playback else '') + f'{page.number:04}.png') - with image: - image.save(target, format='PNG') - result.timings['raster'] += time.monotonic() - started - result.renderedpages += 1 - digest = hashlib.sha256(target.read_bytes()).hexdigest() - rendered[page.primitives] = target.name, digest - pages.append(dict(path=target.name, sha256=digest, playback=is_playback, **generator.performance.page_timing(document, page))) - scores[score_key] = (destination, pages) - write_bundle(path, document, pages, fingerprint) - result.scores.append((path, score_key, pages)) - if progress and len(result.scores) % 50 == 0: - print(f'Engraving body {notation[:12]}: {len(result.scores)}/{len(paths)} scores, fullscore {result.fullscore}, bodyhits {result.bodyhits}, bodies {result.bodies}, renderedpages {result.renderedpages}', flush=True) - except ValueError as error: - raise generator.engraving.types.EngravingError(f'{path.relative_to(root)}: {error}') from error - finally: - bodies.close() - return result +import generator.engraving.worker +import generator.validation def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib.Path | None = None, jobs: int = 1, progress: bool = False) -> set[str]: if type(jobs) is not int or not 1 <= jobs <= 16: @@ -211,22 +33,22 @@ def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib. print(f'Engraving cache enabled: {cache_dir} ({state}; renderer {renderer})', flush=True) elif progress: print('Engraving cache disabled', flush=True) - totals = GroupResult() + totals = generator.engraving.worker.GroupResult() missing, rejected, completed = 0, 0, 0 groups = {} for path in paths: try: document = json.loads(path.read_text(encoding='utf-8')) - generator.model.validate_document(document) + generator.validation.validate_document(document) score_key = generator.engraving.cache.score_fingerprint(document, renderer) destination = path.with_suffix('.pages') - clear_pages(destination) + generator.engraving.worker.clear_pages(destination) entry = cache_dir / 'fullscore' / score_key if cache_dir else None present = entry is not None and (entry.exists() or entry.is_symlink()) pages = generator.engraving.cache.restore_pages(entry, destination, validated_pages) if entry else None if pages is not None: try: - validate_cached_views(document, pages) + generator.engraving.worker.validate_cached_views(document, pages) except (ValueError, TypeError, KeyError): pages = None if pages is None: @@ -235,7 +57,7 @@ def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib. notation = generator.engraving.cache.notation_fingerprint(document, renderer) groups.setdefault(notation, []).append((path, score_key)) else: - write_bundle(path, document, pages, fingerprint) + generator.engraving.worker.write_bundle(path, document, pages, fingerprint) totals.fullscore += 1 completed += 1 except ValueError as error: @@ -272,10 +94,10 @@ def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib. jobs = min(jobs, len(groups)) if jobs <= 1: for notation, group in groups.items(): - finish(engrave_group(notation, group, root, fonts, cache_dir, fingerprint, progress)) + finish(generator.engraving.worker.engrave_group(notation, group, root, fonts, cache_dir, fingerprint, progress)) else: with concurrent.futures.ProcessPoolExecutor(max_workers=jobs, mp_context=multiprocessing.get_context('spawn')) as pool: - futures = [pool.submit(engrave_group, notation, group, root, fonts, cache_dir, fingerprint, progress) for notation, group in groups.items()] + futures = [pool.submit(generator.engraving.worker.engrave_group, notation, group, root, fonts, cache_dir, fingerprint, progress) for notation, group in groups.items()] for future in concurrent.futures.as_completed(futures): finish(future.result()) if progress: diff --git a/generator/engraving/worker.py b/generator/engraving/worker.py new file mode 100644 index 0000000..4128e30 --- /dev/null +++ b/generator/engraving/worker.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +import collections +import dataclasses +import hashlib +import json +import pathlib +import shutil +import tempfile +import time + +import PIL.Image + +import generator.engraving.audit +import generator.engraving.cache +import generator.engraving.fonts +import generator.engraving.layout +import generator.engraving.notation +import generator.engraving.raster +import generator.engraving.types +import generator.engraving.validation +import generator.performance +import generator.validation + +@dataclasses.dataclass +class GroupResult: + scores: list = dataclasses.field(default_factory=list) + fullscore: int = 0 + bodyhits: int = 0 + bodies: int = 0 + renderedpages: int = 0 + rejected: int = 0 + timings: dict = dataclasses.field(default_factory=lambda: dict.fromkeys(('layout', 'pagination', 'raster', 'cache'), 0.0)) + +class BodyBands: + def __init__(self, root, persistent, result, limit=16 * 1024 * 1024): + self.root, self.persistent, self.result, self.limit = root, persistent, result, limit + self.images = collections.OrderedDict() + self.size = 0 + self.known = {} + self.root.mkdir(parents=True, exist_ok=True) + def get(self, notation, page, band, fonts): + key = generator.engraving.cache.band_fingerprint(notation, page, band) + if key in self.images: + self.images.move_to_end(key) + self.result.bodyhits += 1 + return self.images[key] + started = time.monotonic() + entry = self.root / (key + '.rgb') + image = None + if key in self.known: + if entry.is_symlink() or entry.stat().st_size != page.width * (band.bottom - band.top) * 3: + raise generator.engraving.types.EngravingError('temporary notation band was corrupted') + data = entry.read_bytes() + if hashlib.sha256(data).hexdigest() != self.known[key]: + raise generator.engraving.types.EngravingError('temporary notation band was corrupted') + image = PIL.Image.frombytes('RGB', (page.width, band.bottom - band.top), data) + elif self.persistent: + cached = self.persistent / 'notation' / key + image = generator.engraving.cache.restore_band(cached, key, page.width, band.bottom - band.top) + self.result.rejected += int(image is None and (cached.exists() or cached.is_symlink())) + self.result.timings['cache'] += time.monotonic() - started + if image is None: + started = time.monotonic() + image = generator.engraving.raster.rasterize_band(page, band.primitives, band.top, band.bottom, fonts) + self.result.timings['raster'] += time.monotonic() - started + self.result.bodies += 1 + if self.persistent: + started = time.monotonic() + generator.engraving.cache.store_band(self.persistent / 'notation' / key, key, image) + self.result.timings['cache'] += time.monotonic() - started + else: + self.result.bodyhits += 1 + if key not in self.known: + started = time.monotonic() + # Spill finalized pixels, so eviction never repeats rasterization or PNG decoding. + data = image.tobytes() + entry.write_bytes(data) + self.result.timings['cache'] += time.monotonic() - started + self.known[key] = hashlib.sha256(data).hexdigest() + size = image.width * image.height * 3 + while self.images and self.size + size > self.limit: + _, old = self.images.popitem(last=False) + self.size -= old.width * old.height * 3 + old.close() + self.images[key] = image + self.size += size + return image + def close(self): + for image in self.images.values(): + image.close() + self.images.clear() + self.size = 0 + +def clear_pages(destination): + if destination.is_symlink() or destination.is_file(): + destination.unlink() + elif destination.exists(): + shutil.rmtree(destination) + +def write_bundle(path, document, pages, fingerprint): + entries = [{k: v for k, v in {**page, 'path': path.with_suffix('.pages').name + '/' + page['path']}.items() if k != 'playback'} for page in pages] + normal = [entry for entry, page in zip(entries, pages) if not page['playback']] + playback = [entry for entry, page in zip(entries, pages) if page['playback']] + music = document + bundle = {'music': music, 'presentation': {'engine': generator.engraving.validation.ENGINE_NAME, 'style_sha256': fingerprint, + 'width': generator.engraving.validation.PAGE_WIDTH, 'height': generator.engraving.validation.PAGE_HEIGHT, 'pages': normal, 'playback_pages': playback}} + validate_cached_views(document, pages) + bundle['performance'] = generator.performance.timing(document) + path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') + +def validate_cached_views(document, pages): + generator.performance.validate_views(document, [p for p in pages if not p['playback']], [p for p in pages if p['playback']]) + +def engrave_group(notation, paths, root, font_path, cache_dir, fingerprint, progress=False): + family = generator.engraving.fonts.Fonts(font_path) + style = generator.engraving.types.Style() + systems = None + index = None + scores = {} + audited = set() + result = GroupResult() + with tempfile.TemporaryDirectory(prefix='vamp_notation_') as temporary: + bodies = BodyBands(pathlib.Path(temporary), cache_dir, result) + try: + for path, score_key in paths: + try: + document = json.loads(path.read_text(encoding='utf-8')) + style = generator.engraving.types.score_style(document, generator.engraving.types.Style()) + destination = path.with_suffix('.pages') + clear_pages(destination) + destination.mkdir() + if score_key in scores: + source, pages = scores[score_key] + for page in pages: + shutil.copyfile(source / page['path'], destination / page['path']) + result.fullscore += 1 + else: + if systems is None: + started = time.monotonic() + generator.validation.validate_document(document) + index = generator.engraving.notation.ScoreIndex(document) + systems = generator.engraving.layout.layout_systems(document, style, family, index=index) + result.timings['layout'] += time.monotonic() - started + started = time.monotonic() + pages_layout, playback_layout = generator.engraving.layout.page_views(systems, document, family, style, index=index) + placement = tuple((page.width, page.height, tuple((system.start, system.end, system.box) for system in page.systems)) for page in pages_layout) + if placement not in audited: + layout = generator.engraving.types.LayoutDocument(pages_layout, style, fingerprint) + generator.engraving.audit.audit_layout(document, layout, family) + generator.engraving.audit.audit_playback(pages_layout, playback_layout, style, family) + audited.add(placement) + else: + for page in pages_layout + playback_layout: + 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 = [] + rendered = {} + for is_playback, page in [(False, p) for p in pages_layout] + [(True, p) for p in playback_layout]: + if page.primitives in rendered: + name, digest = rendered[page.primitives] + pages.append(dict(path=name, sha256=digest, playback=is_playback, **generator.performance.page_timing(document, page))) + continue + bands = generator.engraving.raster.page_bands(page) + if bands is None: + started = time.monotonic() + image = generator.engraving.raster.rasterize_page(page, family) + else: + body = bodies.get(notation, page, bands[1], family) + started = time.monotonic() + image = generator.engraving.raster.compose_page(page, family, bands, body) + target = destination / (('playback-' if is_playback else '') + f'{page.number:04}.png') + with image: + image.save(target, format='PNG') + result.timings['raster'] += time.monotonic() - started + result.renderedpages += 1 + digest = hashlib.sha256(target.read_bytes()).hexdigest() + rendered[page.primitives] = target.name, digest + pages.append(dict(path=target.name, sha256=digest, playback=is_playback, **generator.performance.page_timing(document, page))) + scores[score_key] = (destination, pages) + write_bundle(path, document, pages, fingerprint) + result.scores.append((path, score_key, pages)) + if progress and len(result.scores) % 50 == 0: + print(f'Engraving body {notation[:12]}: {len(result.scores)}/{len(paths)} scores, fullscore {result.fullscore}, bodyhits {result.bodyhits}, bodies {result.bodies}, renderedpages {result.renderedpages}', flush=True) + except ValueError as error: + raise generator.engraving.types.EngravingError(f'{path.relative_to(root)}: {error}') from error + finally: + bodies.close() + return result diff --git a/generator/guitar.py b/generator/guitar.py index 727da3f..59d985e 100644 --- a/generator/guitar.py +++ b/generator/guitar.py @@ -7,10 +7,10 @@ import itertools import logging import typing +import generator.chord_families import generator.fretboard import generator.model import generator.pitch -import generator.chord_families import generator.vocabulary PREFERRED_MAX_FRET = 12 @@ -31,9 +31,10 @@ class VoicingCost(typing.NamedTuple): def moved(self, distance: int) -> VoicingCost: return self._replace(effort=self.effort + distance) -def voicing_preference(role, center=None): +def voicing_preference(role, center=None, *, composition=None): """Immutable role settings for both selection and scored-retrieval caches.""" - settings = generator.vocabulary.role(role) + composition = composition or generator.vocabulary.current() + settings = composition.role(role) policy = settings.get('voicing', {}) center = settings.get('center', 53 if role.startswith('acoustic') else 60) if center is None else center result = tuple(fractions.Fraction(str(v)) for v in (center, policy.get('register_weight', 1), policy.get('high_position_weight', 1), policy.get('comfortable_max_fret', 9))) @@ -50,9 +51,10 @@ def voicing_preferences(positions, preference): position = max(fractions.Fraction(), fractions.Fraction(sum(p.fret for p in positions), len(positions)) - threshold) return register_weight * register, position_weight * position -def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = ()) -> VoicingCost: - families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else () - preference = voicing_preference('electric_melody') if profile == 'electric' and melody else () +def voicing_cost(positions: tuple[generator.fretboard.TabPosition, ...], profile: str = '', allowed: tuple[int, ...] = (), melody: tuple[int, ...] = (), *, composition=None) -> VoicingCost: + composition = composition or generator.vocabulary.current() + families = tuple(composition.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else () + preference = voicing_preference('electric_melody', composition=composition) if profile == 'electric' and melody else () return _voicing_cost(positions, profile, allowed, melody, families, preference) @functools.lru_cache(maxsize=8192) @@ -69,9 +71,10 @@ def _voicing_cost(positions, profile, allowed, melody, families, preference): melody_string = min((p.string_number - 1 for p in positions if melody and p.midi == max(melody)), default=0) return VoicingCost(extended_grips=generator.fretboard.span_penalty(positions), melody_string=melody_string, string_shortfall=max(0, 4 - len(positions)) if profile == 'electric' else 0, covered_melody=int(bool(above)), covering_tones=above, family_penalty=family_penalty, effort=effort, missing_tones=len(set(allowed) - classes)) -def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = ()) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]: - families = tuple(generator.vocabulary.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else () - preference = voicing_preference('electric_melody') if profile == 'electric' and melody else () +def chord_positions(requirements: generator.fretboard.ChordRequirements, prune: bool = True, profile: str = '', melody: tuple[int, ...] = (), *, composition=None) -> tuple[tuple[generator.fretboard.TabPosition, ...], ...]: + composition = composition or generator.vocabulary.current() + families = tuple(composition.accompaniment('electric')['voicing_families']['rank'].items()) if profile == 'electric' else () + preference = voicing_preference('electric_melody', composition=composition) if profile == 'electric' and melody else () return _chord_positions(requirements, prune, profile, melody, families, preference) @functools.lru_cache(maxsize=256) @@ -211,7 +214,8 @@ def continued_sources(notes): def has_attack(notes: list[dict]) -> bool: return any('_harmony' in n and '_continue_from' not in n or any('tie_from' not in p for p in n['pitches']) for n in notes) -def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: dict, held: dict, preserve_strings: bool, fallback: bool = False): +def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: dict, held: dict, preserve_strings: bool, fallback: bool = False, *, composition=None): + composition = composition or generator.vocabulary.current() pending = [] chords = [n for n in notes if '_harmony' in n] chord = chords[0] if chords else None @@ -229,7 +233,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d requirements = generator.fretboard.chord_requirements(chord['_harmony'], respect_bass=profile != 'electric') if chord else generator.fretboard.ChordRequirements() melody = tuple(sorted({p.midi for _, options in pending for p in options})) if profile == 'electric' else () fresh = has_attack(notes) - candidates = chord_positions(requirements, prune=not (preserve_strings or held or pending or profile), profile=profile, melody=melody) if chord else [()] + candidates = chord_positions(requirements, prune=not (preserve_strings or held or pending or profile), profile=profile, melody=melody, composition=composition) if chord else [()] for exact in extend_positions(held, sorted(pending, key=lambda p: len(p[1]))): exact_positions = dict(exact) for positions in candidates: @@ -253,7 +257,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d choices.append(EventPlacement(note, tuple((p, exact_positions[p['id']]) for p in note['pitches']), added)) if not compatible or not playable_choices(choices): continue - cost = voicing_cost(positions, profile, requirements.allowed, melody) if fresh else VoicingCost() + cost = voicing_cost(positions, profile, requirements.allowed, melody, composition=composition) if fresh else VoicingCost() yield AttackCandidate(tuple(sorted(assignment.items())), tuple(choices), positions, cost) else: shape = tuple(p for _, p in exact) + positions @@ -263,7 +267,7 @@ def attack_candidates(notes: list[dict], tones: dict[str, GuitarTone], active: d 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) if not playable_choices(choices): continue - cost = voicing_cost(shape, profile, requirements.allowed, melody) if fresh else VoicingCost() + cost = voicing_cost(shape, profile, requirements.allowed, melody, composition=composition) if fresh else VoicingCost() yield AttackCandidate(assignment, choices, shape, cost) def guitar_events(document: dict) -> tuple[list, dict[str, GuitarTone]]: @@ -325,7 +329,8 @@ def advance_attacks(candidates, states: dict) -> dict: result[candidate.active] = Placement(cost, candidate.active, previous, candidate.choices) return result -def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool, fallback: bool = False) -> dict: +def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[str, GuitarTone], states: dict, preserve_strings: bool, fallback: bool = False, *, composition=None) -> dict: + composition = composition or generator.vocabulary.current() continued = continued_sources(notes) fresh = has_attack(notes) held_ids = {i for previous in states.values() for i, _ in previous.active if retain_tone(tones[i], onset)} @@ -337,29 +342,30 @@ def advance_positions(notes: list[dict], onset: fractions.Fraction, tones: dict[ result[candidate.active] = Placement(cost, candidate.active, previous, candidate.choices) if not held_ids and not continued: - result = advance_attacks(attack_candidates(notes, tones, {}, {}, preserve_strings, fallback), states) + result = advance_attacks(attack_candidates(notes, tones, {}, {}, preserve_strings, fallback, composition=composition), states) else: for previous in states.values(): active = dict(previous.active) held = {i: p for i, p in previous.active if i not in continued and i in held_ids} - for candidate in attack_candidates(notes, tones, active, held, preserve_strings, fallback): + for candidate in attack_candidates(notes, tones, active, held, preserve_strings, fallback, composition=composition): distance = movement(tuple(p for _, p in previous.active), candidate.shape) if fresh else 0 keep(candidate, previous, previous.cost.moved(distance)) if not result: if not fallback and any(n.get('_harmonize') for n in notes): - return advance_positions(notes, onset, tones, states, preserve_strings, True) + return advance_positions(notes, onset, tones, states, preserve_strings, True, composition=composition) raise ValueError(f'event {notes[0]["id"]}, onset {onset}: no guitar placement; single notes need frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_SINGLE_FRET}; chords need frets {generator.fretboard.MIN_FRET}-{generator.fretboard.MAX_FRET}, an anchor <= {generator.fretboard.MAX_ANCHOR_FRET}, and at most {generator.fretboard.MAX_FRET_SPAN} consecutive fret positions; held/tied strings must remain fixed') return result -def place_guitar(document: dict) -> None: +def place_guitar(document: dict, *, composition=None) -> None: """Choose electric voicings or exact/held melody placements.""" + composition = composition or generator.vocabulary.current() events, tones = guitar_events(document) states = {(): Placement(VoicingCost(), (), None, ())} groups = [(key[0], [item[3] for item in values]) for key, values in itertools.groupby(events, key=lambda e: e[:3])] for index, (onset, notes) in enumerate(groups): next_onset = groups[index + 1][0] if index + 1 < len(groups) else None preserve_strings = next_onset is not None and any('_harmony' in n and onset + generator.model.fraction(n['duration']) > next_onset for n in notes) - states = advance_positions(notes, onset, tones, states, preserve_strings) + states = advance_positions(notes, onset, tones, states, preserve_strings, composition=composition) selected = min(states.values(), key=lambda s: (s.cost, s.active)) skipped = [] while selected.previous is not None: diff --git a/generator/model.py b/generator/model.py index 76e403f..b701979 100644 --- a/generator/model.py +++ b/generator/model.py @@ -3,8 +3,6 @@ from __future__ import annotations import copy import fractions import typing -import generator.fretboard -import generator.pitch MAX_INTEGER = (1 << 63) - 1 NOTE_TYPES = ('maxima', 'long', 'breve', 'whole', 'half', 'quarter', 'eighth', '16th', '32nd', '64th', '128th', '256th', '512th', '1024th') @@ -140,157 +138,3 @@ def make_event(identifier: str, onset: fractions.Fraction, duration: fractions.F if 'alter' in event['pitches'][-1]: event['pitches'][-1]['alter'] = rational(alteration(p['alter'])) return event - -def validate_document(document: dict) -> None: - if not isinstance(document, dict) or not isinstance(document.get('metadata'), dict): - raise ValueError('score requires a metadata object') - if any(not isinstance(document['metadata'].get(name), str) for name in ('title', 'composer', 'key_signature', 'time_signature')): - raise ValueError('score metadata requires title, composer, key_signature and time_signature strings') - staves = document.get('staves', []) - if not isinstance(staves, list) or len(staves) != 1 or not isinstance(staves[0], dict): - raise ValueError('exactly one staff is currently supported') - if not isinstance(staves[0].get('measures'), list) or not staves[0]['measures']: - raise ValueError('score requires measures') - instruments = staves[0].get('instruments', []) - instrument_ids = {i['id'] for i in instruments} - if len(instruments) != len(instrument_ids) or any(not isinstance(i, str) or not i for i in instrument_ids): - raise ValueError('score instruments require unique nonempty IDs') - ids: set[str] = set() - events: dict[str, dict] = {} - tones: dict[str, tuple[dict, fractions.Fraction, fractions.Fraction, str]] = {} - ownership: dict[str, str] = {} - absolute = fractions.Fraction() - for measure in staves[0]['measures']: - if {'repeats', 'endings', 'initial_attributes'} & measure.keys(): - raise ValueError('repeat notation is unsupported; regenerate scores from music written out in reading order') - length = fraction(measure['duration']) - if length <= 0: - raise ValueError('measure duration must be positive') - voices = measure.get('voices', []) - if not voices or len({v['id'] for v in voices}) != len(voices) or any(not v['id'] for v in voices): - raise ValueError('measure requires uniquely identified voice lanes') - previous_ends = {v['id']: fractions.Fraction() for v in voices} - for voice, note in ((v, n) for v in voices for n in v['notes']): - if any({'sound_ties', 'printed_ties'} & pitch.keys() for pitch in note['pitches']): - raise ValueError('pass-specific tie metadata is unsupported; regenerate scores') - if not note['id'] or note['id'] in ids: - raise ValueError(f'duplicate event ID: {note["id"]}') - ids.add(note['id']) - events[note['id']] = note - ownership[note['id']] = voice['id'] - if note.get('voice', voice['id']) != voice['id']: - raise ValueError('event voice ownership disagrees with its lane') - onset, duration = fraction(note['onset']), fraction(note['duration']) - if onset < 0 or duration < 0 or onset + duration > length: - raise ValueError(f'event outside measure: {note["id"]}') - if note['kind'] not in ('note', 'rest', 'spacer'): - raise ValueError('invalid event kind') - if not note.get('grace'): - if duration == 0 or onset < previous_ends[voice['id']]: - raise ValueError('overlapping events in one rhythmic voice') - previous_ends[voice['id']] = onset + duration - elif duration != 0: - raise ValueError('grace notes must not advance metric time') - expected = metric_length(note['written']) - if not note.get('grace') and not note.get('measure_rest') and duration != expected: - raise ValueError('metric and written event durations disagree') - if note['kind'] != 'note' and note.get('pitches'): - raise ValueError('rests and spacers cannot contain pitches') - if note['kind'] == 'note' and not note.get('pitches') and note.get('notehead') != 'slash': - raise ValueError('pitched event requires at least one pitch') - for p in note.get('pitches', []): - if p.get('instrument') and p['instrument'] not in instrument_ids: - raise ValueError('note refers to an unknown score instrument') - if 'unpitched' in p: - position = p['unpitched'] - if position['letter'] not in LETTERS or type(position['octave']) is not int: - raise ValueError('invalid unpitched display position') - else: - pitch_number(p) - if staves[0].get('tablature'): - tab = p.get('tablature', {}) - string, fret = tab.get('string'), tab.get('fret') - if type(string) is not int or type(fret) is not int or string not in dict(generator.fretboard.STANDARD_TUNING) or not generator.fretboard.MIN_FRET <= fret <= generator.fretboard.MAX_FRET: - raise ValueError(f'{p["id"]}: invalid generated tablature position') - state = dict(measure) - for change in measure.get('attributes', []): - if fraction(change['onset']) <= onset: - state.update(change) - if dict(generator.fretboard.STANDARD_TUNING)[string] + fret != generator.pitch.sounding_pitch(p, state): - raise ValueError(f'{p["id"]}: tablature disagrees with sounding pitch') - pd = fraction(p['duration']) - if pd < 0 or (pd == 0 and not note.get('grace')) or pd > duration or not p['id'] or p['id'] in tones: - raise ValueError('invalid chord-tone duration or duplicate ID') - if not note.get('grace') and pd != metric_length(p['written']): - raise ValueError('metric and written chord-tone durations disagree') - tones[p['id']] = (p, absolute + onset, pd, voice['id']) - for harmony in measure.get('harmonies', []): - if not 0 <= fraction(harmony['onset']) < length: - raise ValueError('harmony outside measure') - for change in measure.get('attributes', []): - if not 0 <= fraction(change['onset']) < length: - raise ValueError('attribute change outside measure') - for state in [measure, *measure.get('attributes', [])]: - if 'meter' in state: - validate_meter(state['meter']) - if 'key' in state: - if len(state['key']['alterations']) != 7: - raise ValueError('key alterations must contain seven exact alterations') - for value in state['key']['alterations']: - alteration(value) - if 'clef' in state and (state['clef']['sign'] not in ('G', 'F', 'C', 'percussion', 'none') or not 1 <= state['clef']['line'] <= 6 or not -3 <= state['clef']['octave_change'] <= 3): - raise ValueError('invalid clef') - absolute += length - for identifier, (p, start, duration, voice) in tones.items(): - if target := p.get('tie_to'): - if target not in tones: - raise ValueError(f'dangling tie: {identifier} -> {target}') - other, next_start, _, next_voice = tones[target] - same_pitch = all(p.get(k) == other.get(k) for k in ('unpitched', 'instrument')) if 'unpitched' in p else all(p.get(k) == other.get(k) for k in ('letter', 'alter', 'octave')) - if other.get('tie_from') != identifier or not same_pitch or next_voice != voice or next_start != start + duration: - raise ValueError(f'invalid tie: {identifier} -> {target}') - if staves[0].get('tablature') and p['tablature'] != other['tablature']: - raise ValueError(f'{identifier}: tied guitar tones must retain their string and fret') - if source := p.get('tie_from'): - if source not in tones or tones[source][0].get('tie_to') != identifier: - raise ValueError(f'dangling incoming tie: {identifier}') - span_ids = set() - event_order = {identifier: index for index, identifier in enumerate(events)} - for span in staves[0].get('tuplets', []): - if not span['id'] or span['id'] in span_ids or span['start'] not in ids or span['end'] not in ids or not 0 < span['note_count'] <= 1000000 or not 0 < span['space_count'] <= 1000000 or not 0 <= span['depth'] <= 16: - raise ValueError('invalid tuplet span') - members = [n['id'] for n in events.values() if span['id'] in n.get('tuplet_ids', [])] - if not members or members[0] != span['start'] or members[-1] != span['end'] or event_order[span['start']] > event_order[span['end']]: - raise ValueError('tuplet endpoints disagree with member events') - if len({ownership[n] for n in members}) != 1 or span.get('voice', ownership[members[0]]) != ownership[members[0]]: - raise ValueError('tuplet crosses independent voice lanes') - if span.get('bracket', 'auto') not in ('yes', 'no', 'auto') or span.get('show_number', 'actual') not in ('none', 'actual', 'both'): - raise ValueError('invalid tuplet appearance') - span_ids.add(span['id']) - for note in events.values(): - for verse in note.get('lyrics', []): - start = tones[note['pitches'][0]['id']][1] if note['pitches'] else fractions.Fraction() - if 'extend_to' in verse and not start < fraction(verse['extend_to']) <= absolute: - raise ValueError('lyric extension endpoint is outside the score') - if any(t not in span_ids for t in note.get('tuplet_ids', [])): - raise ValueError('event refers to an unknown tuplet') - if tremolo := note.get('tremolo'): - if tremolo['type'] not in ('single', 'start', 'stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8: - raise ValueError('invalid measured tremolo') - if tremolo['type'] in ('start', 'stop'): - other = events.get(tremolo.get('partner')) - expected_type = 'stop' if tremolo['type'] == 'start' else 'start' - if not other or other.get('tremolo', {}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']: - raise ValueError('invalid paired tremolo') - for span in staves[0].get('octave_shifts', []): - if not 0 <= fraction(span['start']) < fraction(span['end']) <= absolute or not -3 <= span['octaves'] <= 3: - raise ValueError('invalid octave-shift span') - previous_end = fractions.Fraction() - for span in staves[0].get('scale_spans', []): - start, end = fraction(span['start']), fraction(span['end']) - if not previous_end <= start < end <= absolute or not isinstance(span.get('label'), str) or not span['label'].strip() or type(span.get('matched')) is not bool: - raise ValueError('invalid solo scale span') - if not isinstance(span.get('id'), str) or not span['id'] or span['id'] in ids: - raise ValueError('solo scale spans require unique IDs') - ids.add(span['id']) - previous_end = end diff --git a/generator/musicxml.py b/generator/musicxml.py index e5aaca4..6878fa0 100644 --- a/generator/musicxml.py +++ b/generator/musicxml.py @@ -8,6 +8,7 @@ import defusedxml.ElementTree import generator.harmony import generator.model +import generator.validation class MusicXMLError(ValueError): pass @@ -621,7 +622,7 @@ class Importer: 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['staves'][0]['instruments'] = instruments - generator.model.validate_document(result) + generator.validation.validate_document(result) return result def load_musicxml(path: pathlib.Path) -> dict: diff --git a/generator/musicxml_writer.py b/generator/musicxml_writer.py index 5c48ff7..1730f52 100644 --- a/generator/musicxml_writer.py +++ b/generator/musicxml_writer.py @@ -7,6 +7,7 @@ import xml.etree.ElementTree import generator.harmony import generator.model +import generator.validation def element(parent, tag, content=None, **attributes): node = xml.etree.ElementTree.SubElement(parent, tag, {k.replace('_', '-'): str(v) for k, v in attributes.items() if v is not None}) @@ -209,7 +210,7 @@ def write_barline(bar, measure, location): element(node, 'bar-style', measure.get('barlines', {}).get(location, 'regular')) def musicxml_tree(document: dict, *, tablature=False) -> xml.etree.ElementTree.ElementTree: - generator.model.validate_document(document) + generator.validation.validate_document(document) root = xml.etree.ElementTree.Element('score-partwise', {'version': '4.0'}) metadata, staff = document['metadata'], document['staves'][0] element(element(root, 'work'), 'work-title', metadata['title']) diff --git a/generator/parts.py b/generator/parts.py index 518afa3..3a2da69 100644 --- a/generator/parts.py +++ b/generator/parts.py @@ -3,7 +3,6 @@ from __future__ import annotations import fractions import pathlib -import generator.accompaniment import generator.fretboard import generator.model import generator.pitch diff --git a/generator/performance.py b/generator/performance.py index 1d668ad..836f071 100644 --- a/generator/performance.py +++ b/generator/performance.py @@ -1,14 +1,8 @@ -"""Page timing and supplied recordings. No note synthesis or audio transformation.""" +"""Pure page coverage and performance timing.""" from __future__ import annotations import fractions -import hashlib -import json import math -import pathlib -import re -import shutil -import wave import generator.model @@ -117,57 +111,3 @@ def timing(document): return dict(song_id=document['metadata'].get('song_id', ''), duration=generator.model.rational(times[-1]), sustain_start=generator.model.rational(sustain_start), count_in_beats=4, clicks=clicks) - -def describe_wav(path, root, *, song_id='', bpm=0): - if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(root.resolve()): - raise ValueError(f'{path}: expected a contained regular WAV') - try: - with wave.open(str(path), 'rb') as audio: - channels, rate, frames = audio.getnchannels(), audio.getframerate(), audio.getnframes() - if audio.getcomptype() != 'NONE' or audio.getsampwidth() != 2 or channels not in (1, 2) or not 8000 <= rate <= 192000 or frames <= 0: - raise ValueError('expected nonempty mono/stereo PCM16 WAV') - remaining = frames - while remaining: - count = min(remaining, 65536) - if len(audio.readframes(count)) != count * channels * 2: - raise ValueError('truncated WAV data') - remaining -= count - except (wave.Error, EOFError) as error: - raise ValueError(f'{path}: invalid WAV: {error}') from error - with path.open('rb') as source: - digest = hashlib.file_digest(source, 'sha256').hexdigest() - return dict(path=path.relative_to(root).as_posix(), sha256=digest, channels=channels, sample_rate=rate, frames=frames, song_id=song_id, bpm=bpm) - -def copy_audio(static, destination, song_ids): - entries = [] - inputs = [(static / 'sounds' / name, '', 0) for name in ('beat1.wav', 'beat2.wav')] - for path in sorted((static / 'backing').glob('*.wav')): - match = re.fullmatch(r'([a-z0-9]+(?:_[a-z0-9]+)*)_(60|70|80|90|100|110|120)\.wav', path.name) - if not match: - raise ValueError(f'{path}: unsupported backing filename or tempo') - if match[1] not in song_ids: - continue - inputs.append((path, match[1], int(match[2]))) - for source, song_id, bpm in inputs: - description = describe_wav(source, static, song_id=song_id, bpm=bpm) - target = destination / description['path'] - target.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(source, target) - entries.append(description) - return entries - -def validate_recording_lengths(root, entries): - durations = {} - for path in sorted((root / 'songs').glob('*.json')): - document = json.loads(path.read_text()) - performance = timing(document) - song_id = performance['song_id'] - duration = generator.model.fraction(performance['duration']) + performance['count_in_beats'] - if song_id in durations and durations[song_id] != duration: - raise ValueError(f'{song_id}: parts disagree about performance duration') - durations[song_id] = duration - for entry in entries: - if entry['song_id']: - seconds = fractions.Fraction(entry['frames'], entry['sample_rate']) - if seconds < durations[entry['song_id']] * 60 / entry['bpm']: - raise ValueError(f"{entry['path']}: backing recording is shorter than count-in and score") diff --git a/generator/printing.py b/generator/printing.py index 96cad0f..11bfc26 100644 --- a/generator/printing.py +++ b/generator/printing.py @@ -7,7 +7,6 @@ import json import math import pathlib import shutil -import tempfile import time import PIL.PdfParser @@ -19,7 +18,7 @@ import generator.engraving.layout import generator.engraving.notation import generator.engraving.raster import generator.engraving.types -import generator.model +import generator.validation # Two layout units per PDF point: guitar staff spacing is five points. # Raster resolution is independent of layout and the device's display theme. @@ -30,7 +29,7 @@ MEDIA_BOX = [0, 0, 612, 792] VERSION = 'letter-parts-v1' def layout_part(document, fonts): - generator.model.validate_document(document) + generator.validation.validate_document(document) style = dataclasses.replace(generator.engraving.types.score_style(document, STYLE), system_gap=STYLE.system_gap) # Measure numbers are centered over barlines and can extend left of the # staff. Reserve their actual overhang inside the half-inch paper margin. @@ -123,7 +122,7 @@ def renderer_key(fonts): def restore(entry, key, target): try: - if generator.engraving.cache.has_symlink(entry): + if generator.cache.has_symlink(entry): return None pdf, manifest = entry / 'part.pdf', entry / 'print.json' if pdf.is_symlink() or manifest.is_symlink(): @@ -138,21 +137,10 @@ def restore(entry, key, target): return None def store(entry, key, target, pages): - if generator.engraving.cache.has_symlink(entry.parent): - raise ValueError('print cache must not contain symlinks') - entry.parent.mkdir(parents=True, exist_ok=True) - if entry.is_symlink() or entry.is_file(): - entry.unlink() - entry.mkdir(exist_ok=True) - with tempfile.TemporaryDirectory(prefix='.print_', dir=entry.parent) as temporary: - staged = pathlib.Path(temporary) + with generator.cache.stage_entry(entry, '.print_') as staged: shutil.copyfile(target, staged / 'part.pdf') (staged / 'print.json').write_text(json.dumps({'key': key, 'sha256': hashlib.sha256(target.read_bytes()).hexdigest(), 'pages': pages})) - for name in ('part.pdf', 'print.json'): - destination = entry / name - if destination.is_dir() and not destination.is_symlink(): - shutil.rmtree(destination) - (staged / name).replace(destination) + generator.cache.publish_files(entry, staged, ('part.pdf', 'print.json')) def generate_tree(root, font_path, *, cache_dir=None, progress=False): """Called inside the normal build's atomic staging directory.""" diff --git a/generator/production.py b/generator/production.py index e653452..acd92a2 100644 --- a/generator/production.py +++ b/generator/production.py @@ -1,342 +1,23 @@ -"""Independent, reproducible role generation and non-composing assembly. - -Only source documents and production resources enter generate_role. Completed -roles and published assets are downstream values, never composition inputs. -""" +"""File discovery, process scheduling and publication of independent roles.""" from __future__ import annotations import concurrent.futures import copy -import dataclasses -import hashlib -import functools import json import multiprocessing -import os import pathlib -import tempfile -import time -import fractions -import generator.accompaniment -import generator.comping -import generator.fretboard -import generator.model +import generator.arrange.accompaniment +import generator.arrange.assembly +import generator.composition +import generator.composition_types import generator.musicxml import generator.parts import generator.score -import generator.vocabulary -import generator.arrange.accompaniment -import generator.arrange.ending -import generator.arrange.form -import generator.arrange.generate -import generator.arrange.pickup -import generator.solo.analysis -import generator.solo.composer -import generator.solo.notation -import generator.solo.report -import generator.solo.learned -import generator.solo.authored -import generator.input_audit -import generator.solo.arrangement - -VERSION = 'independent-roles-1' -ROLES = ('acoustic_accompaniment', 'electric_accompaniment', 'electric_melody', 'vocal_melody', 'acoustic_solo', 'electric_solo') - -@dataclasses.dataclass(frozen=True) -class SourceAnalysis: - document_json: str - musical_key: str - @classmethod - def read(cls, document): - prepared = copy.deepcopy(document) - staff = generator.score.source_staff(prepared) - for i, m in enumerate(staff['measures']): - m['id'], m['number'] = f'source_{i}', str(i + 1) - staff['tuplets'] = generator.score.reidentify(staff['measures'], staff.get('tuplets', [])) - prepared['metadata'] = {k: v for k, v in prepared['metadata'].items() if k not in ('title', 'composer', 'copyright', 'software')} - prepared['metadata'].update(title='Source', composer='') - staff.update(id='source', name='Source') - melodic = generator.solo.analysis.analyze(prepared) - content = {'melody': [(str(n.start), str(n.end), dataclasses.asdict(n.pitch)) for n in melodic.melody], - 'measures': [{k: m.get(k) for k in ('duration', 'time_signature', 'key', 'harmonies', 'tempos', 'section_label', 'implicit')} for m in staff['measures']]} - # Fraction/string spelling is canonical and never includes filesystem IDs. - key = hashlib.sha256(json.dumps(content, sort_keys=True, default=str).encode()).hexdigest() - return cls(json.dumps(prepared, sort_keys=True), key) - def document(self): - return json.loads(self.document_json) - def accompaniment_key(self): - document = self.document() - document['metadata'].pop('tempo', None) - for measure in generator.score.source_staff(document)['measures']: - measure['tempos'] = [] - return SourceAnalysis.read(document).musical_key - -def windows(document, part): - source = generator.score.source_staff(document)['measures'] - sections = generator.arrange.form.full_arrangement_sections(source, part) - result, cursor = [], fractions.Fraction() - for section in sections: - duration = sum((generator.model.fraction(m['duration']) for m in source[section.source_measure_index:section.source_measure_index + section.measure_count]), fractions.Fraction()) - role = {'slash': 'solo', 'vocal': 'melody'}.get(section.role, section.role) - if section.role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and result: - attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(source[0]) if n['pitches']] - if attacks: - start = cursor - generator.model.fraction(source[0]['duration']) + min(attacks) - a, _, old = result[-1] - result[-1] = (a, start, old) - result.append((start, cursor, part + '_' + role)) - end = cursor + duration - start = cursor - if section.role == 'slash' and generator.score.body_start_measure_index(source) and result: - # Allocate the source's complete pickup bar before composition. This - # does not inspect either generated solo; a role may leave it silent. - start = cursor - generator.model.fraction(source[0]['duration']) - a, _, old = result[-1] - result[-1] = (a, start, old) - result.append((start, end, part + '_' + role)) - cursor = end - result.append((cursor, cursor + generator.arrange.ending.duration(source), part + ('_accompaniment' if part != 'vocal' else '_rest'))) - return tuple(result) - -def dependencies(role, resources, measures=None): - names = ['harmony', 'lead_ins'] if role.endswith('_solo') else [] - accompaniment = {} - if role.endswith('_accompaniment'): - policy = generator.vocabulary.accompaniment(role.split('_')[0], resources) - accompaniment = {'accompaniment': generator.comping_rhythm.relevant_policy(policy, measures) if measures is not None else policy} - return {'roles': {'role': resources['roles']['roles'][role], 'difficulty': resources['roles']['difficulty']}, - **({'voicing_families': generator.vocabulary.accompaniment('electric', resources)['voicing_families']} if role == 'electric_melody' else {}), - **accompaniment, - **{name: resources[name] for name in names}} - -def _code_key(role): - root = pathlib.Path(__file__).parent - paths = [root / n for n in ('production.py', 'model.py', 'harmony.py', 'pitch.py', 'fretboard.py', 'parts.py', 'score.py', 'vocabulary.py')] - paths += list((root / 'arrange').glob('*.py')) - if role.endswith('_solo'): - paths += list((root / 'solo').glob('*.py')) - paths += [root / 'harmonic_time.py', root / 'musicxml.py', root / 'training_io.py'] - elif role.endswith('_accompaniment'): - paths += [root / n for n in ('comping.py', 'comping_rhythm.py', 'chord_families.py', 'accompaniment.py', 'guitar.py')] - if role == 'acoustic_accompaniment': - paths += [root / 'acoustic_voicing.py', root / 'acoustic.py'] - elif role == 'electric_melody': - paths += [root / 'guitar.py', root / 'melody.py', root / 'chord_families.py'] - return generator.vocabulary.fingerprint([(p.relative_to(root).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted(set(paths))]) - -def _resource_key(directory, role): - return {'model': hashlib.sha256((directory / 'models' / 'solo.json').read_bytes()).hexdigest()} if role.endswith('_solo') else {} - -@functools.lru_cache(maxsize=4) -def _solo_model(directory, fingerprint): - return generator.solo.learned.Model.load(pathlib.Path(directory) / 'models' / 'solo.json') - -def _mask(document, allowed): - """Apply source-defined role exclusions, without observing another part.""" - absolute = fractions.Fraction() - for measure in document['staves'][0]['measures']: - length = generator.model.fraction(measure['duration']) - for voice in measure['voices']: - selected = [] - for n in voice['notes']: - start = absolute + generator.model.fraction(n['onset']) - end = start + generator.model.fraction(n['duration']) - intervals = [(max(start, a), min(end, b)) for a, b in allowed if a < end and b > start] - for a, b in intervals: - note = copy.deepcopy(n) - if a != start: - note['onset'] = generator.model.rational(a - absolute) - for pitch in note['pitches']: - pitch.pop('tie_from', None) - generator.arrange.pickup.truncate(note, b - absolute) - if a < b: - selected.append(note) - voice['notes'] = _fill(selected, length, measure['id'] + '_' + voice['id'], voice['id']) - absolute += length - staff = document['staves'][0] - staff['tuplets'] = generator.arrange.pickup.retain_links(staff['measures'], staff.get('tuplets', [])) - -def _fill(notes, length, prefix, voice='voice_arranged'): - notes = sorted(notes, key=lambda n: generator.model.fraction(n['onset'])) - result = [] - cursor = fractions.Fraction() - - def rests(end): - nonlocal cursor - for duration, written in generator.accompaniment.written_durations(end - cursor): - result.append(generator.model.make_event(prefix + '_rest_' + str(len(result)), cursor, duration, 'rest', written=written) | {'voice': voice}) - cursor += duration - - for n in notes: - start = generator.model.fraction(n['onset']) - if start < cursor: - raise ValueError(prefix + ': overlapping independently generated roles') - rests(start) - n['voice'] = voice - result.append(n) - cursor = start + generator.model.fraction(n['duration']) - rests(length) - return result - -def generate_role(source, role, data_dir, settings=None, cache_dir=None): - started = time.monotonic() - with generator.input_audit.composition(data_dir, role, cache_dir) as reads: - result = _generate_role(source, role, data_dir, settings, cache_dir) - # Observed reads are diagnostics, never cache identity or musical seeds. - result['trace'] = {**result['trace'], 'input_boundary': 'production data (including explicit authored repertoire), generator code, Python runtime, own role cache'} - result['observed_reads'] = sorted(reads) - result['seconds'] = time.monotonic() - started - return result - -def _generate_role(source, role, data_dir, settings=None, cache_dir=None): - if role not in ROLES: - raise ValueError('Unknown generation role: ' + role) - data_dir = pathlib.Path(data_dir) - resources = generator.vocabulary.load(data_dir / 'composition') - settings = settings or {} - registered = generator.solo.authored.lookup(source, role.removesuffix('_solo'), data_dir) if role.endswith('_solo') and not settings.get('predict', False) else None - algorithm = VERSION + '/' + generator.solo.learned.VERSION if role.endswith('_solo') else VERSION - inputs = {'version': algorithm, 'source': source.musical_key, 'role': role, 'settings': settings, - 'vocabulary': dependencies(role, resources, generator.score.source_staff(source.document())['measures']), 'catalogs': ({'authored': registered['sha256'], 'id': registered['id'], 'pitch_convention': registered['pitch_convention']} if registered else _resource_key(data_dir, role)), 'code': _code_key(role)} - # Source notation/lyrics also invalidate cached score representation, never the musical seed. - key = generator.vocabulary.fingerprint({'inputs': inputs, 'notation': source.document_json}) - path = pathlib.Path(cache_dir) / role / (key + '.json') if cache_dir else None - if path and path.is_file() and not path.is_symlink(): - try: - entry = json.loads(path.read_text()) - value = entry['result'] - if entry['key'] == key and entry['digest'] == generator.vocabulary.fingerprint(value): - generator.model.validate_document(value['document']) - generator.fretboard.validate_document(value['document']) - return {**value, 'cache_hit': True} - except (ValueError, KeyError, TypeError): - pass - started = time.monotonic() - document = source.document() - part = role.split('_')[0] - trace = {'source': source.musical_key, 'role': role, 'production_resources': inputs['catalogs'], - 'vocabulary': generator.vocabulary.fingerprint(inputs['vocabulary']), 'settings': settings, 'algorithm': algorithm, 'code': inputs['code'], 'cache_key': key} - choice_source = source.accompaniment_key() if role.endswith('_accompaniment') else source.musical_key - choice_data, choice_settings = inputs['vocabulary'], settings - if role.endswith('_accompaniment'): - # Voicing weights invalidate the concrete cache, but must not reshuffle - # rhythm patterns. Removing this new field preserves the pre-policy seed. - choice_data = copy.deepcopy(choice_data) - choice_data['roles']['role'].pop('voicing', None) - for field in ('provenance', 'license', 'research'): - choice_data['accompaniment'].pop(field, None) - choice_settings = {k: v for k, v in settings.items() if k != 'voicing'} - seed = generator.vocabulary.fingerprint({'source': choice_source, 'role': role, 'settings': choice_settings, 'data': choice_data, 'version': algorithm}) - details = {} - solo_report = '' - composition_timings = {} - with generator.vocabulary.using(resources, role, settings | {'choice_seed': seed}): - if role.endswith('_solo'): - result = generator.arrange.generate._arrange_document(document, part, {}, owned={'slash'}) - si = 3 if part == 'acoustic' else 4 - first, count, offset, length = generator.solo.arrangement.regions(document)[si] - # The template/harmony comes directly from source form, not another generated part. - authored = generator.score.source_staff(document)['measures'] - section = generator.arrange.form.full_arrangement_sections(authored, part)[si] - effective = generator.arrange.generate.section_source(authored, section, generator.arrange.form.full_arrangement_sections(authored, part)[si + 1]) - template = copy.deepcopy(effective[section.source_measure_index:section.source_measure_index + count]) - initial = generator.accompaniment.carried_chords(effective)[section.source_measure_index] - start, end = next((a - offset, b - offset) for a, b, r in windows(document, part) if r == role) - notation_first = first - if start < 0: - # The preceding arrangement harmony comes from the source form. Do - # not invent harmony from a comparison score's unlabelled lead-in. - template.insert(0, copy.deepcopy(result['staves'][0]['measures'][first - 1])) - notation_first -= 1 - initial = generator.accompaniment.carried_chords(result['staves'][0]['measures'])[first - 1] - if registered: - solo = generator.solo.authored.realize(registered, generator.solo.analysis.analyze(document), template, part, start, end, initial) - else: - solo_model = _solo_model(str(data_dir), inputs['catalogs']['model']) - solo = generator.solo.composer.compose(generator.solo.analysis.analyze(document), template, part, end, initial=initial, learned_model=solo_model, expressions_enabled=generator.vocabulary.role().get('expressions', True), start=start) - composition_timings = {k: v for k, v in solo.diagnostics.items() if k.endswith('_seconds')} - relative = tuple(dataclasses.replace(n, start=n.start - start, end=n.end - start) for n in solo.notes()) - output = generator.solo.notation.measures(result['staves'][0]['measures'][notation_first:first + count], relative, 'solo_' + part) - generator.solo.notation.replace_region(result, notation_first, output) - details = generator.solo.report.structure(solo) - details['allocation'] = {'start': str(start), 'end': str(end), 'lead_in_measures': first - notation_first} - solo_report = generator.solo.report.describe(solo, first, offset, detail=details) - elif role.endswith('_accompaniment'): - canonical = generator.comping.Canonical(document, part, seed) - result = generator.arrange.generate._arrange_document(document, part, {}, owned={'accompaniment'}, accompaniment=canonical) - details = {'canonical_comping': canonical.report, 'accompaniment_rhythm': canonical.rhythm_report, 'groove_cycles': canonical.groove_report, 'rhythm_source': choice_source} - else: - result = generator.arrange.generate._arrange_document(document, part, {}, owned={'vocal' if part == 'vocal' else 'melody'}) - allowed = tuple((a, b) for a, b, r in windows(document, part) if r == role) - _mask(result, allowed) - if role == "acoustic_accompaniment": - generator.acoustic.validate_comping(result) - staff = result['staves'][0] - for measure in staff['measures']: - measure['id'] = role + '_' + measure['id'] - staff['tuplets'] = generator.score.reidentify(staff['measures'], staff.get('tuplets', [])) - if part != 'vocal': - result['staves'][0]['tablature'] = True - generator.model.validate_document(result) - generator.fretboard.validate_document(result) - value = {'role': role, 'document': result, 'trace': trace, 'details': details, 'report': solo_report, 'seconds': time.monotonic() - started, 'composition_timings': composition_timings, 'cache_hit': False} - # Sidecar diagnostics use the same rational-safe representation as reports. - # Identical insertion order after cold generation and cache loading also - # keeps serialized scores and the catalog byte-for-byte reproducible. - value = json.loads(json.dumps(value, default=str, sort_keys=True)) - if path: - path.parent.mkdir(parents=True, exist_ok=True) - payload = {'key': key, 'digest': generator.vocabulary.fingerprint(value), 'result': value} - with tempfile.NamedTemporaryFile('w', dir=path.parent, delete=False) as out: - json.dump(payload, out, sort_keys=True) - temporary = out.name - os.replace(temporary, path) - return value - -def assemble(source, results): - """Union prevalidated role windows; no selection or pitch/fingering rewrite.""" - documents = {} - for part in generator.parts.PART_TYPES: - selected = [results[r]['document'] for r in ROLES if r.startswith(part + '_')] - document = copy.deepcopy(selected[0]) - staff = document['staves'][0] - if part == 'vocal': - documents[part] = document - continue - staff['tuplets'] = [] - staff['octave_shifts'] = [] - staff['scale_spans'] = [] - for other in selected: - for field in ('tuplets', 'octave_shifts', 'scale_spans'): - staff[field].extend(copy.deepcopy(other['staves'][0].get(field, []))) - for i, m in enumerate(staff['measures']): - # Tuplet rests belong to the source/role rhythm and must survive union. - lanes = {} - for d in selected: - for voice in d['staves'][0]['measures'][i]['voices']: - lane = voice['id'] if part == 'acoustic' and voice['id'] in ('upper', 'bass') else 'voice_arranged' - sounding = [copy.deepcopy(n) for n in voice['notes'] if n['pitches'] or n.get('tuplet_ids')] - if sounding: - lanes.setdefault(lane, []).extend(sounding) - lanes = lanes or {'voice_arranged': []} - m['voices'] = [{'id': lane, 'notes': _fill(notes, generator.model.fraction(m['duration']), m['id'] + '_' + lane, lane)} for lane, notes in lanes.items()] - # Attribute changes belong to source melody pickups and are assembly metadata. - for d in selected: - other = d['staves'][0]['measures'][i] - if other.get('attributes'): - m['attributes'] = copy.deepcopy(other['attributes']) - staff['tuplets'] = generator.arrange.pickup.retain_links(staff['measures'], staff['tuplets']) - owners = {n['id']: v['id'] for m in staff['measures'] for v in m['voices'] for n in v['notes']} - for span in staff['tuplets']: - span['voice'] = owners[span['start']] - generator.model.validate_document(document) - generator.fretboard.validate_document(document) - documents[part] = document - return documents +import generator.score_io +import generator.source def _job(request): - source, role, directory, settings, cache = request - return generate_role(source, role, directory, settings, cache) + return generator.composition.generate_role(request.source, request.role, request.directory, json.loads(request.settings_json), request.cache) def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=False, timings=None): if type(jobs) is not int or not 1 <= jobs <= 16: @@ -345,8 +26,8 @@ def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=Fa sources = {} for path in generator.parts.source_paths(source_dir): document = generator.parts.prepare_document(generator.musicxml.load_musicxml(path)) - sources[path] = (document, SourceAnalysis.read(document)) - requests = [(path, role, (s, role, source_dir.parent, {}, cache_dir)) for path, (_, s) in sources.items() for role in ROLES] + sources[path] = (document, generator.source.SourceAnalysis.read(document)) + requests = [(path, role, generator.composition_types.RoleRequest(s, role, source_dir.parent, cache=cache_dir)) for path, (_, s) in sources.items() for role in generator.composition_types.ROLES] results = {p: {} for p in sources} def accept(path, role, value): @@ -372,16 +53,16 @@ def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=Fa except Exception as error: raise ValueError(f'{path.name}/{role}: {error}') from error for path, (original, source) in sources.items(): - assembled = assemble(source, results[path]) + assembled = generator.arrange.assembly.assemble(source, results[path]) generator.arrange.accompaniment.audit(original, assembled) reuse = generator.arrange.accompaniment.audit(original, {role: value['document'] for role, value in results[path].items()}, role_documents=True) for part, document in assembled.items(): document['metadata'] = copy.deepcopy(original['metadata']) document['metadata']['song_id'] = generator.score.slugify(path.stem) - generator.score.write_json(output_dir / (generator.score.slugify(path.stem) + '_' + part + '.json'), document) + generator.score_io.write_json(output_dir / (generator.score.slugify(path.stem) + '_' + part + '.json'), document) analysis_dir = output_dir.parent / 'analysis' analysis_dir.mkdir(parents=True, exist_ok=True) - (analysis_dir / (generator.score.slugify(path.stem) + '.txt')).write_text('\n'.join(results[path][r]['report'] for r in ROLES)) + (analysis_dir / (generator.score.slugify(path.stem) + '.txt')).write_text('\n'.join(results[path][r]['report'] for r in generator.composition_types.ROLES)) # Not a score file: diagnostics are deliberately outside the songs schema. stable = {role: {k: v for k, v in value.items() if k not in ('seconds', 'composition_timings', 'cache_hit', 'observed_reads')} for role, value in results[path].items()} for role, audit in reuse.items(): diff --git a/generator/role_cache.py b/generator/role_cache.py new file mode 100644 index 0000000..4ed6e9f --- /dev/null +++ b/generator/role_cache.py @@ -0,0 +1,29 @@ +"""Role cache persistence; cache identity never supplies musical randomness.""" +import json + +import generator.cache +import generator.fretboard +import generator.validation + +ENTRYPOINTS = ('generator.composition',) + +def code_key(): + return generator.cache.fingerprint(generator.cache.source_identity(ENTRYPOINTS)) + +def restore(path, key): + if path is None or not path.is_file() or path.is_symlink(): + return None + try: + entry = json.loads(path.read_text()) + value = entry['result'] + if entry['key'] == key and entry['digest'] == generator.cache.fingerprint(value): + generator.validation.validate_document(value['document']) + generator.fretboard.validate_document(value['document']) + return {**value, 'cache_hit': True} + except (OSError, ValueError, KeyError, TypeError): + pass + return None + +def store(path, key, value): + if path is not None: + generator.cache.write_json(path, {'key': key, 'digest': generator.cache.fingerprint(value), 'result': value}) diff --git a/generator/score.py b/generator/score.py index dfc9377..9e0fd4e 100644 --- a/generator/score.py +++ b/generator/score.py @@ -1,53 +1,8 @@ +"""Pure score navigation, cloning and identity operations.""" from __future__ import annotations - import copy -import json -import pathlib import re -import generator.engraving.validation -import generator.fretboard -import generator.model -import generator.pico_artwork -import generator.performance - -def load_json(path: pathlib.Path) -> dict: - document = json.loads(path.read_text(encoding='utf-8')) - return validate_json(document, path) - -def validate_json(document: dict, path: pathlib.Path, validated_pages: set[str] | None = None) -> dict: - if not isinstance(document, dict): - raise ValueError('expected a score object') - if 'music' in document: - presentation = document.get('presentation', {}) - if not isinstance(document['music'], dict) or not isinstance(presentation, dict) or not isinstance(presentation.get('pages'), list) or not presentation['pages']: - raise ValueError('invalid score bundle; regenerate score assets') - if (presentation.get('engine'), presentation.get('width'), presentation.get('height')) != (generator.engraving.validation.ENGINE_NAME, generator.engraving.validation.PAGE_WIDTH, generator.engraving.validation.PAGE_HEIGHT): - raise ValueError('unsupported score presentation; regenerate score assets') - generator.performance.validate_views(document['music'], presentation['pages'], presentation.get('playback_pages')) - if document.get('performance') != generator.performance.timing(document['music']): - raise ValueError('invalid performance timing') - for page in presentation['pages'] + presentation['playback_pages']: - if not isinstance(page, dict) or not isinstance(page.get('path'), str) or not isinstance(page.get('sha256'), str): - raise ValueError('invalid score page manifest') - name = page['path'] - relative = pathlib.Path(name) - if '\x00' in name or '\\' in name or relative.is_absolute() or any(part in ('', '.', '..') for part in name.split('/')) or relative.suffix != '.png': - raise ValueError('invalid score page path') - resolved = (path.parent / relative).resolve() - if not resolved.is_relative_to(path.parent.resolve()): - raise ValueError('score page escapes its asset directory') - generator.engraving.validation.check_page(resolved, validated_pages, expected_sha256=page['sha256']) - document = document['music'] - generator.model.validate_document(document) - return document - -def write_json(path: pathlib.Path, document: dict) -> None: - generator.model.validate_document(document) - generator.fretboard.validate_document(document) - path.parent.mkdir(parents=True, exist_ok=True) - 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' @@ -124,15 +79,3 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]: def make_arranged_document(source_document: dict, staff: dict, *, staff_id: str, staff_name: str, role: str, arrangement_part_type: str, measures: list[dict], tuplets=None, octave_shifts=None) -> dict: return {'metadata': copy.deepcopy(source_document['metadata']), 'staves': [{'id': staff_id, 'name': staff_name, 'clef': copy.deepcopy(staff['clef']), 'role': role, 'arranged': True, 'arrangement_part_type': arrangement_part_type, 'measures': measures, 'tuplets': tuplets or [], 'octave_shifts': copy.deepcopy(octave_shifts or []), 'instruments': copy.deepcopy(staff.get('instruments', []))}]} - -def count_json_files(root: pathlib.Path) -> int: - return sum(1 for p in root.rglob('*.json') if p.is_file()) - -def validate_generated_tree(root: pathlib.Path, validated_pages: set[str] | None = None) -> None: - validated_pages = set() if validated_pages is None else validated_pages - for path in root.rglob('*.json'): - document = json.loads(path.read_text(encoding='utf-8')) - music = validate_json(document, path, validated_pages) - generator.fretboard.validate_document(music) - if 'presentation' in document: - generator.pico_artwork.validate_title(root, document['presentation'].get('title_image')) diff --git a/generator/score_io.py b/generator/score_io.py new file mode 100644 index 0000000..05c605e --- /dev/null +++ b/generator/score_io.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import json +import pathlib + +import generator.engraving.validation +import generator.fretboard +import generator.performance +import generator.pico_artwork +import generator.validation + +def load_json(path: pathlib.Path) -> dict: + document = json.loads(path.read_text(encoding='utf-8')) + return validate_json(document, path) + +def validate_json(document: dict, path: pathlib.Path, validated_pages: set[str] | None = None) -> dict: + if not isinstance(document, dict): + raise ValueError('expected a score object') + if 'music' in document: + presentation = document.get('presentation', {}) + if not isinstance(document['music'], dict) or not isinstance(presentation, dict) or not isinstance(presentation.get('pages'), list) or not presentation['pages']: + raise ValueError('invalid score bundle; regenerate score assets') + if (presentation.get('engine'), presentation.get('width'), presentation.get('height')) != (generator.engraving.validation.ENGINE_NAME, generator.engraving.validation.PAGE_WIDTH, generator.engraving.validation.PAGE_HEIGHT): + raise ValueError('unsupported score presentation; regenerate score assets') + generator.performance.validate_views(document['music'], presentation['pages'], presentation.get('playback_pages')) + if document.get('performance') != generator.performance.timing(document['music']): + raise ValueError('invalid performance timing') + for page in presentation['pages'] + presentation['playback_pages']: + if not isinstance(page, dict) or not isinstance(page.get('path'), str) or not isinstance(page.get('sha256'), str): + raise ValueError('invalid score page manifest') + name = page['path'] + relative = pathlib.Path(name) + if '\x00' in name or '\\' in name or relative.is_absolute() or any(part in ('', '.', '..') for part in name.split('/')) or relative.suffix != '.png': + raise ValueError('invalid score page path') + resolved = (path.parent / relative).resolve() + if not resolved.is_relative_to(path.parent.resolve()): + raise ValueError('score page escapes its asset directory') + generator.engraving.validation.check_page(resolved, validated_pages, expected_sha256=page['sha256']) + document = document['music'] + generator.validation.validate_document(document) + return document + +def write_json(path: pathlib.Path, document: dict) -> None: + generator.validation.validate_document(document) + generator.fretboard.validate_document(document) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') + +def count_json_files(root: pathlib.Path) -> int: + return sum(1 for p in root.rglob('*.json') if p.is_file()) + +def validate_generated_tree(root: pathlib.Path, validated_pages: set[str] | None = None) -> None: + validated_pages = set() if validated_pages is None else validated_pages + for path in root.rglob('*.json'): + document = json.loads(path.read_text(encoding='utf-8')) + music = validate_json(document, path, validated_pages) + generator.fretboard.validate_document(music) + if 'presentation' in document: + generator.pico_artwork.validate_title(root, document['presentation'].get('title_image')) diff --git a/generator/solo/authored.py b/generator/solo/authored.py index efb7945..e8f6bfa 100644 --- a/generator/solo/authored.py +++ b/generator/solo/authored.py @@ -6,20 +6,21 @@ policy. Unknown song/role pairs go to the learned composer. """ from __future__ import annotations import dataclasses +import fractions import hashlib import json import pathlib -import fractions + +import generator.harmonic_time import generator.musicxml import generator.parts -import generator.training_io -import generator.harmonic_time -import generator.solo.analysis -import generator.solo.types +import generator.solo.harmonic import generator.solo.placement import generator.solo.profiles -import generator.solo.harmonic -import generator.production +import generator.solo.types +import generator.source +import generator.training_io +import generator.vocabulary def sounding(evidence, offset=fractions.Fraction()): result = [] @@ -32,9 +33,10 @@ def sounding(evidence, offset=fractions.Fraction()): result.append(generator.solo.types.Note(fractions.Fraction(event['start']) - offset, fractions.Fraction(event['end']) - offset, pitch, role='authored', event_id=f'authored_{i}')) return tuple(result) -def validate_evidence(document, evidence): +def validate_evidence(document, evidence, *, composition=None): """Training uses the same source-form harmonic context as solo playback.""" - h = generator.solo.harmonic.Harmony(generator.harmonic_time.solo_source(document['staves'][0])) + composition = composition or generator.vocabulary.current() + h = generator.solo.harmonic.Harmony(generator.harmonic_time.solo_source(document['staves'][0]), composition=composition) checked = h.evaluate(sounding(evidence), complete=True) if not checked.valid: raise ValueError('Unsupported harmonic relationship in authored solo: ' + checked.error) @@ -56,7 +58,7 @@ def lookup(source, role, data_dir): if not path.is_relative_to((root / 'songs').resolve()): raise ValueError('Registered sources must be in data/songs') doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(path)) - if generator.production.SourceAnalysis.read(doc).musical_key == source.musical_key: + if generator.source.SourceAnalysis.read(doc).musical_key == source.musical_key: target = (manifest.parent / entry['solo']).resolve() if not target.is_relative_to(manifest.parent.resolve()): raise ValueError('Registered solo must be inside data/training') @@ -67,7 +69,8 @@ def lookup(source, role, data_dir): raise ValueError('Multiple authored solos registered for the same source music and role') return matches[0] if matches else None -def realize(entry, source_analysis, measures, part, start, end, initial=None): +def realize(entry, source_analysis, measures, part, start, end, initial=None, *, composition=None): + composition = composition or generator.vocabulary.current() evidence = generator.training_io.read(entry['path'], part, pitch_convention=entry['pitch_convention']) if evidence['discrepancies']: raise ValueError(entry['id'] + ': invalid authored timing/ties: ' + str(evidence['discrepancies'])) @@ -86,7 +89,7 @@ def realize(entry, source_analysis, measures, part, start, end, initial=None): if placed is None: raise ValueError(entry['id'] + ': no legal complete guitar placement') spans = tuple(dataclasses.replace(s, start=s.start + start, end=s.end + start) for s in generator.harmonic_time.read({'measures': measures}, initial=initial)) - checked = generator.solo.harmonic.Harmony(spans).evaluate(placed[1], complete=True) + checked = generator.solo.harmonic.Harmony(spans, composition=composition).evaluate(placed[1], complete=True) if not checked.valid: raise ValueError(entry['id'] + ': ' + checked.error) phrase = generator.solo.types.Phrase('authored', start, end, placed[1], 'authored', 'authored', 'handoff', theme=entry['id'], expressions=checked.contracts) diff --git a/generator/solo/composer.py b/generator/solo/composer.py index eaf3a62..2ecfec6 100644 --- a/generator/solo/composer.py +++ b/generator/solo/composer.py @@ -1,26 +1,28 @@ """The sole production solo composer: frozen learned model plus music validity.""" import dataclasses -import time import fractions import pathlib +import time + import generator.harmonic_time -import generator.vocabulary -import generator.solo.types -import generator.solo.profiles -import generator.solo.harmonic -import generator.solo.placement -import generator.solo.learned import generator.solo.decoder import generator.solo.groups +import generator.solo.harmonic +import generator.solo.learned +import generator.solo.placement +import generator.solo.profiles +import generator.solo.types +import generator.vocabulary -def compose(analysis, measures, part, end, *, learned_model=None, initial=None, expressions_enabled=True, start=fractions.Fraction()): +def compose(analysis, measures, part, end, *, learned_model=None, initial=None, expressions_enabled=True, start=fractions.Fraction(), composition=None): + composition = composition or generator.vocabulary.current() began = time.monotonic() spans = tuple(dataclasses.replace(s, start=s.start + start, end=s.end + start) for s in generator.harmonic_time.read({'measures': measures}, initial=initial)) profile = generator.solo.profiles.for_part(part, analysis) - harmony = generator.solo.harmonic.Harmony(spans, profile) + harmony = generator.solo.harmonic.Harmony(spans, profile, composition=composition) model = learned_model or generator.solo.learned.Model.load(pathlib.Path(__file__).resolve().parents[2] / 'data/models/solo.json') - settings = generator.vocabulary.role(part + '_solo') - builder = generator.solo.decoder.Decoder(analysis, harmony, model, part, start, end, expressions_enabled=expressions_enabled, attempts=settings.get('attempts', 4), seed=settings.get('choice_seed', '')) + settings = composition.role(part + '_solo') + builder = generator.solo.decoder.Decoder(analysis, harmony, model, part, start, end, expressions_enabled=expressions_enabled, attempts=settings.get('attempts', 4), seed=settings.get('choice_seed', ''), composition=composition) selected = builder.compose() decoded = time.monotonic() placed = generator.solo.placement.place(selected.notes, analysis.tempo, profile=profile) @@ -48,10 +50,11 @@ def compose(analysis, measures, part, end, *, learned_model=None, initial=None, 'counts': dict(builder.counts), 'frontiers': builder.trace, 'completed_candidates': [ {'rank': i + 1, 'cost': s.cost, 'events': [(str(a), str(b), p) for a, b, p in s.events]} for i, s in enumerate(builder.completed)], 'expressions_enabled': expressions_enabled}, profile, start) - validate(result) + validate(result, composition=composition) return result -def validate(solo): +def validate(solo, *, composition=None): + composition = composition or generator.vocabulary.current() previous = None for n in solo.notes(): if n.start < solo.start or n.end > solo.end or n.end <= n.start or previous and previous.end > n.start: @@ -59,7 +62,7 @@ def validate(solo): if n.position not in generator.solo.placement.positions(n.pitch.number(), solo.profile): raise ValueError('invalid guitar placement') previous = n - checked = generator.solo.harmonic.Harmony(solo.harmony).evaluate(solo.notes(), until=solo.end, complete=True, + checked = generator.solo.harmonic.Harmony(solo.harmony, composition=composition).evaluate(solo.notes(), until=solo.end, complete=True, expressions=tuple(c for p in solo.phrases for c in p.expressions)) if not checked.valid: raise ValueError(checked.error) diff --git a/generator/solo/decoder.py b/generator/solo/decoder.py index b7f5efe..cea9f6a 100644 --- a/generator/solo/decoder.py +++ b/generator/solo/decoder.py @@ -8,17 +8,18 @@ legacy fallback. A bounded restart handles an exhausted/entirely silent sample. from __future__ import annotations import collections import dataclasses -import hashlib -import random -import math import fractions +import hashlib +import math +import random + +import generator.harmony import generator.model -import generator.vocabulary import generator.solo.expressions import generator.solo.groups import generator.solo.learned import generator.solo.types -import generator.harmony +import generator.vocabulary class SearchExhausted(ValueError): pass @@ -35,7 +36,8 @@ class State: def event_key(events): return tuple((a, b, -1 if p is None else p) for a, b, p in events) class Decoder: - def __init__(self, analysis, harmony, model, role, start, end, *, expressions_enabled=True, attempts=4, seed=''): + def __init__(self, analysis, harmony, model, role, start, end, *, expressions_enabled=True, attempts=4, seed='', composition=None): + self.composition = composition or generator.vocabulary.current() if type(attempts) is not int or not 1 <= attempts <= 16: raise ValueError('attempts must be between 1 and 16') self.analysis, self.harmony, self.model, self.role = analysis, harmony, model, role @@ -45,7 +47,7 @@ class Decoder: self.seed = seed or hashlib.sha256(repr((source, role, generator.solo.learned.VERSION)).encode()).hexdigest() self.expressions_enabled = expressions_enabled self.low, self.high = harmony.profile.register - limits = generator.vocabulary.data('roles')['difficulty'] + limits = self.composition.data('roles')['difficulty'] self.durations = tuple(d for d in model.durations if 0 < d <= 4 and d.denominator in (1, 2, 4) and (d.denominator != 4 or analysis.tempo <= limits['sixteenth_max_bpm'])) self.allow_triplets = analysis.tempo <= limits['triplet_max_bpm'] @@ -92,12 +94,12 @@ class Decoder: bases = [] regions = [(None, n)] regions.extend((s.start, dataclasses.replace(n, start=s.start)) for s in self.harmony.spans - if n.start < s.start < n.end and s.start - n.start <= fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max'])) + if n.start < s.start < n.end and s.start - n.start <= fractions.Fraction(self.composition.data('harmony')['anticipation_max'])) for boundary, arrived in regions: exposed = [s for s, _, _ in self.harmony.segments(arrived) if n.pitch.number() % 12 not in s.roles.basic | s.roles.specified] if not exposed: continue - for rule in generator.vocabulary.data('harmony')['colors']: + for rule in self.composition.data('harmony')['colors']: if arrived.end - arrived.start > fractions.Fraction(rule['max_beats']): continue if any(s.chord == 'N.C.' or not generator.solo.expressions.eligible_color(rule, s, self.harmony) or @@ -139,7 +141,7 @@ class Decoder: if self.harmony.evaluate((n,), complete=True, expressions=(c,), infer=False).valid: return tuple([c]) for arriving in self.harmony.spans: - if n.start < arriving.start < n.end and arriving.start - n.start <= fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']): + if n.start < arriving.start < n.end and arriving.start - n.start <= fractions.Fraction(self.composition.data('harmony')['anticipation_max']): c = generator.solo.types.ExpressionContract(n.event_id + ':anticipation', 'anticipation', 'early_chord_tone', (n,), n, arriving.chord, arriving.start) if self.harmony.evaluate((n,), complete=True, expressions=(c,), infer=False).valid: return tuple([c]) @@ -175,9 +177,9 @@ class Decoder: for rule, boundary in self.color_bases(n): if rule.get('resolve'): for gap in (fractions.Fraction(), fractions.Fraction(1, 2), fractions.Fraction(1)): - if gap <= fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap']): + if gap <= fractions.Fraction(self.composition.data('harmony')['resolution_gap']): requests.append(('color', rule, boundary, n.end + gap, (-2, -1, 1, 2))) - delay = generator.vocabulary.data('lead_ins')['delayed_approach'] + delay = self.composition.data('lead_ins')['delayed_approach'] if n.end - n.start <= fractions.Fraction(delay['max_note']) and n.pitch.number() % 12 not in self.harmony.permitted(n.start, n.end): for arrival in map(fractions.Fraction, delay['arrivals']): if n.end <= n.start + arrival <= n.start + fractions.Fraction(delay['deadline']): @@ -219,7 +221,7 @@ class Decoder: if self.expressions_enabled and not self.harmony.ordinary(n): for events, ns, contracts in self.resolutions(n): add(events, ns, contracts) - rule = generator.vocabulary.data('lead_ins')['passing'] + rule = self.composition.data('lead_ins')['passing'] if duration <= fractions.Fraction(rule['max_note']): for step in rule['steps']: p = number + step @@ -235,8 +237,8 @@ class Decoder: c = generator.solo.types.ExpressionContract(n.event_id + ':passing', 'passing', rule['id'], (n,), target, self.harmony.at(end).chord) add(((time, end, number), (end, end + hold, p)), (n, target), (c, *extra)) if self.expressions_enabled: - limits = generator.vocabulary.data('roles')['difficulty'] - for rule in generator.vocabulary.data('lead_ins')['patterns']: + limits = self.composition.data('roles')['difficulty'] + for rule in self.composition.data('lead_ins')['patterns']: difficulty = rule.get('difficulty') if difficulty and self.analysis.tempo > limits[difficulty + '_max_bpm']: continue @@ -313,9 +315,9 @@ class Decoder: return None contracts = tuple(dataclasses.replace(c, preparation=state.notes[-1]) if c.kind in ('connection', 'passing') and c.preparation is None else c for c in contracts) if self.harmony.ordinary(state.notes[-1]): - _, error = generator.solo.expressions.validate((state.notes[-1], *notes), contracts, self.harmony) + _, error = generator.solo.expressions.validate((state.notes[-1], *notes), contracts, self.harmony, composition=self.composition) else: - _, error = generator.solo.expressions.validate((*state.notes, *notes), (*state.contracts, *contracts), self.harmony) + _, error = generator.solo.expressions.validate((*state.notes, *notes), (*state.contracts, *contracts), self.harmony, composition=self.composition) if error: return None history = state.history diff --git a/generator/solo/expressions.py b/generator/solo/expressions.py index 118f237..51758c0 100644 --- a/generator/solo/expressions.py +++ b/generator/solo/expressions.py @@ -1,13 +1,14 @@ """Typed, target-driven harmonic expression against written harmony and explicit musical relationships.""" from __future__ import annotations import dataclasses -import functools import fractions +import functools + import generator.harmony import generator.model import generator.pitch -import generator.vocabulary import generator.solo.types +import generator.vocabulary @functools.lru_cache(maxsize=512) def parsed_chord(symbol): @@ -90,8 +91,9 @@ def _eligible_color(rule, span, harmony): return False return True -def validate(notes, contracts, harmony): +def validate(notes, contracts, harmony, *, composition=None): """Return event permissions only after checking complete actual gestures.""" + composition = composition or generator.vocabulary.current() actual = {n.event_id: n for n in notes} approved = {} @@ -115,7 +117,7 @@ def validate(notes, contracts, harmony): if not previous or previous.chord.strip('()') == span.chord.strip('()'): return {}, c.id + ': anticipation requires a harmonic change' if c.kind in ('approach', 'enclosure', 'connection'): - rule = next((r for r in generator.vocabulary.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None) + rule = next((r for r in composition.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None) if not rule or len(c.events) != len(rule['semitones']): return {}, c.id + ': unknown approach formula' arrival = c.boundary if c.boundary is not None else target.start @@ -129,7 +131,7 @@ def validate(notes, contracts, harmony): return {}, c.id + ': invalid internal approach gaps' gap = fractions.Fraction(rule['gap']) cursor = target.start - gap - sum((fractions.Fraction(d) for d in rule['durations']), fractions.Fraction()) - sum(gaps, fractions.Fraction()) - if target.start - cursor > fractions.Fraction(generator.vocabulary.data('lead_ins')['max_approach']): + if target.start - cursor > fractions.Fraction(composition.data('lead_ins')['max_approach']): return {}, c.id + ': approach too long' for i, (n, step, duration) in enumerate(zip(c.events, rule['semitones'], rule['durations'])): if n.start != cursor or n.end != cursor + fractions.Fraction(duration) or n.pitch.number() != target.pitch.number() + step: @@ -144,7 +146,7 @@ def validate(notes, contracts, harmony): if c.kind == 'connection' and (not c.preparation or c.preparation.end != c.events[0].start or abs(c.preparation.pitch.number() - c.events[0].pitch.number()) > 2): return {}, c.id + ': chromatic connection lacks its incoming motion' elif c.kind == 'passing': - rule = generator.vocabulary.data('lead_ins')['passing'] + rule = composition.data('lead_ins')['passing'] if c.rule != rule['id'] or len(c.events) != 1 or not c.preparation: return {}, c.id + ': unsupported passing figure' n, = c.events @@ -160,7 +162,7 @@ def validate(notes, contracts, harmony): if not supported(before) or not supported(target): return {}, c.id + ': unsupported passing endpoints' elif c.kind == 'delayed_approach': - rule = generator.vocabulary.data('lead_ins').get('delayed_approach', {}) + rule = composition.data('lead_ins').get('delayed_approach', {}) if c.rule != rule.get('id') or len(c.events) != 1: return {}, c.id + ': unknown delayed approach' n, = c.events @@ -171,7 +173,7 @@ def validate(notes, contracts, harmony): if target.pitch.number() % 12 not in harmony.members(target.start, target.end) or target.pitch.number() % 12 not in harmony.permitted(target.start, target.end): return {}, c.id + ': invalid delayed destination' elif c.kind == 'color': - rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None) + rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None) if not rule: return {}, c.id + ': unknown color policy' colored = False @@ -193,10 +195,10 @@ def validate(notes, contracts, harmony): return {}, c.id + ': no interval expresses the declared color' last = c.events[-1] if rule.get('resolve'): - if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end): + if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(composition.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end): return {}, c.id + ': unresolved color' elif c.kind == 'suspension': - rule = next((r for r in generator.vocabulary.data('harmony').get('suspensions', []) if r['id'] == c.rule), None) + rule = next((r for r in composition.data('harmony').get('suspensions', []) if r['id'] == c.rule), None) if not rule or len(c.events) != 1 or c.boundary is None: return {}, c.id + ': unsupported suspension' held, = c.events @@ -211,23 +213,23 @@ def validate(notes, contracts, harmony): if held.pitch.number() % 12 not in harmony.permitted(held.start, held.end): return {}, c.id + ': suspension outside source-key/chord motion' elif c.kind == 'anticipated_color': - rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None) + rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None) if not rule or len(c.events) != 1 or c.boundary != span.start: return {}, c.id + ': unsupported anticipated color' n, = c.events - if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']): + if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']): return {}, c.id + ': invalid anticipated color interval' arrived = dataclasses.replace(n, start=c.boundary) target = arrived if c.target == n else c.target inner = generator.solo.types.ExpressionContract(c.id + ':arrived', 'color', rule['id'], (arrived,), target, span.chord) - _, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony) + _, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony, composition=composition) if error: return {}, error elif c.kind == 'anticipation': if c.rule not in ('early_chord_tone', 'early_reattack') or len(c.events) != 1 or span.start != c.boundary: return {}, c.id + ': unknown anticipation or non-boundary target' n, = c.events - if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']): + if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']): return {}, c.id + ': invalid anticipation interval' if c.rule == 'early_chord_tone' and (n != target or not c.boundary < n.end): return {}, c.id + ': invalid held anticipation' @@ -238,7 +240,7 @@ def validate(notes, contracts, harmony): elif c.kind == 'blues': if len(c.events) != 1: return {}, c.id + ': blues treatment requires one inflected event' - rule = next((r for r in generator.vocabulary.data('lead_ins')['blues'] if r['id'] == c.rule), None) + rule = next((r for r in composition.data('lead_ins')['blues'] if r['id'] == c.rule), None) n, = c.events chord = parsed_chord(span.chord) if not rule or n.end - n.start > fractions.Fraction(1, 2) or n.end != target.start or target.pitch.number() % 12 not in span.roles.basic: @@ -262,15 +264,16 @@ def validate(notes, contracts, harmony): approved[n.event_id] = c return approved, '' -def validate_frontier(notes, contracts, harmony, until, complete): +def validate_frontier(notes, contracts, harmony, until, complete, *, composition=None): """Validate heard prefixes and bounded future obligations separately. Future contract events are used ONLY to establish rule feasibility. They do not count as heard resolution; the contract stays pending until its entire target hold is present. Missing past events and expired deadlines fail. """ + composition = composition or generator.vocabulary.current() if complete: - approved, error = validate(notes, contracts, harmony) + approved, error = validate(notes, contracts, harmony, composition=composition) return approved, (), error actual = {n.event_id: n for n in notes} future = dict(actual) @@ -290,5 +293,5 @@ def validate_frontier(notes, contracts, harmony, until, complete): return {}, (), c.id + ': missing/changed event before resolution deadline' if incomplete: pending.append(c) - approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony) + approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony, composition=composition) return {key: c for key, c in approved.items() if key in actual}, tuple(pending), error diff --git a/generator/solo/harmonic.py b/generator/solo/harmonic.py index 5e71c4a..0581304 100644 --- a/generator/solo/harmonic.py +++ b/generator/solo/harmonic.py @@ -5,14 +5,16 @@ permission follows the actual sounded interval and validated resolution. """ from __future__ import annotations import dataclasses -import functools import fractions +import functools + import generator.harmony import generator.model -import generator.solo.types -import generator.solo.profiles import generator.solo.expressions +import generator.solo.profiles import generator.solo.recognition +import generator.solo.types +import generator.vocabulary @dataclasses.dataclass(frozen=True) class Roles: @@ -55,7 +57,8 @@ class Result: contracts: tuple = () class Harmony: - def __init__(self, spans, profile=None): + def __init__(self, spans, profile=None, *, composition=None): + self.composition = composition or generator.vocabulary.current() self.profile = profile or generator.solo.profiles.UNRESTRICTED self.source_spans = spans self.spans = [] @@ -112,14 +115,14 @@ class Harmony: expressions = tuple(dataclasses.replace(c, events=tuple(self.identified(n) for n in c.events), target=self.identified(c.target), preparation=self.identified(c.preparation) if c.preparation else None) for c in expressions) frontier = until if until is not None else max((n.end for n in notes), default=fractions.Fraction()) - permissions, pending, error = generator.solo.expressions.validate_frontier(notes, expressions, self, frontier, complete or until is None) + permissions, pending, error = generator.solo.expressions.validate_frontier(notes, expressions, self, frontier, complete or until is None, composition=self.composition) if error: return Result(False, 0, error=error) if infer and not pending and (complete or until is None) and any(not self.ordinary(n) and n.event_id not in permissions for n in notes): - expressions, error = generator.solo.recognition.recognize(notes, self, expressions) + expressions, error = generator.solo.recognition.recognize(notes, self, expressions, composition=self.composition) if error: return Result(False, 0, error=error) - permissions, pending, error = generator.solo.expressions.validate_frontier(notes, expressions, self, frontier, True) + permissions, pending, error = generator.solo.expressions.validate_frontier(notes, expressions, self, frontier, True, composition=self.composition) if error: return Result(False, 0, error=error) exposed = sounded = fractions.Fraction() diff --git a/generator/solo/profiles.py b/generator/solo/profiles.py index 551b350..2391e0d 100644 --- a/generator/solo/profiles.py +++ b/generator/solo/profiles.py @@ -2,11 +2,10 @@ from __future__ import annotations import dataclasses -import functools import fractions +import functools import generator.fretboard -import generator.vocabulary @dataclasses.dataclass(frozen=True) class Instrument: diff --git a/generator/solo/recognition.py b/generator/solo/recognition.py index 36997a7..60d3d45 100644 --- a/generator/solo/recognition.py +++ b/generator/solo/recognition.py @@ -5,9 +5,10 @@ Existing expression validators prove each candidate; deterministic set-cover search chooses a mutually consistent explanation, never edits the notes. """ import fractions -import generator.vocabulary + import generator.solo.expressions import generator.solo.types +import generator.vocabulary def signature(c): return (c.kind, c.rule, tuple(n.event_id for n in c.events), c.target.event_id, @@ -20,7 +21,8 @@ def merge(*groups): result.setdefault(signature(c), c) return tuple(result.values()) -def recognize(notes, harmony, provided=(), *, budget=4096): +def recognize(notes, harmony, provided=(), *, budget=4096, composition=None): + composition = composition or generator.vocabulary.current() required = {n.event_id for n in notes if not harmony.ordinary(n)} fixed = {n.event_id for c in provided for n in c.events} if required <= fixed: @@ -46,7 +48,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): if not (covered & required): return False combined = merge(provided, bundle) - _, error = generator.solo.expressions.validate(notes, combined, harmony) + _, error = generator.solo.expressions.validate(notes, combined, harmony, composition=composition) if error: return False candidates[tuple(signature(a) for a in bundle)] = (bundle, covered) @@ -57,11 +59,11 @@ def recognize(notes, harmony, provided=(), *, budget=4096): for i, n in enumerate(notes): if n.event_id not in required: continue - future = [t for t in notes[i + 1:] if n.end <= t.start <= n.end + fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap']) + future = [t for t in notes[i + 1:] if n.end <= t.start <= n.end + fractions.Fraction(composition.data('harmony')['resolution_gap']) and 0 < abs(t.pitch.number() - n.pitch.number()) <= 2] regions = [(None, harmony.at(n.start))] regions.extend((s.start, s) for s in harmony.spans if n.start < s.start < n.end - and s.start - n.start <= fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max'])) + and s.start - n.start <= fractions.Fraction(composition.data('harmony')['anticipation_max'])) for boundary, span in regions: if not span or span.chord == 'N.C.': continue @@ -69,7 +71,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): c = contract('anticipation', 'early_chord_tone', (n,), n, boundary) if add(c): single.setdefault(n.event_id, []).append((c,)) - for rule in generator.vocabulary.data('harmony')['colors']: + for rule in composition.data('harmony')['colors']: if not generator.solo.expressions.eligible_color(rule, span, harmony) and boundary is not None: continue kind = 'anticipated_color' if boundary is not None else 'color' @@ -85,7 +87,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): target = notes[i + 1] for s in harmony.spans: if n.start < s.start < n.end: - for rule in generator.vocabulary.data('harmony').get('suspensions', []): + for rule in composition.data('harmony').get('suspensions', []): add(contract('suspension', rule['id'], (n,), target, s.start)) elif s.start == n.end == target.start and n.pitch.number() == target.pitch.number(): add(contract('anticipation', 'early_reattack', (n,), target, s.start)) @@ -99,7 +101,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): targets.append((None, support)) elif c.kind == 'anticipation': targets.append((c.boundary, support)) - for rule in generator.vocabulary.data('lead_ins')['patterns']: + for rule in composition.data('lead_ins')['patterns']: size = len(rule['semitones']) if i < size: continue @@ -113,7 +115,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): add(contract(rule['kind'], rule['id'], ns, target, boundary, preparation), extras) # Delayed approaches may include real space or intervening valid music. # They still have an exact semitone target and a finite deadline. - rule = generator.vocabulary.data('lead_ins')['delayed_approach'] + rule = composition.data('lead_ins')['delayed_approach'] for n in reversed(notes[:i]): if target.start - n.start > fractions.Fraction(rule['deadline']): break @@ -124,8 +126,8 @@ def recognize(notes, harmony, provided=(), *, budget=4096): if n.event_id in required: for left in [(), *single.get(before.event_id, ())]: for right in [(), *single.get(target.event_id, ())]: - add(contract('passing', generator.vocabulary.data('lead_ins')['passing']['id'], (n,), target, preparation=before), merge(left, right)) - for rule in generator.vocabulary.data('lead_ins')['blues']: + add(contract('passing', composition.data('lead_ins')['passing']['id'], (n,), target, preparation=before), merge(left, right)) + for rule in composition.data('lead_ins')['blues']: add(contract('blues', rule['id'], (n,), target, preparation=before)) options = {identifier: [] for identifier in sorted(required - fixed, key=lambda identifier: (by_id[identifier].start, identifier))} for bundle, covered in candidates.values(): @@ -152,7 +154,7 @@ def recognize(notes, harmony, provided=(), *, budget=4096): identifier = min(remaining, key=lambda x: (len(options[x]), by_id[x].start, x)) for bundle, added in options[identifier]: combined = merge(chosen, bundle) - _, error = generator.solo.expressions.validate(notes, combined, harmony) + _, error = generator.solo.expressions.validate(notes, combined, harmony, composition=composition) if error: continue result = solve(combined, covered | added) diff --git a/generator/solo/report.py b/generator/solo/report.py index 2f14193..4922cb8 100644 --- a/generator/solo/report.py +++ b/generator/solo/report.py @@ -1,9 +1,10 @@ """Compact reports of actual selected phrases, not duplicated draft ancestry.""" -import dataclasses, fractions, json -import generator.solo.placement -import generator.solo.harmonic import collections -import generator.fretboard +import dataclasses, fractions, json + +import generator.solo.harmonic +import generator.solo.placement +import generator.vocabulary def plain(value): if dataclasses.is_dataclass(value): @@ -16,8 +17,9 @@ def plain(value): return str(value) return value -def structure(solo): - harmony = generator.solo.harmonic.Harmony(solo.harmony) +def structure(solo, *, composition=None): + composition = composition or generator.vocabulary.current() + harmony = generator.solo.harmonic.Harmony(solo.harmony, composition=composition) contracts = tuple(c for p in solo.phrases for c in p.expressions) checked = harmony.evaluate(solo.notes(), until=solo.end, complete=True, expressions=contracts) phrases = [] @@ -37,8 +39,9 @@ def structure(solo): 'harmonic_roles': harmony.describe(ns, expressions=contracts), 'harmonic_policy': 'contextual (shared by authored and predicted music)', 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'harmonic_exposure': checked.exposure, 'unresolved_treatments': plain(checked.pending)} -def describe(solo, measure_offset=0, time_offset=0, *, detail=None): - detail = detail or structure(solo) +def describe(solo, measure_offset=0, time_offset=0, *, detail=None, composition=None): + composition = composition or generator.vocabulary.current() + detail = detail or structure(solo, composition=composition) lines = [f'{solo.diagnostics.get("construction", "solo")}: {solo.part}', 'Times are exact quarter beats from the chorus start; negative times are allocated lead-ins. Pitches sound one octave below guitar notation.', 'Approximate fingering costs are not a biomechanical guarantee.', json.dumps(detail['phrasing'])] for p in solo.phrases: diff --git a/generator/solo/analysis.py b/generator/source.py similarity index 56% rename from generator/solo/analysis.py rename to generator/source.py index 5a5a868..777c282 100644 --- a/generator/solo/analysis.py +++ b/generator/source.py @@ -1,7 +1,14 @@ """Immutable source events, meter boundaries and tempo. No solo construction.""" from __future__ import annotations -import dataclasses, fractions -import generator.model, generator.score, generator.solo.types +import copy +import dataclasses +import fractions +import hashlib +import json + +import generator.model +import generator.score +import generator.solo.types @dataclasses.dataclass(frozen=True) class Analysis: @@ -43,3 +50,32 @@ def analyze(document): boundaries.append(boundaries[-1] + generator.model.fraction(m['duration'])) tempo = next((generator.model.fraction(t['bpm']) * generator.model.written_length(t['written']) for m in measures for t in m.get('tempos', []) if 'bpm' in t), fractions.Fraction(120)) return Analysis(tuple(boundaries), tempo, attacks(document), boundaries[generator.score.body_start_measure_index(measures)]) + +@dataclasses.dataclass(frozen=True) +class SourceAnalysis: + document_json: str + musical_key: str + @classmethod + def read(cls, document): + prepared = copy.deepcopy(document) + staff = generator.score.source_staff(prepared) + for i, m in enumerate(staff['measures']): + m['id'], m['number'] = f'source_{i}', str(i + 1) + staff['tuplets'] = generator.score.reidentify(staff['measures'], staff.get('tuplets', [])) + prepared['metadata'] = {k: v for k, v in prepared['metadata'].items() if k not in ('title', 'composer', 'copyright', 'software')} + prepared['metadata'].update(title='Source', composer='') + staff.update(id='source', name='Source') + melodic = analyze(prepared) + content = {'melody': [(str(n.start), str(n.end), dataclasses.asdict(n.pitch)) for n in melodic.melody], + 'measures': [{k: m.get(k) for k in ('duration', 'time_signature', 'key', 'harmonies', 'tempos', 'section_label', 'implicit')} for m in staff['measures']]} + # Fraction/string spelling is canonical and never includes filesystem IDs. + key = hashlib.sha256(json.dumps(content, sort_keys=True, default=str).encode()).hexdigest() + return cls(json.dumps(prepared, sort_keys=True), key) + def document(self): + return json.loads(self.document_json) + def accompaniment_key(self): + document = self.document() + document['metadata'].pop('tempo', None) + for measure in generator.score.source_staff(document)['measures']: + measure['tempos'] = [] + return SourceAnalysis.read(document).musical_key diff --git a/generator/summary.py b/generator/summary.py index c291aca..205d88c 100644 --- a/generator/summary.py +++ b/generator/summary.py @@ -6,6 +6,7 @@ import typing import generator.model import generator.score +import generator.score_io def _arranged_staff(document: dict[str, typing.Any]) -> dict[str, typing.Any]: staves = document.get("staves") @@ -61,7 +62,7 @@ def performance_document_summary(document: dict[str, typing.Any]) -> dict[str, t def performance_tree_summary(root: pathlib.Path) -> dict[str, typing.Any]: return { - path.name: performance_document_summary(generator.score.load_json(path)) + path.name: performance_document_summary(generator.score_io.load_json(path)) for path in sorted(root.glob("*.json")) } diff --git a/generator/train.py b/generator/train.py index 2be328a..bf6ed90 100644 --- a/generator/train.py +++ b/generator/train.py @@ -3,26 +3,25 @@ from __future__ import annotations import argparse import collections import dataclasses +import fractions import hashlib import json import os import pathlib +import shutil import tempfile import time -import fractions + +import generator.fretboard +import generator.harmonic_time import generator.harmony -import generator.model import generator.musicxml import generator.parts +import generator.solo.authored +import generator.solo.learned +import generator.source import generator.training_io import generator.vocabulary -import generator.harmonic_time -import generator.solo.analysis -import generator.solo.learned -import shutil -import generator.production -import generator.solo.authored -import generator.fretboard @dataclasses.dataclass(frozen=True) class Span: @@ -57,8 +56,8 @@ def load_examples(manifest): if not source.is_relative_to((manifest.parent.parent / 'songs').resolve()): raise ValueError('Training sources must be inside data/songs') document = generator.parts.prepare_document(generator.musicxml.load_musicxml(source)) - a = generator.solo.analysis.analyze(document) - pair = (generator.production.SourceAnalysis.read(document).musical_key, entry['slot']) + a = generator.source.analyze(document) + pair = (generator.source.SourceAnalysis.read(document).musical_key, entry['slot']) if pair in pairs: raise ValueError('Duplicate source music and solo slot') pairs.add(pair) diff --git a/generator/validation.py b/generator/validation.py new file mode 100644 index 0000000..11244df --- /dev/null +++ b/generator/validation.py @@ -0,0 +1,161 @@ +"""Score document validation, above pure musical values and instrument rules.""" +from __future__ import annotations +import fractions + +import generator.fretboard +import generator.model +import generator.pitch + +def validate_document(document: dict) -> None: + if not isinstance(document, dict) or not isinstance(document.get('metadata'), dict): + raise ValueError('score requires a metadata object') + if any(not isinstance(document['metadata'].get(name), str) for name in ('title', 'composer', 'key_signature', 'time_signature')): + raise ValueError('score metadata requires title, composer, key_signature and time_signature strings') + staves = document.get('staves', []) + if not isinstance(staves, list) or len(staves) != 1 or not isinstance(staves[0], dict): + raise ValueError('exactly one staff is currently supported') + if not isinstance(staves[0].get('measures'), list) or not staves[0]['measures']: + raise ValueError('score requires measures') + instruments = staves[0].get('instruments', []) + instrument_ids = {i['id'] for i in instruments} + if len(instruments) != len(instrument_ids) or any(not isinstance(i, str) or not i for i in instrument_ids): + raise ValueError('score instruments require unique nonempty IDs') + ids: set[str] = set() + events: dict[str, dict] = {} + tones: dict[str, tuple[dict, fractions.Fraction, fractions.Fraction, str]] = {} + ownership: dict[str, str] = {} + absolute = fractions.Fraction() + for measure in staves[0]['measures']: + if {'repeats', 'endings', 'initial_attributes'} & measure.keys(): + raise ValueError('repeat notation is unsupported; regenerate scores from music written out in reading order') + length = generator.model.fraction(measure['duration']) + if length <= 0: + raise ValueError('measure duration must be positive') + voices = measure.get('voices', []) + if not voices or len({v['id'] for v in voices}) != len(voices) or any(not v['id'] for v in voices): + raise ValueError('measure requires uniquely identified voice lanes') + previous_ends = {v['id']: fractions.Fraction() for v in voices} + for voice, note in ((v, n) for v in voices for n in v['notes']): + if any({'sound_ties', 'printed_ties'} & pitch.keys() for pitch in note['pitches']): + raise ValueError('pass-specific tie metadata is unsupported; regenerate scores') + if not note['id'] or note['id'] in ids: + raise ValueError(f'duplicate event ID: {note["id"]}') + ids.add(note['id']) + events[note['id']] = note + ownership[note['id']] = voice['id'] + if note.get('voice', voice['id']) != voice['id']: + raise ValueError('event voice ownership disagrees with its lane') + onset, duration = generator.model.fraction(note['onset']), generator.model.fraction(note['duration']) + if onset < 0 or duration < 0 or onset + duration > length: + raise ValueError(f'event outside measure: {note["id"]}') + if note['kind'] not in ('note', 'rest', 'spacer'): + raise ValueError('invalid event kind') + if not note.get('grace'): + if duration == 0 or onset < previous_ends[voice['id']]: + raise ValueError('overlapping events in one rhythmic voice') + previous_ends[voice['id']] = onset + duration + elif duration != 0: + raise ValueError('grace notes must not advance metric time') + expected = generator.model.metric_length(note['written']) + if not note.get('grace') and not note.get('measure_rest') and duration != expected: + raise ValueError('metric and written event durations disagree') + if note['kind'] != 'note' and note.get('pitches'): + raise ValueError('rests and spacers cannot contain pitches') + if note['kind'] == 'note' and not note.get('pitches') and note.get('notehead') != 'slash': + raise ValueError('pitched event requires at least one pitch') + for p in note.get('pitches', []): + if p.get('instrument') and p['instrument'] not in instrument_ids: + raise ValueError('note refers to an unknown score instrument') + if 'unpitched' in p: + position = p['unpitched'] + if position['letter'] not in generator.model.LETTERS or type(position['octave']) is not int: + raise ValueError('invalid unpitched display position') + else: + generator.model.pitch_number(p) + if staves[0].get('tablature'): + tab = p.get('tablature', {}) + string, fret = tab.get('string'), tab.get('fret') + if type(string) is not int or type(fret) is not int or string not in dict(generator.fretboard.STANDARD_TUNING) or not generator.fretboard.MIN_FRET <= fret <= generator.fretboard.MAX_FRET: + raise ValueError(f'{p["id"]}: invalid generated tablature position') + state = dict(measure) + for change in measure.get('attributes', []): + if generator.model.fraction(change['onset']) <= onset: + state.update(change) + if dict(generator.fretboard.STANDARD_TUNING)[string] + fret != generator.pitch.sounding_pitch(p, state): + raise ValueError(f'{p["id"]}: tablature disagrees with sounding pitch') + pd = generator.model.fraction(p['duration']) + if pd < 0 or (pd == 0 and not note.get('grace')) or pd > duration or not p['id'] or p['id'] in tones: + raise ValueError('invalid chord-tone duration or duplicate ID') + if not note.get('grace') and pd != generator.model.metric_length(p['written']): + raise ValueError('metric and written chord-tone durations disagree') + tones[p['id']] = (p, absolute + onset, pd, voice['id']) + for harmony in measure.get('harmonies', []): + if not 0 <= generator.model.fraction(harmony['onset']) < length: + raise ValueError('harmony outside measure') + for change in measure.get('attributes', []): + if not 0 <= generator.model.fraction(change['onset']) < length: + raise ValueError('attribute change outside measure') + for state in [measure, *measure.get('attributes', [])]: + if 'meter' in state: + generator.model.validate_meter(state['meter']) + if 'key' in state: + if len(state['key']['alterations']) != 7: + raise ValueError('key alterations must contain seven exact alterations') + for value in state['key']['alterations']: + generator.model.alteration(value) + if 'clef' in state and (state['clef']['sign'] not in ('G', 'F', 'C', 'percussion', 'none') or not 1 <= state['clef']['line'] <= 6 or not -3 <= state['clef']['octave_change'] <= 3): + raise ValueError('invalid clef') + absolute += length + for identifier, (p, start, duration, voice) in tones.items(): + if target := p.get('tie_to'): + if target not in tones: + raise ValueError(f'dangling tie: {identifier} -> {target}') + other, next_start, _, next_voice = tones[target] + same_pitch = all(p.get(k) == other.get(k) for k in ('unpitched', 'instrument')) if 'unpitched' in p else all(p.get(k) == other.get(k) for k in ('letter', 'alter', 'octave')) + if other.get('tie_from') != identifier or not same_pitch or next_voice != voice or next_start != start + duration: + raise ValueError(f'invalid tie: {identifier} -> {target}') + if staves[0].get('tablature') and p['tablature'] != other['tablature']: + raise ValueError(f'{identifier}: tied guitar tones must retain their string and fret') + if source := p.get('tie_from'): + if source not in tones or tones[source][0].get('tie_to') != identifier: + raise ValueError(f'dangling incoming tie: {identifier}') + span_ids = set() + event_order = {identifier: index for index, identifier in enumerate(events)} + for span in staves[0].get('tuplets', []): + if not span['id'] or span['id'] in span_ids or span['start'] not in ids or span['end'] not in ids or not 0 < span['note_count'] <= 1000000 or not 0 < span['space_count'] <= 1000000 or not 0 <= span['depth'] <= 16: + raise ValueError('invalid tuplet span') + members = [n['id'] for n in events.values() if span['id'] in n.get('tuplet_ids', [])] + if not members or members[0] != span['start'] or members[-1] != span['end'] or event_order[span['start']] > event_order[span['end']]: + raise ValueError('tuplet endpoints disagree with member events') + if len({ownership[n] for n in members}) != 1 or span.get('voice', ownership[members[0]]) != ownership[members[0]]: + raise ValueError('tuplet crosses independent voice lanes') + if span.get('bracket', 'auto') not in ('yes', 'no', 'auto') or span.get('show_number', 'actual') not in ('none', 'actual', 'both'): + raise ValueError('invalid tuplet appearance') + span_ids.add(span['id']) + for note in events.values(): + for verse in note.get('lyrics', []): + start = tones[note['pitches'][0]['id']][1] if note['pitches'] else fractions.Fraction() + if 'extend_to' in verse and not start < generator.model.fraction(verse['extend_to']) <= absolute: + raise ValueError('lyric extension endpoint is outside the score') + if any(t not in span_ids for t in note.get('tuplet_ids', [])): + raise ValueError('event refers to an unknown tuplet') + if tremolo := note.get('tremolo'): + if tremolo['type'] not in ('single', 'start', 'stop') or type(tremolo['marks']) is not int or not 0 <= tremolo['marks'] <= 8: + raise ValueError('invalid measured tremolo') + if tremolo['type'] in ('start', 'stop'): + other = events.get(tremolo.get('partner')) + expected_type = 'stop' if tremolo['type'] == 'start' else 'start' + if not other or other.get('tremolo', {}).get('partner') != note['id'] or other['tremolo']['type'] != expected_type or other['tremolo']['marks'] != tremolo['marks']: + raise ValueError('invalid paired tremolo') + for span in staves[0].get('octave_shifts', []): + if not 0 <= generator.model.fraction(span['start']) < generator.model.fraction(span['end']) <= absolute or not -3 <= span['octaves'] <= 3: + raise ValueError('invalid octave-shift span') + previous_end = fractions.Fraction() + for span in staves[0].get('scale_spans', []): + start, end = generator.model.fraction(span['start']), generator.model.fraction(span['end']) + if not previous_end <= start < end <= absolute or not isinstance(span.get('label'), str) or not span['label'].strip() or type(span.get('matched')) is not bool: + raise ValueError('invalid solo scale span') + if not isinstance(span.get('id'), str) or not span['id'] or span['id'] in ids: + raise ValueError('solo scale spans require unique IDs') + ids.add(span['id']) + previous_end = end diff --git a/generator/vocabulary.py b/generator/vocabulary.py index d40ba62..263e92a 100644 --- a/generator/vocabulary.py +++ b/generator/vocabulary.py @@ -2,12 +2,14 @@ from __future__ import annotations import contextlib import contextvars +import copy import dataclasses +import fractions import functools import hashlib import json import pathlib -import fractions +import types ROOT = pathlib.Path(__file__).resolve().parents[1] / 'data' / 'composition' ACTIVE = contextvars.ContextVar('composition_vocabulary', default=None) @@ -16,7 +18,7 @@ def load(root=ROOT): # Content, not just the directory, keys memoization. Long-lived individual # role builds must observe edited production data without restarting Python. names = ('roles', 'harmony', 'lead_ins', 'accompaniment') - return _load(tuple((name, (pathlib.Path(root) / (name + '.json')).read_text()) for name in names)) + return copy.deepcopy(_load(tuple((name, (pathlib.Path(root) / (name + '.json')).read_text()) for name in names))) @functools.lru_cache(maxsize=16) def _load(contents): @@ -105,26 +107,54 @@ def _load(contents): @functools.lru_cache(maxsize=1) def _default(): - return {'data': load(), 'role': '', 'settings': {}} + return CompositionContext.create(load()) + +def _freeze(value): + if isinstance(value, dict): + return types.MappingProxyType({key: _freeze(item) for key, item in value.items()}) + if isinstance(value, list): + return tuple(_freeze(item) for item in value) + return value + +@functools.lru_cache(maxsize=32) +def _frozen_json(contents): + return _freeze(json.loads(contents)) + +@dataclasses.dataclass(frozen=True) +class CompositionContext: + """Owned, immutable, pickleable policy inputs with stable iteration order.""" + resources_json: str + name: str = '' + settings_json: str = '{}' + @classmethod + def create(cls, resources, name='', settings=None): + return cls(json.dumps(resources), name, json.dumps(settings or {})) + def data(self, name): + return _frozen_json(self.resources_json)[name] + def role(self, name=None): + return {**self.data('roles')['roles'].get(name or self.name, {}), **_frozen_json(self.settings_json)} + def accompaniment(self, part): + value = self.data('accompaniment') + return {**{key: item for key, item in value.items() if key != 'instruments'}, **value['instruments'][part]} def current(): return ACTIVE.get() or _default() def data(name): - return current()['data'][name] + return current().data(name) def accompaniment(part, resources=None): - value = (resources or current()['data'])['accompaniment'] + if resources is None: + return current().accompaniment(part) + value = resources['accompaniment'] return {**{k: v for k, v in value.items() if k != 'instruments'}, **value['instruments'][part]} def role(name=None): - active = current() - name = name or active['role'] - return {**active['data']['roles']['roles'].get(name, {}), **active['settings']} + return current().role(name) @contextlib.contextmanager def using(resources, name, settings=None): - token = ACTIVE.set({'data': resources, 'role': name, 'settings': settings or {}}) + token = ACTIVE.set(CompositionContext.create(resources, name, settings)) try: yield finally: diff --git a/include/vamp_zero/score_loader.hpp b/include/vamp_zero/score_loader.hpp index 423317f..6817b5a 100644 --- a/include/vamp_zero/score_loader.hpp +++ b/include/vamp_zero/score_loader.hpp @@ -25,7 +25,8 @@ public: // An already-running completion may overlap stop; capture its consumer weakly. score_loader(boost::asio::any_io_executor executor, std::function>(vamp_zero::score_reference)> load, - std::function completed = {}); + std::function completed = {}, + std::optional worker = {}); ~score_loader(); score_loader(score_loader const &) = delete; auto operator=(score_loader const &) -> score_loader & = delete; diff --git a/include/vamp_zero/ui.hpp b/include/vamp_zero/ui.hpp index bbfb3e6..3db12cb 100644 --- a/include/vamp_zero/ui.hpp +++ b/include/vamp_zero/ui.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -24,7 +25,7 @@ class score_loader; class zero_ui { public: - zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::database_config database, std::string assets_path, std::function command_handler = {}, std::function menu_handler = {}); + zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::database_config database, std::string assets_path, std::function command_handler = {}, std::function menu_handler = {}, std::optional score_worker = {}); ~zero_ui(); zero_ui(zero_ui const &) = delete; auto operator=(zero_ui const &) -> zero_ui & = delete; diff --git a/src/vamp_zero/app.cpp b/src/vamp_zero/app.cpp index afac652..b638395 100644 --- a/src/vamp_zero/app.cpp +++ b/src/vamp_zero/app.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -322,6 +323,7 @@ private: auto run_headless(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected { auto io = boost::asio::io_context{}; + auto score_worker = boost::asio::thread_pool{1}; auto endpoint = make_server_endpoint(config); if (!endpoint) { return std::unexpected(endpoint.error()); @@ -335,7 +337,7 @@ auto run_headless(vamp_zero::config const &config, vamp_zero::control_state &sta if (audio != nullptr) { audio->sync(state.snapshot()); } - auto loader = std::make_shared(io.get_executor(), [executor = io.get_executor(), database = config.database, assets = config.runtime.assets_path](vamp_zero::score_reference reference) { return vamp_zero::async_load_score(executor, database, std::move(reference), assets); }); + auto loader = std::make_shared(io.get_executor(), [executor = score_worker.get_executor(), database = config.database, assets = config.runtime.assets_path](vamp_zero::score_reference reference) { return vamp_zero::async_load_score(executor, database, std::move(reference), assets); }, std::function{}, score_worker.get_executor()); auto playback = playback_poll{io, state, audio, loader, config.runtime.assets_path, true}; auto heartbeat = write_heartbeat(config); if (!heartbeat) { @@ -354,11 +356,12 @@ auto run_headless(vamp_zero::config const &config, vamp_zero::control_state &sta auto run_display(vamp_zero::config const &config, vamp_zero::control_state &state, vamp_zero::audio_controller *audio) -> std::expected { auto io = boost::asio::io_context{}; + auto score_worker = boost::asio::thread_pool{1}; auto endpoint = make_server_endpoint(config); if (!endpoint) { return std::unexpected(endpoint.error()); } - auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, config.database, config.runtime.assets_path, [&state](vamp_protocol::page_command command) { static_cast(state.apply(command)); }, [&state](vamp_zero::menu_action action) { static_cast(state.apply(action)); }}; + auto ui = vamp_zero::zero_ui{io.get_executor(), config.display, config.database, config.runtime.assets_path, [&state](vamp_protocol::page_command command) { static_cast(state.apply(command)); }, [&state](vamp_zero::menu_action action) { static_cast(state.apply(action)); }, score_worker.get_executor()}; state.set_observer([&ui](vamp_zero::app_state app) { auto status = ui.publish(std::move(app)); if (!status) { diff --git a/src/vamp_zero/score_loader.cpp b/src/vamp_zero/score_loader.cpp index 0f54dce..455f84d 100644 --- a/src/vamp_zero/score_loader.cpp +++ b/src/vamp_zero/score_loader.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -13,8 +14,8 @@ struct vamp_zero::score_loader::state : std::enable_shared_from_this { - state(boost::asio::any_io_executor executor, std::function>(vamp_zero::score_reference)> load, std::function completed) - : executor{std::move(executor)}, load{std::move(load)}, completed{std::move(completed)} {} + state(boost::asio::any_io_executor executor, std::function>(vamp_zero::score_reference)> load, std::function completed, std::optional worker) + : executor{executor}, worker{worker.value_or(executor)}, load{std::move(load)}, completed{std::move(completed)} {} static auto run(std::weak_ptr weak, std::uint64_t generation, std::function>(vamp_zero::score_reference)> load, vamp_zero::score_reference reference) -> boost::asio::awaitable> { { auto self = weak.lock(); @@ -63,15 +64,16 @@ struct vamp_zero::score_loader::state : std::enable_shared_from_this { auto start() -> void { in_flight = true; // post is required: co_spawn can enter inline on an already-running executor. - boost::asio::post(executor, [weak = weak_from_this(), generation = generation, load = load, - reference = *reference, executor = executor]() mutable { - boost::asio::co_spawn(executor, run(weak, generation, std::move(load), std::move(reference)), - [weak, generation](std::exception_ptr exception, std::expected loaded) { + boost::asio::post(worker, [weak = weak_from_this(), generation = generation, load = load, + reference = *reference, executor = executor, worker = worker]() mutable { + boost::asio::co_spawn(worker, run(weak, generation, std::move(load), std::move(reference)), + boost::asio::bind_executor(executor, [weak, generation](std::exception_ptr exception, std::expected loaded) { finish(weak, generation, exception, std::move(loaded)); - }); + })); }); } boost::asio::any_io_executor executor; + boost::asio::any_io_executor worker; std::function>(vamp_zero::score_reference)> load; std::function completed; std::mutex mutex{}; @@ -83,8 +85,8 @@ struct vamp_zero::score_loader::state : std::enable_shared_from_this { std::bool_t stopped{}; }; -vamp_zero::score_loader::score_loader(boost::asio::any_io_executor executor, std::function>(vamp_zero::score_reference)> load, std::function completed) - : state_{std::make_shared(std::move(executor), std::move(load), std::move(completed))} {} +vamp_zero::score_loader::score_loader(boost::asio::any_io_executor executor, std::function>(vamp_zero::score_reference)> load, std::function completed, std::optional worker) + : state_{std::make_shared(std::move(executor), std::move(load), std::move(completed), std::move(worker))} {} vamp_zero::score_loader::~score_loader() { stop(); diff --git a/src/vamp_zero/ui.cpp b/src/vamp_zero/ui.cpp index 851dcc3..66a8049 100644 --- a/src/vamp_zero/ui.cpp +++ b/src/vamp_zero/ui.cpp @@ -184,13 +184,13 @@ private: } -vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::database_config database, std::string assets_path, std::function command_handler, std::function menu_handler) +vamp_zero::zero_ui::zero_ui(boost::asio::any_io_executor executor, luma::application_config config, vamp_zero::database_config database, std::string assets_path, std::function command_handler, std::function menu_handler, std::optional score_worker) : application_{std::make_shared(std::move(config))}, runtime_{std::make_shared(executor, std::move(command_handler), std::move(menu_handler))}, - score_loader_{std::make_shared(executor, [executor, database = std::move(database), assets = std::move(assets_path)](vamp_zero::score_reference reference) { return vamp_zero::async_load_score(executor, database, std::move(reference), assets); }, [weak = std::weak_ptr{application_}] { + score_loader_{std::make_shared(executor, [executor = score_worker.value_or(executor), database = std::move(database), assets = std::move(assets_path)](vamp_zero::score_reference reference) { return vamp_zero::async_load_score(executor, database, std::move(reference), assets); }, [weak = std::weak_ptr{application_}] { if (auto application = weak.lock()) { static_cast(application->request_layout()); - } })} {} + } }, score_worker)} {} vamp_zero::zero_ui::~zero_ui() { stop(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 074d7f2..6eb3693 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,6 +61,11 @@ add_test( # Complete deterministic phrase searches make this suite CPU-sensitive; # leave headroom for concurrent asset generation on development machines. set_tests_properties(product_vamp_generator PROPERTIES TIMEOUT 600) +add_test( + NAME product_vamp_assets + COMMAND ${VAMP_PYTHON_COMMAND} -m tests.asset_contract ${CMAKE_BINARY_DIR}/generated_assets + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} +) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} diff --git a/tests/asset_contract.py b/tests/asset_contract.py new file mode 100644 index 0000000..08ac38d --- /dev/null +++ b/tests/asset_contract.py @@ -0,0 +1,17 @@ +"""Compare complete generated assets with the reviewed, pinned output baseline.""" +import hashlib +import json +import pathlib +import sys + +def verify(root): + expected = json.loads(pathlib.Path(__file__).with_name('fixtures').joinpath('assets_manifest.json').read_text()) + actual = {path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.rglob('*')) if path.is_file() and path.relative_to(root).parts[0] != 'analysis'} + changed = sorted(name for name in expected.keys() | actual.keys() if expected.get(name) != actual.get(name)) + if changed: + raise AssertionError('Generated assets differ from the reviewed baseline:\n' + '\n'.join(changed)) + print(f'All {len(expected)} generated assets match the reviewed baseline.') + +if __name__ == '__main__': + verify(pathlib.Path(sys.argv[1])) diff --git a/tests/fixtures/assets_manifest.json b/tests/fixtures/assets_manifest.json new file mode 100644 index 0000000..e80b7a0 --- /dev/null +++ b/tests/fixtures/assets_manifest.json @@ -0,0 +1,411 @@ +{ + "backing/all_of_me_120.wav": "5104097be1d4ed0459dacedb61c2509effcfe0e26025b24574291a3467962f3d", + "backing/autumn_leaves_120.wav": "990e0218f9a09161a7a8a255bd1e64daf9fd01ac41935faa273228906775d016", + "catalog.db": "0891794877e06d201e1e149ea82f7bfdc880baf35c78a29b7b724f6c0ecce1e9", + "fonts/petaluma/OFL.txt": "5789e74d38031e93a4fbdb0a72392d1fd58cf15c29da3efa695c5099433ad0ce", + "fonts/petaluma/PetalumaScript.otf": "3b4be972cc9f857a7c1019ff49cd07cb083d5cc99058c0fb8563897fc75c32c8", + "pico/titles/42076b2eaa61a6fb408eeaec133b130967cf2c32981a9444dde6903ed7771fc8.mask": "42076b2eaa61a6fb408eeaec133b130967cf2c32981a9444dde6903ed7771fc8", + "pico/titles/8ed7df6668e9e4a24c148ba0fe9ac57209e504ca6a7e8a9b29179c097076ecae.mask": "8ed7df6668e9e4a24c148ba0fe9ac57209e504ca6a7e8a9b29179c097076ecae", + "print/all_of_me_acoustic.pdf": "aff0697fa82bde5283f705395028a3e4f6e87d4e0f1eab271dea9cffdb6c72e2", + "print/all_of_me_electric.pdf": "1185ece1e8f507d6fd7046e9d78fcb28b9e0a667c1027304abcb13c8e0519b97", + "print/all_of_me_vocal.pdf": "0ff78cb9d315dff93e7a88209610e959fa804c480cf63a8c4fb018a17c46ff66", + "print/autumn_leaves_acoustic.pdf": "badafe4d5484ab7e8a7c94633174d2233233436318fcf62bfcc043f624d097d6", + "print/autumn_leaves_electric.pdf": "a7416f9c4390072f74736024162172340d273de5108681bdf6543c9b8fa69d60", + "print/autumn_leaves_vocal.pdf": "78cc66dd27c7088e196f917eb752266989de13c51361692195b6bb9765210001", + "songs/all_of_me_acoustic.json": "b5f9f3781a79a351a59bbb54a333cbb1dac9e21c46a33c4296d6baef0ed8ff41", + "songs/all_of_me_acoustic.pages/0001.png": "165e827c3a05c1ee2aa1e5fd41c9dd2b273dac04e067df6d3e136f16fd2255a1", + "songs/all_of_me_acoustic.pages/0002.png": "b8738a70ad3d32911a8147ff18e62e4aa26651277562c348cb28f9180f045888", + "songs/all_of_me_acoustic.pages/0003.png": "af9631bdb1736de168527ac7a881de5c7b9fa1fe532aa10bffe4b4419cb67297", + "songs/all_of_me_acoustic.pages/0004.png": "8b2fdc7a064945bdfafb77b9a5f87313a86ef759a87daedb861cd3822dd2fdd2", + "songs/all_of_me_acoustic.pages/0005.png": "c11b85ac3affe9cee9184c126dd9cbffea5ef01d5f238440f831ce80b477f09b", + "songs/all_of_me_acoustic.pages/0006.png": "28bcdd62274aa79a6626935279dd88a9c85223f9ee3e4e7e914cd2ff56c704b6", + "songs/all_of_me_acoustic.pages/0007.png": "bfe462e693c5eb849b53463b2d64ba201e11196f64d5ec94e7d80a00f437d95f", + "songs/all_of_me_acoustic.pages/0008.png": "a7fa969178c12295a31429289b6d7ec273cc6e388343c174793d503856084c2c", + "songs/all_of_me_acoustic.pages/0009.png": "19a2fb51af8e4e09dafcba21b4e2277052125bb34222d2b1638a75582266bcea", + "songs/all_of_me_acoustic.pages/0010.png": "d423c4a0ae3c9d5e626bf8a633b202501ea5768c83ddbf68a6df8265d2a88315", + "songs/all_of_me_acoustic.pages/0011.png": "5e21353bfac521f66b49b6cd4e0dc00c4e85c27bac41d69809b8f88f3e9be310", + "songs/all_of_me_acoustic.pages/0012.png": "c36c7a82d1900f5394b1af3ab0bf663055e2bd7a1788fd60f416d7ec17902c7a", + "songs/all_of_me_acoustic.pages/0013.png": "063ba3b58613d9c383e379b0c6fa9b2d8b620cff5ff4def145fca918fa80f5e0", + "songs/all_of_me_acoustic.pages/0014.png": "ae3622cc934dbc126bff80f02bda6159edabd5044b5fc6c2448161652dc983ad", + "songs/all_of_me_acoustic.pages/0015.png": "99acae22eadaacd195fd986ae31f10c32800972e4d164f82cb5151ee5691c030", + "songs/all_of_me_acoustic.pages/0016.png": "c73a7111028d2e875ed441c54f9299b93947f53738e1815cb463d8a84fb7ef19", + "songs/all_of_me_acoustic.pages/0017.png": "6fa69caf6a91a38b3da2c57c146f4e79be712237b54910afe22d504cb7b46004", + "songs/all_of_me_acoustic.pages/0018.png": "f92e147f22c688b8936a7f2589b8ad6b7337e7b0aa9d9136641cff269623945b", + "songs/all_of_me_acoustic.pages/0019.png": "de5094eb5c2809146af4dc5470d58ab8ae70feb138d6fca83aa837aeb5bacc36", + "songs/all_of_me_acoustic.pages/0020.png": "d7c14a96a210c03918ad1c42240b34b27baf3412d03a443a0e51ffc631b27569", + "songs/all_of_me_acoustic.pages/0021.png": "81435e1c822309d48cd80a454a468e595f98684b8fc13e9aab61e7f9efbc8326", + "songs/all_of_me_acoustic.pages/0022.png": "78e7d1cc5b05ea1dd59b396d3554ee6b335b30d4adfc92d9da7deb80e420865c", + "songs/all_of_me_acoustic.pages/0023.png": "ed8a533012b886e46430d25b21d2a7e6ff026399f1f04a6561d8b8c465e7edf8", + "songs/all_of_me_acoustic.pages/0024.png": "d1ef3d847cc874f50fe1f7e84acba316431a56ef04101371c5eeefb38e03f54d", + "songs/all_of_me_acoustic.pages/0025.png": "6c22754c4af52c0bc1b986a9bb40d2611eaf19b891686d4fdd7082a7b37ae700", + "songs/all_of_me_acoustic.pages/0026.png": "f7433c965a235e41952d8d259c9aea0630792fe0470360208eba22d50ca2bc12", + "songs/all_of_me_acoustic.pages/0027.png": "2a7f5cb133d68b538f4ffb6e99a531a0416dcc62b8b35b9fdd002ada3277474a", + "songs/all_of_me_acoustic.pages/0028.png": "7c5a369f5dc4acef1bcc270543d6d8075a7066ceafa1cf2fabeca0fb78bcd9f7", + "songs/all_of_me_acoustic.pages/0029.png": "b58871c97069f13a3d5af60473af5aa5fb081e3373424b7761ca6f312f06e45b", + "songs/all_of_me_acoustic.pages/playback-0002.png": "89275aa531c7787a33b1450a8e84afbdb7d952a42f0b2105b58fca57243ed8d6", + "songs/all_of_me_acoustic.pages/playback-0003.png": "838b2d12000f748c0fd102de50eaea0ca3950cef07c051c6657d81f628169297", + "songs/all_of_me_acoustic.pages/playback-0004.png": "439e1856d32875c83d8d156bc9aa9f11bc493564bb342cf99044bf578980e25e", + "songs/all_of_me_acoustic.pages/playback-0005.png": "7789a952684a77309e19ca867a5500928fc96f67f5f82f371b346c9153778e96", + "songs/all_of_me_acoustic.pages/playback-0006.png": "ced45cec6c95fb0616552be40cf0543f9ab730ba0cf2414e72b156c12a74a9bc", + "songs/all_of_me_acoustic.pages/playback-0007.png": "2ed7cca6a962fead8ffcc95361aa3549e664038d99be1ec3d258ed51e917a53d", + "songs/all_of_me_acoustic.pages/playback-0008.png": "3efcd0671de4d386b189181ee7ebe03d4c50eddcfa9080c07a99c4642d4d58aa", + "songs/all_of_me_acoustic.pages/playback-0009.png": "cc4dc2ccfccda12074d69452e8e0427d40e6a0c9fb3b3da8308ab3b84240dbb2", + "songs/all_of_me_acoustic.pages/playback-0010.png": "fca310b66181635a06f6da5150a7e6ea13ce169aae94fe722139c371f5bbe1b6", + "songs/all_of_me_acoustic.pages/playback-0011.png": "dc92c989d193387eb5cc690b8274e57326eea652fa18688e37d405ca69cef051", + "songs/all_of_me_acoustic.pages/playback-0012.png": "7043e31820e325d716e7fa77992d4e416a7c9b8cf6391fc87e71ae6c685a75f8", + "songs/all_of_me_acoustic.pages/playback-0013.png": "67fbc90a1d4c3b2455bae0d0fb8f035430ad8cfed8f45a3a87988116cad5aeea", + "songs/all_of_me_acoustic.pages/playback-0014.png": "93561857be092664c798e2dd04cc6c15284c2a2e1759a056a978242f3b86e3ab", + "songs/all_of_me_acoustic.pages/playback-0015.png": "1ae5794742afae872c34f3bf1d21f3f6276d8ede4cca737ee5b58bc0a6c4b627", + "songs/all_of_me_acoustic.pages/playback-0016.png": "99274eea14c17d69f7fa940cc2dd14148e1406f4b3ed37fbabe72e1edcbc09b9", + "songs/all_of_me_acoustic.pages/playback-0017.png": "ac38a8f3c94e7a331516c90a46daa435f5c820a7db9527895dfccb2c4f2d4b3d", + "songs/all_of_me_acoustic.pages/playback-0018.png": "02c1308be295df3a8e1464e8809acbf9fb283d5e0bc400d30c99cba6f383f4d4", + "songs/all_of_me_acoustic.pages/playback-0019.png": "b009489f22aea8f68c8ad4ae0e09f7244c75e330245032ffe31bcf4812bf14b3", + "songs/all_of_me_acoustic.pages/playback-0020.png": "5a57c46ec799b8588ac42d7cf5bc98d5597302bde4d75745be343428cd4cbf74", + "songs/all_of_me_acoustic.pages/playback-0021.png": "184d0d6fbf9864dd2e3ab2a947eb21fa0ba6ef8ffc19f4ff4e05902b640c48d9", + "songs/all_of_me_acoustic.pages/playback-0022.png": "d4edcc5eb21d4b1fd97110a4e063cd1997f9f490012ea15f54f2a54ff71f0533", + "songs/all_of_me_acoustic.pages/playback-0023.png": "4fc5208032155c47172d7868529876b798596f73c9cca31bf68f4d7a63fa5660", + "songs/all_of_me_acoustic.pages/playback-0024.png": "5451f8652c7b7966d20db24fc07229dbc8839b63b04f318a7682c63e04216287", + "songs/all_of_me_acoustic.pages/playback-0025.png": "2b607bd202d85aef6d9b87d51a0cf16c6d15a14ab00ed4e894be7bdd9e5cfb77", + "songs/all_of_me_acoustic.pages/playback-0026.png": "a741f9177b792d6392eeaa1dc098df7f503e3a829fc862dabf0fc89a2334d6f1", + "songs/all_of_me_acoustic.pages/playback-0027.png": "2bf42f6fc15e0941c54f9f098f4983f2f45d9a7567eaa604a6b6319c0ce31c19", + "songs/all_of_me_acoustic.pages/playback-0028.png": "bead93f269e3ea6a91a0e71a03eb3e386c8a0f972034e7e3e23cf8807de2e589", + "songs/all_of_me_acoustic.pages/playback-0029.png": "196dd7c0f49190342cbcf4e88267462ea4fd353949f1a66fba0d129349579f85", + "songs/all_of_me_acoustic.pages/playback-0030.png": "339afb9ccf4e5fc0b18205aa3154ec0494d873494245f14512d1c77e7acdb3f2", + "songs/all_of_me_acoustic.pages/playback-0031.png": "f30c7d9824cf9eee91050879ee1b406e608d47461fa5de720639ce060ac10368", + "songs/all_of_me_acoustic.pages/playback-0032.png": "421170df4fdb0b746077ba2959ff33480c05edfaffc2505e7ebcf51a9aecf2ca", + "songs/all_of_me_acoustic.pages/playback-0033.png": "2af68de5667ca9b8d738703c562c0338e8c2fb5face892d07eb2894cb7b8e6c8", + "songs/all_of_me_acoustic.pages/playback-0034.png": "da78e34c05b7527c636124a349a38a95201173ca59d5778a88b7ad19fd458973", + "songs/all_of_me_acoustic.pages/playback-0035.png": "d3b699e968ef04d32f3330a4af99e4722f0bf436daaa818e16ea8076dd35968f", + "songs/all_of_me_acoustic.pages/playback-0036.png": "48c5688c1368946da513361c0e241ad13157fe7cf4dcaec216fe85031e63455f", + "songs/all_of_me_acoustic.pages/playback-0037.png": "e11a4272fe7596b73ac1ed2029c259268c0255bdf4805026fd28412c955a9b88", + "songs/all_of_me_acoustic.pages/playback-0038.png": "2ed4ea3113dfdf04f5416154904fc2bd409024e6beca62823bf8d49306f9dd32", + "songs/all_of_me_acoustic.pages/playback-0039.png": "506e04f4080141b20b51122756e1fb636b795b0c7a9cfea794787ef8f6d20b28", + "songs/all_of_me_acoustic.pages/playback-0040.png": "71b20e99fd27f4222435cb1349df375fc93f0749e6c07bc49946493044cfb088", + "songs/all_of_me_acoustic.pages/playback-0041.png": "01e099e1e9bb31eb89c0806ca3885793373243de5cb759b87782aba64548eca0", + "songs/all_of_me_acoustic.pages/playback-0042.png": "a26dc16d90c54856c8897fac364be9d4a5e67616afcd863582e4f36c14e689c2", + "songs/all_of_me_acoustic.pages/playback-0043.png": "1385cc3c40f04f897b69f277dc0f89a3c70fb0f4509722f13ab3f8a08d4a301b", + "songs/all_of_me_acoustic.pages/playback-0044.png": "c12d277f421d3120d318bf7ad14662153a1fa7af6941dd0362da460e967e0312", + "songs/all_of_me_acoustic.pages/playback-0045.png": "95c52e795b0aff090ce88d8f3048ccb1bfedb478a8ea0fc9611843e7d3233b33", + "songs/all_of_me_acoustic.pages/playback-0046.png": "f4fb50ff913ba429633c2e80c5dac60fa16dd3ea9a1c4873794f1e482c6c9c90", + "songs/all_of_me_acoustic.pages/playback-0047.png": "9ce7a7f624a654561fd132628400a16e95665cbc8bc33c437c7d21835de18adf", + "songs/all_of_me_acoustic.pages/playback-0048.png": "2faf0c360563387702a2244b26dba8885d5227c3ded4238d480fb81b607531f5", + "songs/all_of_me_acoustic.pages/playback-0049.png": "4e94b56e4962ad097bf6ad703be517215b11749e234efc86b7bf1e7ed845f0a2", + "songs/all_of_me_acoustic.pages/playback-0050.png": "aeeea727737f35d556abba012444551fde6f1202f99e0959b807d39840384801", + "songs/all_of_me_acoustic.pages/playback-0051.png": "7f918b1847cf774167f62437cc5524b2d7465355e7d3a34bc1778e0c5e01df7f", + "songs/all_of_me_acoustic.pages/playback-0052.png": "5da7caa11f7d223c7be0baf95d049ee226a28236c91a7fe3c95424fe509c5f63", + "songs/all_of_me_acoustic.pages/playback-0053.png": "6dae421ab2d5a85d253f0cc6a750d562a47f2a4c782fc70259c0d0dcdd5f6008", + "songs/all_of_me_acoustic.pages/playback-0054.png": "9a3cd474eaadf4c40b3df43ec805d47d1bc81110d0c56d1e17356d550b731cef", + "songs/all_of_me_acoustic.pages/playback-0055.png": "6489f62576b32c1c7071361fa9d3db372a47927d66f6865ee417bb99c9c04c15", + "songs/all_of_me_acoustic.pages/playback-0056.png": "f7ab14f136adcc844acec06935fdfec2b166972f6c7d6b3993883aaa0ca9e859", + "songs/all_of_me_acoustic.pages/playback-0057.png": "8fcc3723e92877b1005cedba8045a4488e5134e3f1f2185a465b8b6115823f4f", + "songs/all_of_me_acoustic.pages/playback-0058.png": "7d0a9ef281a2e38814506d04932276bcb5041c1c3947dd6aaa874ff066a038fd", + "songs/all_of_me_electric.json": "ae2bf27df59d93436bdcb8ae67821e635014e104e64ec8a4585326bc4bdc087a", + "songs/all_of_me_electric.pages/0001.png": "4080b675aeb49f1b745a18641fc284235b9c3f5e36d07bf258772300f6fed6b8", + "songs/all_of_me_electric.pages/0002.png": "b4d590883d16efdd43d8199324bb825ac23d84f1f5d4fc5c5131a82d504eb756", + "songs/all_of_me_electric.pages/0003.png": "6d16d6d083ddc2c3025c1b379cc145cb0e1de8074e7a51bfcaa817089c51e7a7", + "songs/all_of_me_electric.pages/0004.png": "106a0cd8cd598455a9694d62c3bc905c6e8f038910b40efabafdf43cf2836642", + "songs/all_of_me_electric.pages/0005.png": "5e69575380ed38032dc7ede57d4e815540e15eaa8fad7baeee3d60dd84b7d70b", + "songs/all_of_me_electric.pages/0006.png": "d21342d7e8dd3a63c246ae37dd69567914cb7c3e6c5171de04ed0ffbdc008717", + "songs/all_of_me_electric.pages/0007.png": "c418f413c20c48ce85fcb86ab20eb64d1b9635a45adaac8acfc54403ab2477b9", + "songs/all_of_me_electric.pages/0008.png": "166c390f64d654ac0adba802c45b2c5f3c47bcacf57a4180ddb0f36597f349f9", + "songs/all_of_me_electric.pages/0009.png": "c50e6e4582423176dd2c4e52933eb6263192350f63d16192bd13842d1bd901c8", + "songs/all_of_me_electric.pages/0010.png": "50b939ff62a4430102cd0d851758a326a7d9ee4478afc6471428c3b20692a154", + "songs/all_of_me_electric.pages/0011.png": "2a063242e638507b6978f03043505c441b392ab90943e8b8f39c9a5b6489105c", + "songs/all_of_me_electric.pages/0012.png": "843fd8cd70334df8b327e1316fd3df00efba5f2e7587abc96f14808f25d785ba", + "songs/all_of_me_electric.pages/0013.png": "d1648e1633ad74b790d1b21d3ac85946ea66e048ac8ebebc338dc58eba1453aa", + "songs/all_of_me_electric.pages/0014.png": "5859ff6cd737294f010ecfcf9202924530c037b56e14ffbb29ea7f3fc712a11f", + "songs/all_of_me_electric.pages/0015.png": "b38390c67e0e56a9218f1fd82d386583f8a5fb827255357d6d375ece7cc4e39a", + "songs/all_of_me_electric.pages/0016.png": "ce83a70d0c06b0fee259c1b537b889aa82e74f058a3d46edcbe2956a7cb96aa0", + "songs/all_of_me_electric.pages/0017.png": "97b5da09798a71b8ceae26748da222ea6a90dab14dd59ae4e0eddf55f42d706f", + "songs/all_of_me_electric.pages/0018.png": "124accb8cbb0b8becc086a3e0883052d1d44add715adc46c7fe1e4d1c65d0c62", + "songs/all_of_me_electric.pages/0019.png": "f3886454bbce1128ec0de1de71a2a1dba4dcec41b68fedf815a2c22280c83708", + "songs/all_of_me_electric.pages/0020.png": "6c78af0299bc6cc344800a18f3eeb310ef273f732588dd6d131a9315660c6c8b", + "songs/all_of_me_electric.pages/0021.png": "8d248495a0a6c711d1619475ffafb16ad6f831a6246bb0c32cb2500e69d0ad8b", + "songs/all_of_me_electric.pages/0022.png": "42e5cf25476df3a4ee8030a32d3522401f5de45c539504e544780ebb862bc3c2", + "songs/all_of_me_electric.pages/0023.png": "a7dfa4060c0b5fc26440ed299466db702eeb2cf80cd40c91f83e031f8be53b93", + "songs/all_of_me_electric.pages/playback-0002.png": "403f0cfd04a73ecc7d6277c1ad5d6f0638218386d71a18a960316ad75625de3a", + "songs/all_of_me_electric.pages/playback-0003.png": "0aaa246f780dcdce077fadf99f0bb2febb47eb70462c341545a01c351fa03a18", + "songs/all_of_me_electric.pages/playback-0004.png": "f272b7991e1e2721dea59fdc74836421ef501f7b937b5487ce82985d54feb65e", + "songs/all_of_me_electric.pages/playback-0005.png": "227c94ab1b8f2d28cda0cf2c55710796b1739f48e4e47ae8d88af5b3e7f3fa71", + "songs/all_of_me_electric.pages/playback-0006.png": "196910b9157c0fd142aed1555e294068a726ebf4a12fb077953ab4ffa078cc3b", + "songs/all_of_me_electric.pages/playback-0007.png": "65a2e7466bed99e53a52fa6135926a5d11309e2f5141b14dc1ecde85bb4a2974", + "songs/all_of_me_electric.pages/playback-0008.png": "d28dd164add8c1081408f6310bddad34f60c8fbbf2559d88f8d3024ff089ae18", + "songs/all_of_me_electric.pages/playback-0009.png": "7f9814e8de25ffa797cfcb723bf0146943d9dd0b3c7708e1b8eee34ccdcf65c9", + "songs/all_of_me_electric.pages/playback-0010.png": "0aeb5e7a51504708f1ac1a7f971c8bfd6164745ac334a44cdceb3ddc3cdc62d2", + "songs/all_of_me_electric.pages/playback-0011.png": "d3f67966eb3cc8a5198f8c5832ed9387607e3c657ba730361c06bee9bdeb8862", + "songs/all_of_me_electric.pages/playback-0012.png": "21e1f3c7978796f5b82aa3cf126ece81e409f6c48129681292f5703fb05dc371", + "songs/all_of_me_electric.pages/playback-0013.png": "6cbfc21a3ecdf4980b10636b8374cb918ff017abc5592dc9aef4edf55d27ddbd", + "songs/all_of_me_electric.pages/playback-0014.png": "dd2d3cf5ed13638c91a4b9f92462200494bc78001062f38d8be172d08804e5cf", + "songs/all_of_me_electric.pages/playback-0015.png": "e9dd230dd3410f8ccc6ba55c04155653a02a7cc54ba9903f07bd115894d57019", + "songs/all_of_me_electric.pages/playback-0016.png": "870884db997e94c9bafa6f8a173059dffa55f99f3a38e400e59658a251737b97", + "songs/all_of_me_electric.pages/playback-0017.png": "21f0489f8f948e4131a7c14585169f0d4e9795166a75459eeadc698a49e7c36a", + "songs/all_of_me_electric.pages/playback-0018.png": "e834520df8a74a2f762044d4d2a0bc220124227e348555b33bf73dca11d848a2", + "songs/all_of_me_electric.pages/playback-0019.png": "d046a861e398d0fa4e2eb0bd316f6101455e4c27cb9c73ece1c7a7e8e296032f", + "songs/all_of_me_electric.pages/playback-0020.png": "979701ac47e9cdcd4f21fd63af0dac2e0fede6567c9bb417c4a3133b99df66ee", + "songs/all_of_me_electric.pages/playback-0021.png": "62edac1a5171cd41a1e9eeaf67991ec7574941bb03a74e2bd742ea68840617f8", + "songs/all_of_me_electric.pages/playback-0022.png": "343e6c59f3f3b5718df93d3ef871ca6e041a0a674d719df122b6ab298dcd96ba", + "songs/all_of_me_electric.pages/playback-0023.png": "14735b44d4a7b65b957c1d733e7082c639a0e4acc9caffffde1b7bddf236eee7", + "songs/all_of_me_electric.pages/playback-0024.png": "cc21f078595a1fd138f00153b30cebcccecd0acc8c8b4a4a17e7908ad854e1a6", + "songs/all_of_me_electric.pages/playback-0025.png": "ce3df5c546abada13bf1cc0b409d30239df2bc9eff0f7d04857c75b4734bc642", + "songs/all_of_me_electric.pages/playback-0026.png": "367021a47fac7e2dc54e3288399b708ae46bd21a6055e8593eead39d1b195d58", + "songs/all_of_me_electric.pages/playback-0027.png": "8544e3d66736b42b4b4c6f4433b00945f14aabdf2fa02159e666e5005f66206e", + "songs/all_of_me_electric.pages/playback-0028.png": "96d88ee25fe1e2543fb0599a5e6f18f6fa20430146a3cbe2ec1cb4e164776943", + "songs/all_of_me_electric.pages/playback-0029.png": "689aac0d1696ce3940d05bceadad057c485bd36ffe6615aa679ee974c36b2cf1", + "songs/all_of_me_electric.pages/playback-0030.png": "c9961fb33a22d8cfe0ac23b708b491c8fe066c28565f946634aa35ecc50d2164", + "songs/all_of_me_electric.pages/playback-0031.png": "eb03bc4ff5d5037c5e068323060610f268678a4c8083d3f4f727c7171cc6b1cb", + "songs/all_of_me_electric.pages/playback-0032.png": "6ee11cbeb454dd51199b702178f54c8c17bb626542776419818477c77bb5c6e6", + "songs/all_of_me_electric.pages/playback-0033.png": "8b0bc5cd261d542f6aad5ba56d23aabba32892739b32e09e84f0c147beea7708", + "songs/all_of_me_electric.pages/playback-0034.png": "f413fb00b1231b1783a0578063212217043adc73a270c23cad8c8913bfd70e98", + "songs/all_of_me_electric.pages/playback-0035.png": "88f772174ce53af5d84ac4d804d827e4c3096ff64126b0ea2eb206589d75fe46", + "songs/all_of_me_electric.pages/playback-0036.png": "f0e09108d57afdc64791164cb8df197ff1794e5df3bd803325b1ab4ec904d761", + "songs/all_of_me_electric.pages/playback-0037.png": "b1a6b868a18f4a5b18dfb8b11d38ed5e5505c3a1736cf4d71172c6f75d9e6c22", + "songs/all_of_me_electric.pages/playback-0038.png": "aaef3ac9fc6d52703bb4bbfe588c51f630e6cef75a85fa9cbd3197c5c252bb23", + "songs/all_of_me_electric.pages/playback-0039.png": "43606ff740a8127560d60a384b9ca910a294cb96de13f3da2492a646097abe82", + "songs/all_of_me_electric.pages/playback-0040.png": "8f7809d7a0fcad64e9a7c5ab644b7de771460d7d48b554ef603d0c93a98a0124", + "songs/all_of_me_electric.pages/playback-0041.png": "7c0befdd861d8a98d170c04fa813a0c091568bf74bf94fcc14024ce7618d016f", + "songs/all_of_me_electric.pages/playback-0042.png": "349e59923c00ccd2675f61763dcb2bc81d9debe3efac409e0b25d8bcd1e3bd42", + "songs/all_of_me_electric.pages/playback-0043.png": "90c5d4f9149e380fcfcfdb18882381f535c8f619b475c480210dffe3570a3d8f", + "songs/all_of_me_electric.pages/playback-0044.png": "3548e497fe84e7a6f3344cdd5bfb7b4d44e89b6f2cc6778a8bf891fdb0d95ae3", + "songs/all_of_me_electric.pages/playback-0045.png": "1f21274695106a635e92551f7d12b739a82f1a5a17db8d911e60a00fca170e10", + "songs/all_of_me_vocal.json": "04c2712621766ee10529135463c0ceeffdd2def707134c81fb450e30a2d6cd67", + "songs/all_of_me_vocal.pages/0001.png": "5ff59ef10db0937c0dc132effc4f1ab761c6a4cdd9898aed1cbfc79c3ee126ad", + "songs/all_of_me_vocal.pages/0002.png": "8c737499c4e537b779160b86bf779de9b0a6771ece8f594af64ccd66e896f0d6", + "songs/all_of_me_vocal.pages/0003.png": "1ae152662e101117056704e7ccc92e7fadf546e4b0b68b11a463b8a778a2449e", + "songs/all_of_me_vocal.pages/0004.png": "d921899d08554455d72c71709d1110d808c5227de5cfc0c9aff68a168c7d4ecb", + "songs/all_of_me_vocal.pages/0005.png": "4c9fb6f70a887dceb7aca582122c7ba3d06a38431a37247b422e11a81f917e5e", + "songs/all_of_me_vocal.pages/0006.png": "a2792f1dc70d11d7f1b26f4c51686c8c6739844021e237c0d30f3604a2bc6306", + "songs/all_of_me_vocal.pages/0007.png": "ecdad2ac3c0a38bb0a553986b68fd64b6bce4dae7e2e654c5a152bc83abc61b9", + "songs/all_of_me_vocal.pages/0008.png": "0e619566374434cae278bd66344d94c80507e7477f678172da1f1cdf24bb65b9", + "songs/all_of_me_vocal.pages/0009.png": "8ef33bc47271fe389b3cc1c03be11e8689e8f5d1e3638a8af127adecbe90cde2", + "songs/all_of_me_vocal.pages/0010.png": "5f40610e4d3b143d3260b574c28c633b3a18e6e298fb3afe8adac95878bde3f7", + "songs/all_of_me_vocal.pages/0011.png": "32c857d8cdac61f34eb97031e27dce2f3b7d85f354e002c76c6947c07a49fb48", + "songs/all_of_me_vocal.pages/0012.png": "f6674afe2c6f4ead074491509ff12fc572656a73d894b170a7ef78ea0f83adce", + "songs/all_of_me_vocal.pages/0013.png": "886cee78fa1a4f5868a6adba5205d0843a6b9de0af42ea80e047c514b6ba0bc4", + "songs/all_of_me_vocal.pages/0014.png": "0e7d208b5da11cf6fd25b499ee138d865e63e85e2d9392ea9363623145b5677b", + "songs/all_of_me_vocal.pages/0015.png": "ba792e980325d694638ff90c7e9b87713de6b313c8ca2667e2889cd278581c42", + "songs/all_of_me_vocal.pages/playback-0002.png": "ad7fc6426e39e204109e3122bfa142d203abe60ba76a59b4df507e61745d923a", + "songs/all_of_me_vocal.pages/playback-0003.png": "4980352dee0600cad30f54ffba81577f4285248c7987df23aaa84f28544d02ed", + "songs/all_of_me_vocal.pages/playback-0004.png": "ad23b22d0fb931f098b88124fb6e48d2a1b03437f12944d8dbfbab9e5a6639e1", + "songs/all_of_me_vocal.pages/playback-0005.png": "065262ff6b758419107fd41f7b3956d996c325d6552a72f62281288c57ed29c1", + "songs/all_of_me_vocal.pages/playback-0006.png": "bd6cff39e52a41aab31473740a77357c47b2bb2b3dff42cfa794d3fe7888d7c1", + "songs/all_of_me_vocal.pages/playback-0007.png": "7cf0ebec216da49ab7385514a10e466656770d6668975840c8967f132414d2ce", + "songs/all_of_me_vocal.pages/playback-0008.png": "b4d59c19612d34d766cf2d76db71c6d6238d9afd6ce88f51dfebd117e6b1c5e1", + "songs/all_of_me_vocal.pages/playback-0009.png": "3d30c0180874fee2163821c4ef160cc5e134e5c5069f6e77ba542e40f5174bde", + "songs/all_of_me_vocal.pages/playback-0010.png": "5464485dc6c19973ac39dd3e573b9d7884cae22a6ef632f3c31e0e863635c7d6", + "songs/all_of_me_vocal.pages/playback-0011.png": "761aa1f8a8096046dabef1c1a30875205aec3309030cd60ae27938f51247f154", + "songs/all_of_me_vocal.pages/playback-0012.png": "0285ff0feffa0a61ec1a26b3eea54ca9156cba357847c74b1cfeac2084d454f4", + "songs/all_of_me_vocal.pages/playback-0013.png": "6177b132b398724081c814a65006ec205df52ea90403ed0c543e6ac9bdfdf2e8", + "songs/all_of_me_vocal.pages/playback-0014.png": "5fb997b60b54f808490b9fa0c6fedc396540f3b407c33b4562b54290daca4afe", + "songs/all_of_me_vocal.pages/playback-0015.png": "2a4a6345c63a9e6548519a3303d369bff76896eb1c4bfb40da42dc29fe08f710", + "songs/all_of_me_vocal.pages/playback-0016.png": "2ce1db668951fa36a075f119a423b1f50a1c4f346a29b1077cd55ecb31a5fa7b", + "songs/all_of_me_vocal.pages/playback-0017.png": "9f2677349e9d54f8de873e12399fb1f30a24d18e5f06c990aea2190cc41737e3", + "songs/all_of_me_vocal.pages/playback-0018.png": "25e7ab38426b3ce2c970c0c474aac7758f0f8c6a816fd0472a1a98ae4e37df9f", + "songs/all_of_me_vocal.pages/playback-0019.png": "f580eb1801c2ffad826c48a5a3c0b35c7513a8e4949723bba3f8f4522d03534e", + "songs/all_of_me_vocal.pages/playback-0020.png": "e6f23793d569cab0069560e88b1bc38e8fe44b1a11843d3a1a7639a33a6ab2fd", + "songs/all_of_me_vocal.pages/playback-0021.png": "a3cd07885f5ec333ba7ab3bf61807cb66263b7a4da4f1cc53e36717388a0cc13", + "songs/all_of_me_vocal.pages/playback-0022.png": "82529db7c06aa1ebe3bf5608287d2f4a6822337a5e1261d4687a6a9665478efc", + "songs/all_of_me_vocal.pages/playback-0023.png": "0b2c34c2524e5d437187d4bbf5cda395c55149081cc1478405a338d29af02ee3", + "songs/all_of_me_vocal.pages/playback-0024.png": "08d783cfb2d4bc09e83f3647a86455394f044dcce723ab9c5fa55063b222e844", + "songs/all_of_me_vocal.pages/playback-0025.png": "9c65d79d14f04bf05bee9de69c6c37a871e50aab635de797a91eb62fd81aac31", + "songs/all_of_me_vocal.pages/playback-0026.png": "79e87e2185d99c32aa753ac54068fabb698f81b16d6017ee7285f3b7c4d20054", + "songs/all_of_me_vocal.pages/playback-0027.png": "015cd105d6e5d2130393ced1268413b4929ff77231c30501b3a2748be6783b45", + "songs/all_of_me_vocal.pages/playback-0028.png": "7f3f16ced98698ef1034101976c05a1fb33b7c513057066b160b42eefacdbe64", + "songs/all_of_me_vocal.pages/playback-0029.png": "dfe0f2ea02945997653b0fac1b6d1355cd1c9a2cb5307f38ee59d51a54d4ec53", + "songs/all_of_me_vocal.pages/playback-0030.png": "98e2a6862eff212591ffc51d55c48ab65267e670bdb56d6c7ca6fb00ad750086", + "songs/autumn_leaves_acoustic.json": "e07a7e4fec89c811e6c26c1bba64af690743662074faa6b8f0fc81288097b53f", + "songs/autumn_leaves_acoustic.pages/0001.png": "54cbffc07ff6a31d8c340a4acaa14e6818b50b08d60041d7aa409fed01c2864c", + "songs/autumn_leaves_acoustic.pages/0002.png": "563ffa15c52a986603a17596e3ae9177bfc5222910a5c5bf31cbf61cdd1845fe", + "songs/autumn_leaves_acoustic.pages/0003.png": "fad7549f767ab0b261e58266ef9d6f227f5fe8d90f5c1ebcb8ecf1b0fee790a2", + "songs/autumn_leaves_acoustic.pages/0004.png": "fb5b442534e05f2fa9729965d0a05acc9c2d2878b9af297b6d646d7f625dd1e2", + "songs/autumn_leaves_acoustic.pages/0005.png": "a32a12e59624f65cf30685c5f4439514575d54fff42243506278b68b4961199c", + "songs/autumn_leaves_acoustic.pages/0006.png": "749df9fa3bf3bed1033b7a9950755f64e19e9793da91f35fa95e44da85d7dd4d", + "songs/autumn_leaves_acoustic.pages/0007.png": "e90ab72d11b381e70b11c987532ab25d84518d272fb7b03517d25a1f952aba6a", + "songs/autumn_leaves_acoustic.pages/0008.png": "846f529b487a4df90f8041d0c893225e8b3cf71feb1bd8ee903fd3a31126b8ef", + "songs/autumn_leaves_acoustic.pages/0009.png": "e8ac60d749ee6e1be2bd40457be800927eb8a321d11b2b98e663ef3835d2e8bd", + "songs/autumn_leaves_acoustic.pages/0010.png": "694c524e7cfc5b23e0fd8a301c230f7871bb783ad5243b7d45f34101a2ea22ae", + "songs/autumn_leaves_acoustic.pages/0011.png": "25e7b830b5fe8b9c9aaab2d31ab713a1b8da720f2ed116c547162ad77b99c10c", + "songs/autumn_leaves_acoustic.pages/0012.png": "afb17b0fae1be9504395a497776033736d7eccb8b040061333bdfe456035acb5", + "songs/autumn_leaves_acoustic.pages/0013.png": "76bd3e7c8a6c3a95a98f8c96e521509bc3ff4563fd5446032939e7c2c577f21d", + "songs/autumn_leaves_acoustic.pages/0014.png": "9701a0f3d5f5a3f32fb18ff1c11c5cc525961a9a9416c98b83818ff4607afb37", + "songs/autumn_leaves_acoustic.pages/0015.png": "235584fcf484de70f3beb463d1eb3783ed20d6c296eacd1fb8a5b9b3127eccab", + "songs/autumn_leaves_acoustic.pages/0016.png": "fe7ce9c311c2f57e9b6cb10fb5ca01f7f103d0d0f29e4da806e96b6cc6e005a9", + "songs/autumn_leaves_acoustic.pages/0017.png": "639450b174f68900a71cbbd3ab8a66b621f239e07922673e9482e49f30a892a0", + "songs/autumn_leaves_acoustic.pages/0018.png": "c15b4d2cfa76bebc9866dd022527999b0e7a09726738a7da6ba2285c7c4d9420", + "songs/autumn_leaves_acoustic.pages/0019.png": "84f637a60432cc35981a895899993b9b1b9f35a6ea6d211bb4aa95ccaeb42d8c", + "songs/autumn_leaves_acoustic.pages/0020.png": "e20b7a45b003e68b0f890b263f06adbcda947c3ecada541c2f0feb9dc9424741", + "songs/autumn_leaves_acoustic.pages/0021.png": "1651902b453243d5bd7fbc203417d232398063d2161eae37dfaba9327a3d35ad", + "songs/autumn_leaves_acoustic.pages/0022.png": "3051d9af506ddc661b259f2ca5c9f2d80076b1e9d6e303f38970091958f48bf0", + "songs/autumn_leaves_acoustic.pages/0023.png": "7c554926f10ee5090bde0da5f182f965cf8650889f8a3d6227334006ba65ec8c", + "songs/autumn_leaves_acoustic.pages/0024.png": "ead80368cefbb664988df29571cc3ac68a4ec599d70f0c53d628715e9795fc1b", + "songs/autumn_leaves_acoustic.pages/0025.png": "953fabb18457d14a5797ad5a6fd5ce0f3af099bb3877f08fd2813fdeca7e2481", + "songs/autumn_leaves_acoustic.pages/0026.png": "13da788d7bcce194552854ebdbb1601d5322bed6ac0bbae09e3cc07c34610379", + "songs/autumn_leaves_acoustic.pages/0027.png": "bbc81588269392772e6ae2671289716e1f1b51cfe8ebb790e887e866e465a7a7", + "songs/autumn_leaves_acoustic.pages/0028.png": "109adbfb7abf9059d198ce6abe1612457f6f0674636219ab33ba109e89fb8ef5", + "songs/autumn_leaves_acoustic.pages/0029.png": "1052f1b8e1e7746fc846c6893a02e587b0b204f32d8307800d7fd74ac06383ca", + "songs/autumn_leaves_acoustic.pages/playback-0002.png": "e82e130512e34e2801d39a730d4acac3ca689e0736ab100770d49404a598d034", + "songs/autumn_leaves_acoustic.pages/playback-0003.png": "5ba697f7f059b627dd4467589c4f4891413cbf7ad85932800a7fc00c7c70255a", + "songs/autumn_leaves_acoustic.pages/playback-0004.png": "f1bb58fce2cdcffa5523ac60099b6e52c356c6696fc1a4ff60a5cccd2dd3ee1e", + "songs/autumn_leaves_acoustic.pages/playback-0005.png": "15645d74bb6ab75901e87485dbcfc2c797e8a79b03973c1e1708889be2fc68c8", + "songs/autumn_leaves_acoustic.pages/playback-0006.png": "724eaab2251f9b799a439fb0fab1cb200ca471ee8a7e13718167e0de7481e20c", + "songs/autumn_leaves_acoustic.pages/playback-0007.png": "b1043e1876883e3d380cf8e3a59b2f6850d48054e9d0e570ca1eb988f3b78cf1", + "songs/autumn_leaves_acoustic.pages/playback-0008.png": "e5529398605d81e4265239afc5b6117bf0e89743fa2662d9edc4c8a1a71dba3b", + "songs/autumn_leaves_acoustic.pages/playback-0009.png": "690e52975aee8c6e21231e3d2e8be84fcb15fd42134bbb69c8bdccc3e8c8be7a", + "songs/autumn_leaves_acoustic.pages/playback-0010.png": "fb950d14561f570f1f645903cd4bce289bac97d182a237b1d26d3ca4c1e40357", + "songs/autumn_leaves_acoustic.pages/playback-0011.png": "3c6d7cb9269f25fd89feaac66eb14c4e1c5f40b1c1e5b4a11cd9993e90d70180", + "songs/autumn_leaves_acoustic.pages/playback-0012.png": "b317e5306a4f8ab59bfd21688640aa02df31382dcadbaaeac016e8682d4a95fd", + "songs/autumn_leaves_acoustic.pages/playback-0013.png": "b0949d15a05cb2458fd2fa5f6f842855591ddcea8caa260ce3006d849a7b3498", + "songs/autumn_leaves_acoustic.pages/playback-0014.png": "676df949c9074b9f096f1ec8535ff78a8852020a23ba50055bf95e4a03515771", + "songs/autumn_leaves_acoustic.pages/playback-0015.png": "1da00065ad926e8e46ff475f1db3a21d1beb1b7d021fe1e899e1ceb330dd6a6e", + "songs/autumn_leaves_acoustic.pages/playback-0016.png": "62504e382dee30dbb52e999c70bb4cd7619412cdd4304deb619527f5e81e03db", + "songs/autumn_leaves_acoustic.pages/playback-0017.png": "4f30f876ad7bec3f1a7962838b9431b640a1cd663421068e8d31400c4123b1c5", + "songs/autumn_leaves_acoustic.pages/playback-0018.png": "af9ddd4d84fddf1356ae21ca5f54f74011bbc7860b6ca34094daba6f0374ae8f", + "songs/autumn_leaves_acoustic.pages/playback-0019.png": "b46f18b2e4dcd8272f6b97a4599787e16a94afdd78adcb3c697cbc415573d861", + "songs/autumn_leaves_acoustic.pages/playback-0020.png": "6c84621d515f3fa2b6b5ddedb4c7d7a28e1f7916ab86241a27aed3e5e0036c97", + "songs/autumn_leaves_acoustic.pages/playback-0021.png": "9a95f5db7d7350cd5dd1b81408c65395826329eb0f2e95d0f12d93f281e7cf69", + "songs/autumn_leaves_acoustic.pages/playback-0022.png": "9c41ee3793c44018873ffbc0847e001a2d2a7273ae487b1d8f47a7ec10b1c51e", + "songs/autumn_leaves_acoustic.pages/playback-0023.png": "213ee4b1ed54411d16c880907b0d4d6c04ba9f97bed9eec4e4c5a09067040987", + "songs/autumn_leaves_acoustic.pages/playback-0024.png": "b37549829a19c82c525d4b2710b6ad0db21f7ca9735b71817e6e06de826796c9", + "songs/autumn_leaves_acoustic.pages/playback-0025.png": "950f311dad81dcaff3b6d73128c2869ab6d7277b7bac4358599d9bad3fe07147", + "songs/autumn_leaves_acoustic.pages/playback-0026.png": "6495623d373cf5d464c01e8a3c7f6e95984af75fc8ba6370fe72bba2a9c5a1d2", + "songs/autumn_leaves_acoustic.pages/playback-0027.png": "36e1a9c2df257d706af5f319c4616dfc9bcd6aab587f97bedcb4a1e8594a524d", + "songs/autumn_leaves_acoustic.pages/playback-0028.png": "e732e810da00f84bf74fe2f26d57b2fae01ea4691c39bf17cfc89b7707672716", + "songs/autumn_leaves_acoustic.pages/playback-0029.png": "01da01ecae8bcdaad33240fe686b2f44604a40ed5979cd5bb0aa4481035c9f11", + "songs/autumn_leaves_acoustic.pages/playback-0030.png": "52d1acf4d3cc5bc531ca879e9d47281d72dafb8079dca349bd12965528ecba52", + "songs/autumn_leaves_acoustic.pages/playback-0031.png": "e0c220f7f8522679bf628b03ce472b03340b8951f4be5010952822fcad8f6bd2", + "songs/autumn_leaves_acoustic.pages/playback-0032.png": "42801ea8c17bc1f49689a6ca7379928dfb1e569ee7354a014451ded48112bc5c", + "songs/autumn_leaves_acoustic.pages/playback-0033.png": "e447393a28aad3360e02dd4fdd6886eff73a46503f94511056938f68e629e13e", + "songs/autumn_leaves_acoustic.pages/playback-0034.png": "0b490c24ccba2e1d43649e64d21e48b1dea2f34f91d2e459d850bb02a4fd5e17", + "songs/autumn_leaves_acoustic.pages/playback-0035.png": "079872d5fb0bde62bc41909ca74246ed9804c6d46e8bef78185fffd6f41fd33a", + "songs/autumn_leaves_acoustic.pages/playback-0036.png": "6b90cb2c9cf07e7e5896cb57961cba6de2c5cb226772e0ad67ca75caab34dc3d", + "songs/autumn_leaves_acoustic.pages/playback-0037.png": "d5d2d73bfac0ad110ac55c54e87bd63a26799d14af76fe20378100e72b715782", + "songs/autumn_leaves_acoustic.pages/playback-0038.png": "7ee879807b0e1371326ea4cfd38ba59218c649397ef1448589f0a4b0557b9b85", + "songs/autumn_leaves_acoustic.pages/playback-0039.png": "9566293d49f978823df090545d7239172ce836b54d0a184c2f50672509f5095a", + "songs/autumn_leaves_acoustic.pages/playback-0040.png": "f4a68c14292b2c8cf1b870f0f5ea74603439a20317c931435b94bf4d69b6c7ba", + "songs/autumn_leaves_acoustic.pages/playback-0041.png": "d899bd39cc713bbda7c521537d81c775b609e02df199d4b740eace1a948cb27e", + "songs/autumn_leaves_acoustic.pages/playback-0042.png": "e6f07ad76a91f5a0a1c563a238e5f859fd0d8799d18e429ebc6967c1c391ae0e", + "songs/autumn_leaves_acoustic.pages/playback-0043.png": "9b0cd3c1bce012b04afe478cbeba630643de02d695324c42f2062c515efb7321", + "songs/autumn_leaves_acoustic.pages/playback-0044.png": "1eca5856dbd1f7c58297b9e47766675efa44fabbbbf5668d07dede8d47c2bdea", + "songs/autumn_leaves_acoustic.pages/playback-0045.png": "d9eaf533b05d3a889bba6005f1cef1c1b62466db18aa7bac819f5ba2b25d27e7", + "songs/autumn_leaves_acoustic.pages/playback-0046.png": "8019f806b1782995ad79a2f06463e0241c3442195ba9cbc3f6a0c2719c3afa85", + "songs/autumn_leaves_acoustic.pages/playback-0047.png": "b0384920846c4089363ef8cfa4a5ce70fc2562788592a06138282b4c019da225", + "songs/autumn_leaves_acoustic.pages/playback-0048.png": "b7a7489c6ef5744c5daa995600896dd2694eeab383f878a02dc7057e530d657e", + "songs/autumn_leaves_acoustic.pages/playback-0049.png": "c76ff27923d2908a30e1eea6d880064de7e7c941814608452c3b8dd594daf850", + "songs/autumn_leaves_acoustic.pages/playback-0050.png": "ff9d2e94684c7b0342f6b9833c8e7e3c5611be9a436e8fd403d809c8540f8ed5", + "songs/autumn_leaves_acoustic.pages/playback-0051.png": "f376a335fe15961cee9781f8ea53307b3f6578a06b194f3304e4704278e5defb", + "songs/autumn_leaves_acoustic.pages/playback-0052.png": "8be1c27434d95f231ddad913981f140a3df9beb4914ca6e4d1e4a5dc42a65798", + "songs/autumn_leaves_acoustic.pages/playback-0053.png": "7261e3d2465bf7d4de7b7710aff229d64b61cf3861870cc6aeda931d88d602b3", + "songs/autumn_leaves_acoustic.pages/playback-0054.png": "51375ba98654c3ddf198dd4da24c72b688b37f6c3670fd0eb77ffa5fdabb3965", + "songs/autumn_leaves_acoustic.pages/playback-0055.png": "c2e8d36d522a39131eff7af3cfabc8139baa90ca55d1e523aa6cb2590be933c6", + "songs/autumn_leaves_acoustic.pages/playback-0056.png": "2d4c1e0611eb4c4d2e653e25f4719f4da8ae93b4d481e22bc401f615cd04093e", + "songs/autumn_leaves_acoustic.pages/playback-0057.png": "d15d0441b80e087fc3d8f01dd7086a6fcad5ec6c66df3ee410920ddf8e8172fc", + "songs/autumn_leaves_electric.json": "90250a7776c295c4886c1cad6aaa0ee5f42430e9309289fdc35b8a99ec0e1de3", + "songs/autumn_leaves_electric.pages/0001.png": "6a10d99239f2a2f7cb86f10a371410191e8eab0d7335ac96f382efb7ebc8be73", + "songs/autumn_leaves_electric.pages/0002.png": "6ab880d8341c5ee92fece84d0756f942bd12e4f80cef6648f3ed779d386a93b5", + "songs/autumn_leaves_electric.pages/0003.png": "26e73ccda608f5baebec6e9c1beceb5586b13b5d138f0d29d4c13f58a6924715", + "songs/autumn_leaves_electric.pages/0004.png": "d7d47739c2c32a676198d091d546e027e218530dbe76147a71ad7156dad96f8c", + "songs/autumn_leaves_electric.pages/0005.png": "3c190110e3b59e42185dedcd4351e36f90d6e79788732f91b55fc92e059053ab", + "songs/autumn_leaves_electric.pages/0006.png": "39ce5045c8bb217a3b8820d4a1af14896faf313ca91ebe63ff848eba6c76e0fc", + "songs/autumn_leaves_electric.pages/0007.png": "1f5f36692863b4ccc4f08cf3a8b9c2c8edbd9f5b72d8ee76aae78dee8fb5220f", + "songs/autumn_leaves_electric.pages/0008.png": "52e12ff6e4983451ad7e59c45f3c5cd049171e00f2c19ce40e10de994764cbb7", + "songs/autumn_leaves_electric.pages/0009.png": "eb18d7f17fd0a101ae02df700409f7064cdc1ed585f315dcc25220490a3a3ca4", + "songs/autumn_leaves_electric.pages/0010.png": "8b923a580500ec4dddfba53ac57029806acc34f4bc51045296d46ba9db9fe0b8", + "songs/autumn_leaves_electric.pages/0011.png": "ceafe100023db75a8d34ea08e8c66155ba76aa12e8a24c9b83294d2a01779ca9", + "songs/autumn_leaves_electric.pages/0012.png": "813635f070252ce2cfa76de28fdb7badca2534783edfd7d791983d8b22d05b20", + "songs/autumn_leaves_electric.pages/0013.png": "e828faeb75fa104b6e15991e0489db7a43652bbde47ebcf818348a54614d5452", + "songs/autumn_leaves_electric.pages/0014.png": "6bcff78f5fcd2b214795e1463cda7f817785f2cd80c9727147ea12fe169760cf", + "songs/autumn_leaves_electric.pages/0015.png": "19bde11ca45dd5927797ee23a8cdcbfc891e7ff29bc9a01c2ca945b84e07e5e8", + "songs/autumn_leaves_electric.pages/0016.png": "071a61fa83944bea0b04aa221b19d124876c2a5f1b1e8347b8edaed4ed1aae92", + "songs/autumn_leaves_electric.pages/0017.png": "1d1ecbdf8e123aa2d003e352e85070eaa8b20979dbe25ea04be61f9fb8efccd0", + "songs/autumn_leaves_electric.pages/0018.png": "d551633f1c99dbe7b74e2fb2c2c7ef490eceee0d812cf697702de2077c2b74b7", + "songs/autumn_leaves_electric.pages/0019.png": "71672fc8613539914aa4069a4668751363e0be3d2e9822cf078755fa6b9116f3", + "songs/autumn_leaves_electric.pages/0020.png": "86747ada8e9ff0b1c05824d1a1ec9328f01cf33a65c3ae13c64a1e075c0a52ba", + "songs/autumn_leaves_electric.pages/0021.png": "c981fe628c1621c6f5add8635e33aaacae112fe954b2b43f15b3f02604c6c9e9", + "songs/autumn_leaves_electric.pages/playback-0002.png": "53f34213f307f1c7ab471c3c28db34a28e551936989b1a450282a325b533b71d", + "songs/autumn_leaves_electric.pages/playback-0003.png": "33f686464316f82a30d2b71bbafb6f8d2a74c44461f9b6b2c4754632671c4381", + "songs/autumn_leaves_electric.pages/playback-0004.png": "d99dcc2f8f15f120c2bf3cc6eba91b794aa57cd0078961d930ffc8dae843c791", + "songs/autumn_leaves_electric.pages/playback-0005.png": "568f242912b71a2a0e6b438fabaeaf043a62087d1b3018d61838093e6812c392", + "songs/autumn_leaves_electric.pages/playback-0006.png": "dc5b754b3d11d136a89c82b3160b3cb631572065505b2770f529c312c8beb0cc", + "songs/autumn_leaves_electric.pages/playback-0007.png": "13d877cc54210a860f0e08006c5e386a6a3e5c339e2d334df11ad37afd8b8fec", + "songs/autumn_leaves_electric.pages/playback-0008.png": "90c1df01c76f8c919cfd7276f80a899259851e090975ea1d1cabdcc613d02d72", + "songs/autumn_leaves_electric.pages/playback-0009.png": "dc2be361fa7c0c2c56fe02c24a97bcb628a1eff960e8b1db78739679f562759e", + "songs/autumn_leaves_electric.pages/playback-0010.png": "07d5c2b2ae31ebd88d4fb4708caefac36aca5d436048c1ffad55feffa6d87925", + "songs/autumn_leaves_electric.pages/playback-0011.png": "8d22eb7cc86906c36967434e31811a4542b1e44f6590eabff23f9a940916d207", + "songs/autumn_leaves_electric.pages/playback-0012.png": "c964863cbd956968210fc939b75ef6681d6f797f50860b75d8b7d0736ca12305", + "songs/autumn_leaves_electric.pages/playback-0013.png": "7d9c22406e24aff928e56cbebbb4749d1e8880542975fd53447afaca19b44584", + "songs/autumn_leaves_electric.pages/playback-0014.png": "2fce4a9c3624e90ebf964965a11d37e951db7e18824a2268c487abe2dd414168", + "songs/autumn_leaves_electric.pages/playback-0015.png": "edda9e50c852a5da934c21cd2d8a70a19e8c54b9c7020feb0d542ed0b257c2b7", + "songs/autumn_leaves_electric.pages/playback-0016.png": "50cba6043f888330d3a01ecd695de8f3ed7f3e51ae94da54b3faadb1088e74de", + "songs/autumn_leaves_electric.pages/playback-0017.png": "7c8bcaccb8b7879665b50a1185a90cc3ee31e6bcd21ecc2fed2dcd723d31e114", + "songs/autumn_leaves_electric.pages/playback-0018.png": "084a522a8e146a7fe533b3498eb842a586cf743c6a034b79f15b067a6ce52a4e", + "songs/autumn_leaves_electric.pages/playback-0019.png": "9f47a55406500ef63d7936065fd53685cef4d72b7b2190a14e6daf28e4d8bbbd", + "songs/autumn_leaves_electric.pages/playback-0020.png": "95a7d3e90b7a7cf435ce87c1d83366822f38f53857d993cb9c242afce3f1ac2a", + "songs/autumn_leaves_electric.pages/playback-0021.png": "f983e3bccc183efa8fbbe9f83a7629b056820d15129bc84108f02934a4b52ae3", + "songs/autumn_leaves_electric.pages/playback-0022.png": "614a51979fa8c9229503018645533118f05f496ffee4be4003e18a2281015844", + "songs/autumn_leaves_electric.pages/playback-0023.png": "223fcf58efd3e678fd0e5a761a2f504eea569a1cb4f6b27f85baa8d425bdb499", + "songs/autumn_leaves_electric.pages/playback-0024.png": "8c3ef22114aa00a42f603daac996f923d3d4aef9ee1cfdd2daa437a8760a9c85", + "songs/autumn_leaves_electric.pages/playback-0025.png": "ba7d51c2f2eeeefa52099d2401d695ccdf3091a49599c206eaa1daa39c68eccf", + "songs/autumn_leaves_electric.pages/playback-0026.png": "9c09778168a6b0b44d3beedb49c915a9e126fe47e8ec0afd4e72061ebadae533", + "songs/autumn_leaves_electric.pages/playback-0027.png": "53f851a9583415aa36f60aa4655f04d92e38cc3a679d9205bd93fbc971798e41", + "songs/autumn_leaves_electric.pages/playback-0028.png": "591e475f21cf481cb35278ee065cd808fa141162320d54c1ad589d8ff692143e", + "songs/autumn_leaves_electric.pages/playback-0029.png": "01fe5d38ca9867a442696e3332fb1a5c11ffadcd1e1182ed6164f6caae1c7950", + "songs/autumn_leaves_electric.pages/playback-0030.png": "6cc22a97e81be03b6eef0ef11b0cf8d088336eb19da26a8bb4e366c3615145db", + "songs/autumn_leaves_electric.pages/playback-0031.png": "93a7835d071c07c17d9b3fc50e1e2ff3b19d0b3087f21ef19c18bc622519ded6", + "songs/autumn_leaves_electric.pages/playback-0032.png": "ffdd2864d336f12375e963cb0fd4647992b790d92899f809a17f0aa2941b9324", + "songs/autumn_leaves_electric.pages/playback-0033.png": "8c2097ce48fb79c218aeeda3513025d3c6c3488aaf4682c672cf703f05366b74", + "songs/autumn_leaves_electric.pages/playback-0034.png": "07192aa9420386cb8165b1f43be384b83a7b588b383e57d3905b235acf52d77b", + "songs/autumn_leaves_electric.pages/playback-0035.png": "786e44c54bdb27b095dff2f903df5319e794355f986a2ed86882de78e299b589", + "songs/autumn_leaves_electric.pages/playback-0036.png": "f4d8b82bda3c599dd803b66a5ef1d7ef77f57d5757dafcda168caba6fb8a0399", + "songs/autumn_leaves_electric.pages/playback-0037.png": "d3e999968b05cb703fba44bcb1f3b8cc1b0934ca7513f9e952db41a27d9dbccd", + "songs/autumn_leaves_electric.pages/playback-0038.png": "43773db6f940cdd5457053e9d1cebb1f715d7a9d069fef8f29dd280bc664383f", + "songs/autumn_leaves_electric.pages/playback-0039.png": "da683c3e3d6198a49464d6e9463f9bed2ad9595c6c69ac09adaeeeeddb626003", + "songs/autumn_leaves_electric.pages/playback-0040.png": "1e1fe40450357ed40ab4a688906e62c69306dc706ec53e4c23de94d7228dffc3", + "songs/autumn_leaves_electric.pages/playback-0041.png": "f7c3ab577b12be529b9c73b44521852db930681e6dba1410aae569e526fc4a29", + "songs/autumn_leaves_electric.pages/playback-0042.png": "a827c32cf60d5bafb3b042d36f14883bee389e677091bfc5033771356c164254", + "songs/autumn_leaves_vocal.json": "412981cfd4ea208bbda8de8bbfc2d751164c379340bc02a61233692ef19f1032", + "songs/autumn_leaves_vocal.pages/0001.png": "4b9402563b8ae88a34f25482d5db3d5279643bed60731c7baac69a030b3ce2d9", + "songs/autumn_leaves_vocal.pages/0002.png": "b961fb05e5539ce4335b561df9fc9558b81574872a35956da0beb192a8db258e", + "songs/autumn_leaves_vocal.pages/0003.png": "a3e5423f083d8159e52dd4f1fa1532b982a4290dc3db75921a120fa3befb5c7d", + "songs/autumn_leaves_vocal.pages/0004.png": "9aecc0d1c84bdebdf214c99469ef2fbc7a8e625471b8b06e26c3267dea1dfc42", + "songs/autumn_leaves_vocal.pages/0005.png": "80f3956a196ef56b01647c7daa0d58989c12f04b2ddd8c166cedaf846c1d0f18", + "songs/autumn_leaves_vocal.pages/0006.png": "56e0fcb5faf8c5bd514efd6568284d6548708b4fed7d470c138d9478db34eb77", + "songs/autumn_leaves_vocal.pages/0007.png": "81b80d0a70883a69e10b8f06fd2c2f904670dc3a1cae13b980d239ae4a882e46", + "songs/autumn_leaves_vocal.pages/0008.png": "d55633751f78440bd94f1dbbed846c151cf79bebc5fa4b0f338add0c4b6d2e99", + "songs/autumn_leaves_vocal.pages/0009.png": "2da18b1db70f1cff719f697c39ecda329dbc60d3fe0d605351fcae8583e6901a", + "songs/autumn_leaves_vocal.pages/0010.png": "321d18c54e138248b31a00df6e3a95b9af62588a227a83e56db7475acb923d7a", + "songs/autumn_leaves_vocal.pages/0011.png": "c02c41a2ff2b14ec5bde44f22bec03fd71edf2d9e272d2ef0a8434b843a26b6b", + "songs/autumn_leaves_vocal.pages/0012.png": "ac4bf6573e196d5afbb0049bcc54836a7acac6a1e9e8cce48dedad203f4b43e0", + "songs/autumn_leaves_vocal.pages/0013.png": "6c22d2dde54e4e7d6cd4cef815ff4e4a9738cd48707f344e90e75737939f68ec", + "songs/autumn_leaves_vocal.pages/0014.png": "3b602afdbb2705281dc2dc5120427105649c5df1692155540c9ad9adae3621c8", + "songs/autumn_leaves_vocal.pages/0015.png": "59339718670a3c8d97b6efb7acc3e7234cb26b11e135da9f502689c77824be8a", + "songs/autumn_leaves_vocal.pages/playback-0002.png": "c95138bff07d8dae52535a96bb33cc3a31c6509d7da49c60c35a55f2a1072c4c", + "songs/autumn_leaves_vocal.pages/playback-0003.png": "fb030bc5b0aa831b534223b63b07cbadd7ab76ce9376c3bde8965f09e73d41f8", + "songs/autumn_leaves_vocal.pages/playback-0004.png": "250262570aac9adaff71ddde21a61c65550afb46849016dcd5162cfcedb522da", + "songs/autumn_leaves_vocal.pages/playback-0005.png": "4832b81482f73003ec7e2990f1558ac8d4cc62883ad1c01d8e78837a37a6de6c", + "songs/autumn_leaves_vocal.pages/playback-0006.png": "0b753c25706437aa8b716f9a254e5dfc03c0c441dd2dfc4f038c3fba7528dd19", + "songs/autumn_leaves_vocal.pages/playback-0007.png": "9f9cfa20a8dfa15cd5ac9e0c47f5d6c0632177702229d5f5c7566c315aa99459", + "songs/autumn_leaves_vocal.pages/playback-0008.png": "d525a0af7688927b7cb5f14302ab7151b9e2d0dd83d04d4b53ac7414f8c928e4", + "songs/autumn_leaves_vocal.pages/playback-0009.png": "f6dbfeca5e20dcf475f8da0285632b317759e1d2d4d69a54a4232d853db68004", + "songs/autumn_leaves_vocal.pages/playback-0010.png": "b54d909532c99f359dded5fc2cb11f93309bd8a158b500ee3be524338e471f48", + "songs/autumn_leaves_vocal.pages/playback-0011.png": "619b4621c5ade46fd29e5e70f08c8d421e1916e0578fa2979bcb4a8204dd99d7", + "songs/autumn_leaves_vocal.pages/playback-0012.png": "4472606d66ca6d1a6d490e89f127327e121331eba14c89b1f20e108ed63cb2af", + "songs/autumn_leaves_vocal.pages/playback-0013.png": "525be592c83a6a119c1b2bc8f0eba24de0bac7ac66c56899f10921e959ae7290", + "songs/autumn_leaves_vocal.pages/playback-0014.png": "8f792dd41bad096867edf9e51fcbe5e6773db1256e849cc32b9402aed59ca991", + "songs/autumn_leaves_vocal.pages/playback-0015.png": "2767f2d9979040258f3a619274e6f11fc8e750b7dc3f0d34adbf373455c08200", + "songs/autumn_leaves_vocal.pages/playback-0016.png": "8a61095e966c67c35df99da5fe32d0bb710c9e0443f135585593100a3b76a8bd", + "songs/autumn_leaves_vocal.pages/playback-0017.png": "234f9045d2cc31fc73f1c11bed69641acc5887ac794d051760ba041df5279b8f", + "songs/autumn_leaves_vocal.pages/playback-0018.png": "6d3156bc58fbcd88abc9c5b18e41ab62a7fece8996655c6723a52501869a60a3", + "songs/autumn_leaves_vocal.pages/playback-0019.png": "9851262495f7b0916982a8f46421948068d4208af9ec1d7ff22fd38a80860d24", + "songs/autumn_leaves_vocal.pages/playback-0020.png": "03026a3ab967ac2c963336c475f9ecb3033c799ae28d9e832a2e9d3ac5367c99", + "songs/autumn_leaves_vocal.pages/playback-0021.png": "d72cd7ed7c58cd4fd44372f6dfcef7e91b84b3c74f747bf1fc84a2c81c44da36", + "songs/autumn_leaves_vocal.pages/playback-0022.png": "46ff7fd7202c1378a9598982379d4d7322f5de934280a78eb5cd76a14f6fad35", + "songs/autumn_leaves_vocal.pages/playback-0023.png": "406830c4e11ecc52774bf67351a2d6c3410f2606df206c43cafa44c5beed42cf", + "songs/autumn_leaves_vocal.pages/playback-0024.png": "072dda94b8a251c775663f81b4b264edac9bdb64c67d115fd87ae469eb7b812f", + "songs/autumn_leaves_vocal.pages/playback-0025.png": "b98a8b5879620dfe0bccd063f8d6122fc6826b86b11d07da3aa7abb8a07f265d", + "songs/autumn_leaves_vocal.pages/playback-0026.png": "476255d5814bba2f838f9addc852f080408c24fa3d1c2b549c93da3d59bc11bb", + "songs/autumn_leaves_vocal.pages/playback-0027.png": "0678a2cf026519a06e62c8ef3599a429290092968785e71431584634bba26375", + "songs/autumn_leaves_vocal.pages/playback-0028.png": "d89db0cd8a2435a978a2e11b070c7aaacb3f791542aed8da4b3e17af888ceee5", + "songs/autumn_leaves_vocal.pages/playback-0029.png": "b8ae37ad031080e1a70f8113123e6706455798e51e8dae87913381f846661e08", + "songs/autumn_leaves_vocal.pages/playback-0030.png": "c04d59f0f791b9d813ba659fdb4bd616b649afca3a79b9ed437781f2301f04d8", + "sounds/beat1.wav": "634e73055b4ae05857de54c0cff0bee2ac4476dcde874b474b192512ba113a39", + "sounds/beat2.wav": "9bc510822228479fe4d4fd85be0d3b2f9eac009449f7fd6f916dd54661124c34" +} diff --git a/tests/fixtures/role_outputs.json b/tests/fixtures/role_outputs.json new file mode 100644 index 0000000..c1e8da8 --- /dev/null +++ b/tests/fixtures/role_outputs.json @@ -0,0 +1,16725 @@ +{ + "acoustic_accompaniment": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "acoustic", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_acoustic", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_1_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n1_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_1_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_1_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n3_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_1_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_1_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_2_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_2_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_2_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n3_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_2_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_2_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_3_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n1_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_3_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_3_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n3_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_3_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_3_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_4_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_4_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_4_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n3_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_4_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_4_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_5_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n1_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_5_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_5_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n3_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_5_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_5_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_6_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_6_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_6_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n3_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_6_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_6_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_7_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_9_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n1_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_9_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_9_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n3_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_9_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_9_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_10_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_10_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_10_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n3_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_10_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_10_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_accompaniment_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_11_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n1_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_11_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_11_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n3_p3", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_11_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_11_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_12_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_12_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_12_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n3_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n3_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n3_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_accompaniment_measure_12_n3_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_12_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_accompaniment_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_accompaniment_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_13_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_13_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "tie_to": "acoustic_accompaniment_measure_14_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_13_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_to": "acoustic_accompaniment_measure_14_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_13_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_to": "acoustic_accompaniment_measure_14_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_13_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_to": "acoustic_accompaniment_measure_14_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "acoustic_accompaniment_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_14_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_14_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "tie_from": "acoustic_accompaniment_measure_13_n1_p1", + "tie_to": "acoustic_accompaniment_measure_15_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_14_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_from": "acoustic_accompaniment_measure_13_n1_p2", + "tie_to": "acoustic_accompaniment_measure_15_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_14_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_from": "acoustic_accompaniment_measure_13_n1_p3", + "tie_to": "acoustic_accompaniment_measure_15_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_14_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_from": "acoustic_accompaniment_measure_13_n1_p4", + "tie_to": "acoustic_accompaniment_measure_15_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "acoustic_accompaniment_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_accompaniment_measure_15_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_15_n1_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "tie_from": "acoustic_accompaniment_measure_14_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_15_n1_p2", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_from": "acoustic_accompaniment_measure_14_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_15_n1_p3", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_from": "acoustic_accompaniment_measure_14_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "acoustic_accompaniment_measure_15_n1_p4", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_from": "acoustic_accompaniment_measure_14_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Acoustic", + "octave_shifts": [], + "role": "guitar", + "tablature": true, + "tuplets": [] + } + ] + }, + "acoustic_solo": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "acoustic", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_acoustic", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_1_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_2_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_3_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_4_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_5_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_6_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "acoustic_solo_measure_7_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "acoustic_solo_measure_7_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 1, + "type": "quarter" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_7_n2", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_solo_measure_7_n2_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "acoustic_solo_measure_7_n3", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_7_n4", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_solo_measure_7_n4_p1", + "letter": "c", + "octave": 4, + "tablature": { + "fret": 3, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n2", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_solo_measure_8_n2_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n3", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n4", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "acoustic_solo_measure_8_n4_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n5", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n6", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "acoustic_solo_measure_8_n6_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 5, + "string": 5 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "acoustic_solo_measure_8_n7", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_9_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_10_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "acoustic_solo_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_11_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_12_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "acoustic_solo_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "acoustic_solo_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_13_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "acoustic_solo_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_14_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "acoustic_solo_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "acoustic_solo_measure_15_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Acoustic", + "octave_shifts": [], + "role": "guitar", + "tablature": true, + "tuplets": [] + } + ] + }, + "electric_accompaniment": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "electric", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_electric", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_1_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_2_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_3_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_3_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_3_n1_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_3_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_3_n1_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_3_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_accompaniment_measure_3_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_3_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_3_n3_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_3_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_3_n3_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 1, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_3_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_4_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_4_n2", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "electric_accompaniment_measure_4_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_4_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_4_n3_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_4_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_4_n3_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "written": { + "dots": 0, + "type": "half" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_4_n4", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_5_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_6_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_7_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_7_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_7_n1_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_7_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_7_n1_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_7_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_accompaniment_measure_7_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_7_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_7_n3_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_7_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_7_n3_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 1, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_7_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_8_n2", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "electric_accompaniment_measure_8_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_8_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_8_n3_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_8_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_8_n3_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "written": { + "dots": 0, + "type": "half" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_8_n4", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_9_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_10_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_accompaniment_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_11_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_11_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_11_n1_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_11_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_accompaniment_measure_11_n1_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_11_n2", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_accompaniment_measure_11_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_11_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_11_n3_p2", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_11_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 3 + }, + "id": "electric_accompaniment_measure_11_n3_p4", + "letter": "a", + "octave": 5, + "tablature": { + "fret": 5, + "string": 1 + }, + "written": { + "dots": 1, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_11_n4", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_12_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_12_n2", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 2 + }, + "hidden": false, + "id": "electric_accompaniment_measure_12_n3", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_12_n3_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_12_n3_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_12_n3_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "half" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 2 + }, + "id": "electric_accompaniment_measure_12_n3_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "written": { + "dots": 0, + "type": "half" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "half" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_accompaniment_measure_12_n4", + "kind": "rest", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [], + "voice": "1", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "electric_accompaniment_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_accompaniment_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_13_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_13_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_to": "electric_accompaniment_measure_14_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_13_n1_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_to": "electric_accompaniment_measure_14_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_13_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_to": "electric_accompaniment_measure_14_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_13_n1_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "tie_to": "electric_accompaniment_measure_14_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_accompaniment_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_14_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_14_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_from": "electric_accompaniment_measure_13_n1_p1", + "tie_to": "electric_accompaniment_measure_15_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_14_n1_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_from": "electric_accompaniment_measure_13_n1_p2", + "tie_to": "electric_accompaniment_measure_15_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_14_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_from": "electric_accompaniment_measure_13_n1_p3", + "tie_to": "electric_accompaniment_measure_15_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_14_n1_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "tie_from": "electric_accompaniment_measure_13_n1_p4", + "tie_to": "electric_accompaniment_measure_15_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_accompaniment_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_accompaniment_measure_15_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_15_n1_p1", + "letter": "f", + "octave": 4, + "tablature": { + "fret": 3, + "string": 4 + }, + "tie_from": "electric_accompaniment_measure_14_n1_p1", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_15_n1_p2", + "letter": "b", + "octave": 4, + "tablature": { + "fret": 4, + "string": 3 + }, + "tie_from": "electric_accompaniment_measure_14_n1_p2", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_15_n1_p3", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "tie_from": "electric_accompaniment_measure_14_n1_p3", + "written": { + "dots": 0, + "type": "whole" + } + }, + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "id": "electric_accompaniment_measure_15_n1_p4", + "letter": "g", + "octave": 5, + "tablature": { + "fret": 3, + "string": 1 + }, + "tie_from": "electric_accompaniment_measure_14_n1_p4", + "written": { + "dots": 0, + "type": "whole" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Electric", + "octave_shifts": [], + "role": "guitar", + "tablature": true, + "tuplets": [] + } + ] + }, + "electric_melody": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "electric", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_electric", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_1_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_2_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_3_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_4_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_5_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_5_n1_p1", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 1, + "string": 2 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_5_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_5_n2_p1", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 3, + "string": 2 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_5_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_5_n3_p1", + "letter": "e", + "octave": 5, + "tablature": { + "fret": 5, + "string": 2 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_5_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_5_n4_p1", + "letter": "f", + "octave": 5, + "tablature": { + "fret": 6, + "string": 2 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_6_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_6_n1_p1", + "letter": "c", + "octave": 5, + "tablature": { + "fret": 5, + "string": 3 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_6_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_6_n2_p1", + "letter": "d", + "octave": 5, + "tablature": { + "fret": 7, + "string": 3 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_6_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_6_n3_p1", + "letter": "e", + "octave": 5, + "tablature": { + "fret": 9, + "string": 3 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_melody_measure_6_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "electric_melody_measure_6_n4_p1", + "letter": "f", + "octave": 5, + "tablature": { + "fret": 10, + "string": 3 + }, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_7_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_9_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_10_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_melody_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_11_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_12_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "electric_melody_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_melody_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_13_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_melody_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_14_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_melody_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_melody_measure_15_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Electric", + "octave_shifts": [], + "role": "guitar", + "tablature": true, + "tuplets": [] + } + ] + }, + "electric_solo": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "electric", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_electric", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_1_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_2_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_3_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_4_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_5_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_6_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_7_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_solo_measure_9_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_9_n2", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_solo_measure_9_n2_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 10, + "string": 6 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_9_n3", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_9_n4", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 5 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_solo_measure_9_n4_p1", + "letter": "c", + "octave": 4, + "tablature": { + "fret": 8, + "string": 6 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_9_n5", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_solo_measure_10_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_10_n2", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_solo_measure_10_n2_p1", + "letter": "d", + "octave": 4, + "tablature": { + "fret": 10, + "string": 6 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 3 + }, + "hidden": false, + "id": "electric_solo_measure_10_n3", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 1, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 2, + "numerator": 1 + }, + "hidden": false, + "id": "electric_solo_measure_10_n4", + "kind": "note", + "onset": { + "denominator": 2, + "numerator": 7 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 2, + "numerator": 1 + }, + "id": "electric_solo_measure_10_n4_p1", + "letter": "c", + "octave": 4, + "tablature": { + "fret": 8, + "string": 6 + }, + "written": { + "dots": 0, + "type": "eighth" + } + } + ], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "eighth" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "electric_solo_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_11_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_12_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "electric_solo_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "electric_solo_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_13_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_solo_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_14_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "electric_solo_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "capo": 0, + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "electric_solo_measure_15_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Electric", + "octave_shifts": [], + "role": "guitar", + "tablature": true, + "tuplets": [] + } + ] + }, + "vocal_melody": { + "metadata": { + "composer": "", + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "tempo": 124, + "time_signature": "4/4", + "title": "Source" + }, + "staves": [ + { + "arranged": true, + "arrangement_part_type": "vocal", + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "id": "staff_arranged_vocal", + "instruments": [], + "measures": [ + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_1_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_1", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "1", + "section_label": "Rhythm Lead-in", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_1_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_2_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_2", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "2", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_2_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_3_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_3", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "3", + "section_label": "Vocal Melody", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_3_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_3_n1_p1", + "letter": "c", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_3_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_3_n2_p1", + "letter": "d", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_3_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_3_n3_p1", + "letter": "e", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_3_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_3_n4_p1", + "letter": "f", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_4_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_4", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "4", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_4_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_4_n1_p1", + "letter": "c", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_4_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_4_n2_p1", + "letter": "d", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_4_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_4_n3_p1", + "letter": "e", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_4_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_4_n4_p1", + "letter": "f", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_5_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_5", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "5", + "section_label": "Guitar Melody", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_5_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_6_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_6", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "6", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_6_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_7_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_7", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "7", + "section_label": "Acoustic Solo", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_7_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_8_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_8", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "8", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_8_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_9_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_9", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "9", + "section_label": "Electric Solo", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_9_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_10_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_10", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "10", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_10_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_11_h1", + "kind": "minor-seventh", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "d" + }, + "symbol": "Dm7" + } + ], + "id": "vocal_melody_measure_11", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "11", + "section_label": "Vocal Melody", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_11_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_11_n1_p1", + "letter": "c", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_11_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_11_n2_p1", + "letter": "d", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_11_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_11_n3_p1", + "letter": "e", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_11_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_11_n4_p1", + "letter": "f", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_12_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_12", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "12", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "1", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_12_n1", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_12_n1_p1", + "letter": "c", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_12_n2", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 1 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_12_n2_p1", + "letter": "d", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_12_n3", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 2 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_12_n3_p1", + "letter": "e", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + }, + { + "duration": { + "denominator": 1, + "numerator": 1 + }, + "hidden": false, + "id": "vocal_melody_measure_12_n4", + "kind": "note", + "onset": { + "denominator": 1, + "numerator": 3 + }, + "pitches": [ + { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "duration": { + "denominator": 1, + "numerator": 1 + }, + "id": "vocal_melody_measure_12_n4_p1", + "letter": "f", + "octave": 4, + "written": { + "dots": 0, + "type": "quarter" + } + } + ], + "voice": "1", + "written": { + "dots": 0, + "type": "quarter" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [ + { + "degrees": [], + "id": "vocal_melody_measure_13_h1", + "kind": "dominant", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "root": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "g" + }, + "symbol": "G7" + } + ], + "id": "vocal_melody_measure_13", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "13", + "section_label": "Ending", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_13_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "vocal_melody_measure_14", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "14", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_14_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + }, + { + "attributes": [], + "clef": { + "line": 2, + "octave_change": 0, + "sign": "G" + }, + "duration": { + "denominator": 1, + "numerator": 4 + }, + "ending_sustain": true, + "harmonies": [], + "id": "vocal_melody_measure_15", + "implicit": false, + "key": { + "alterations": [ + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + }, + { + "denominator": 1, + "numerator": 0 + } + ], + "fifths": 0, + "mode": "major", + "tonic": { + "alter": { + "denominator": 1, + "numerator": 0 + }, + "letter": "c" + } + }, + "key_signature": "C", + "meter": { + "groups": [ + { + "beats": [ + 4 + ], + "unit": 4 + } + ] + }, + "number": "15", + "section_label": "", + "staff_details": { + "lines": 5, + "tunings": [] + }, + "tempos": [], + "time_signature": "4/4", + "transpose": { + "chromatic": 0, + "diatonic": 0, + "explicit": true, + "octave_change": -1 + }, + "voices": [ + { + "id": "voice_arranged", + "notes": [ + { + "duration": { + "denominator": 1, + "numerator": 4 + }, + "hidden": false, + "id": "vocal_melody_measure_15_n1", + "kind": "rest", + "onset": { + "denominator": 1, + "numerator": 0 + }, + "pitches": [], + "voice": "voice_arranged", + "written": { + "dots": 0, + "type": "whole" + } + } + ] + } + ] + } + ], + "name": "Vocal", + "octave_shifts": [], + "role": "vocal", + "tuplets": [] + } + ] + } +} diff --git a/tests/production_fixtures.py b/tests/production_fixtures.py index fd8124c..b66b766 100644 --- a/tests/production_fixtures.py +++ b/tests/production_fixtures.py @@ -11,7 +11,6 @@ import generator.harmonic_time import generator.solo.types import generator.solo.placement import generator.solo.profiles -import generator.solo.decoder import generator.solo.learned import generator.solo.harmonic import dataclasses @@ -45,6 +44,6 @@ def install(test): test.enterContext(unittest.mock.patch('generator.production.generate_tree', side_effect=serial)) test.enterContext(unittest.mock.patch('generator.vocabulary.load', return_value=resources)) test.enterContext(unittest.mock.patch('generator.solo.authored.lookup', return_value=None)) - test.enterContext(unittest.mock.patch('generator.production._solo_model', return_value=predictor)) - test.enterContext(unittest.mock.patch('generator.production._resource_key', return_value={'model': 'synthetic infrastructure fixture'})) + test.enterContext(unittest.mock.patch('generator.composition._solo_model', return_value=predictor)) + test.enterContext(unittest.mock.patch('generator.composition._resource_key', return_value={'model': 'synthetic infrastructure fixture'})) test.enterContext(unittest.mock.patch('generator.solo.composer.compose', side_effect=compose)) diff --git a/tests/solo_fixtures.py b/tests/solo_fixtures.py index 2ee41a6..2ae0b74 100644 --- a/tests/solo_fixtures.py +++ b/tests/solo_fixtures.py @@ -1,6 +1,6 @@ import copy, fractions import generator.model, generator.parts, generator.harmony -import generator.solo.analysis, generator.solo.composer, generator.solo.types +import generator.source, generator.solo.composer, generator.solo.types import tests.test_musicxml import generator.harmonic_time @@ -20,7 +20,7 @@ def song(meter=4, count=6, chords=('Dm7', 'G7', 'CM7')): return generator.parts.prepare_document(document) def compose(document, part='acoustic', cutoff=None): - analysis = generator.solo.analysis.analyze(document) + analysis = generator.source.analyze(document) measures = document['staves'][0]['measures'] end = sum(generator.model.fraction(m['duration']) for m in measures) if cutoff is None else cutoff return generator.solo.composer.compose(analysis, measures, part, end) diff --git a/tests/test_acoustic.py b/tests/test_acoustic.py index 09fa7a2..9923314 100644 --- a/tests/test_acoustic.py +++ b/tests/test_acoustic.py @@ -1,10 +1,10 @@ +import generator.validation import copy import fractions import pathlib import unittest import unittest.mock -import generator.accompaniment import generator.acoustic import generator.arrange.generate import generator.engraving.fonts @@ -41,19 +41,19 @@ def acoustic_document(bars): for note in notes: note.update(voice=lane, stem='down' if lane == 'bass' else 'up') measure['voices'].append({'id': lane, 'notes': notes}) - generator.model.validate_document(document) + generator.validation.validate_document(document) return document class AcousticTests(unittest.TestCase): def combine(self, document): generator.acoustic.prepare(document) - generator.model.validate_document(document) + generator.validation.validate_document(document) prepared = copy.deepcopy(document) generator.acoustic.prepare(document) self.assertEqual(document, prepared) before = sounding(document) generator.acoustic.combine(document) - generator.model.validate_document(document) + generator.validation.validate_document(document) self.assertEqual(sounding(document), before) self.assertTrue(all(len(m['voices']) == 1 for m in document['staves'][0]['measures'])) for measure in document['staves'][0]['measures']: @@ -130,7 +130,7 @@ class AcousticTests(unittest.TestCase): separate = builder(source, 'acoustic') combined = builder(source, 'acoustic') self.assertEqual(sounding(separate), sounding(combined)) - generator.model.validate_document(combined) + generator.validation.validate_document(combined) for measure in combined['staves'][0]['measures']: self.assertIn(len(measure['voices']), (1, 2)) if len(measure['voices']) == 2: diff --git a/tests/test_acoustic_pairs.py b/tests/test_acoustic_pairs.py index b86a39d..a85b875 100644 --- a/tests/test_acoustic_pairs.py +++ b/tests/test_acoustic_pairs.py @@ -1,4 +1,5 @@ """Acoustic attack assignment across musical, rather than storage, boundaries.""" +import generator.validation import copy import dataclasses import fractions @@ -108,7 +109,7 @@ class AcousticPairs(unittest.TestCase): source['staves'][0]['measures'] = rendered source['staves'][0].update(arrangement_part_type='acoustic', tablature=True) generator.acoustic.combine(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) generator.acoustic.validate_comping(source) attacks = [n for n in generator.score.measure_notes(rendered[2]) if n['pitches']] diff --git a/tests/test_acoustic_voicing.py b/tests/test_acoustic_voicing.py index 1ab25d8..3483018 100644 --- a/tests/test_acoustic_voicing.py +++ b/tests/test_acoustic_voicing.py @@ -1,3 +1,4 @@ +import generator.validation import copy import fractions import itertools @@ -7,11 +8,9 @@ import unittest import generator.accompaniment import generator.acoustic import generator.acoustic_voicing -import generator.arrange.generate import generator.fretboard import generator.harmony import generator.model -import generator.musicxml import tests.test_acoustic import tests.test_accompaniment @@ -79,7 +78,7 @@ class AcousticVoicingTests(unittest.TestCase): self.assertIn(strings, ([5], [6])) self.assertFalse(any(k.startswith('_') for k in note)) self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in note['pitches'])) - generator.model.validate_document(document) + generator.validation.validate_document(document) return spans def test_independent_attacks_keep_one_complete_voicing(self): document = planned_document([[(0, 'FM7')]]) @@ -89,7 +88,7 @@ class AcousticVoicingTests(unittest.TestCase): before = tests.test_acoustic.sounding(document) generator.acoustic.combine(document) self.assertEqual(tests.test_acoustic.sounding(document), before) - generator.model.validate_document(document) + generator.validation.validate_document(document) def test_four_strings_allow_all_defined_bass_inversions(self): for symbol in ('CM', 'Dm', 'Caug', 'Cdim', 'C7', 'Cdim7', 'C7no5', 'C7b9', 'C7no1', 'C6', 'C13', 'Csus', 'Csus2'): with self.subTest(chord=symbol): diff --git a/tests/test_arrangement_ending.py b/tests/test_arrangement_ending.py index 23258d1..7d43bff 100644 --- a/tests/test_arrangement_ending.py +++ b/tests/test_arrangement_ending.py @@ -1,11 +1,14 @@ """The held recording tail has exact score time and no vocal attacks.""" +import generator.arrange.form +import generator.composition +import generator.source +import generator.validation import copy import fractions import pathlib import unittest import generator.acoustic -import generator.arrange.ending import generator.arrange.generate import generator.fretboard import generator.harmony @@ -13,7 +16,6 @@ import generator.model import generator.musicxml import generator.parts import generator.performance -import generator.production import generator.score import tests.solo_fixtures @@ -45,7 +47,7 @@ class ArrangementEnding(unittest.TestCase): self.assertEqual(generator.model.fraction(timing['sustain_start']), 672) self.assertEqual(generator.model.fraction(timing['duration']), 684) self.assertEqual(len(timing['clicks']), 672) - generator.model.validate_document(result) + generator.validation.validate_document(result) generator.fretboard.validate_document(result) self.assertEqual(source, before) def test_ending_uses_final_meter_and_does_not_extend_solo_windows(self): @@ -54,10 +56,10 @@ class ArrangementEnding(unittest.TestCase): prepared = generator.parts.prepare_document(source) for part in ('acoustic', 'electric'): role = part + '_accompaniment' - result = generator.production.generate_role(generator.production.SourceAnalysis.read(prepared), role, pathlib.Path('data')) + result = generator.composition.generate_role(generator.source.SourceAnalysis.read(prepared), role, pathlib.Path('data')) tail = result['document']['staves'][0]['measures'][-3:] self.assertEqual([generator.model.fraction(m['duration']) for m in tail], [meter] * 3) - windows = generator.production.windows(prepared, part) + windows = generator.arrange.form.windows(prepared, part) self.assertEqual(windows[-1][2], role) self.assertEqual(windows[-1][1] - windows[-1][0], 3 * meter) self.assertEqual(sum(b - a for a, b, r in windows if r == part + '_solo'), 2 * meter) diff --git a/tests/test_arrangement_pickup.py b/tests/test_arrangement_pickup.py index 407e191..34b0868 100644 --- a/tests/test_arrangement_pickup.py +++ b/tests/test_arrangement_pickup.py @@ -1,9 +1,9 @@ +import generator.validation import copy import fractions import pathlib import unittest -import generator.arrange.form import generator.arrange.generate import generator.arrange.pickup import generator.model @@ -29,7 +29,7 @@ class ArrangementPickupTests(unittest.TestCase): for part in generator.parts.PART_TYPES: with self.subTest(song=path.stem, part=part): result = generator.arrange.generate.arrange_document(document, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] self.assertEqual(len(measures), 171) self.assertEqual([m['number'] for m in measures], [str(i) for i in range(1, 172)]) @@ -57,7 +57,7 @@ class ArrangementPickupTests(unittest.TestCase): for part, boundaries in (('vocal', (1, 5)), ('electric', (2,))): with self.subTest(part=part): result = generator.arrange.generate._arrange_document(generator.parts.prepare_document(document), part, owned={'melody', 'vocal'}) - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] self.assertEqual(len(measures), 9) for boundary in boundaries: @@ -84,13 +84,13 @@ class ArrangementPickupTests(unittest.TestCase): self.assertEqual(notes[1]['lyrics'][0]['syllables'][0]['text'], 'doo') self.assertEqual([generator.model.fraction(n['onset']) for n in notes], [0, 1]) document['staves'][0]['measures'] = [destination] - generator.model.validate_document(document) + generator.validation.validate_document(document) def test_pickup_harmonies_are_planned_for_every_part_at_the_same_offset(self): document = source(tests.test_parts.xml_harmony('F') + tests.test_musicxml.xml_note('F', 1, 'quarter')) prepared = generator.parts.prepare_document(document) for part in generator.parts.PART_TYPES: result = generator.arrange.generate.arrange_document(document, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) for index in (0, 1, 4): chords = result['staves'][0]['measures'][index]['harmonies'] self.assertEqual([(generator.model.fraction(h['onset']), h['root']['letter']) for h in chords], [(0, 'c'), (3, 'f')]) @@ -102,7 +102,7 @@ class ArrangementPickupTests(unittest.TestCase): document = source(notes, divisions=3) for part in ('vocal', 'electric'): result = generator.arrange.generate.arrange_document(document, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) spans = result['staves'][0]['tuplets'] self.assertEqual(len(spans), 2 if part == 'vocal' else 1) self.assertEqual(len({s['id'] for s in spans}), len(spans)) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 738e24e..8df9c68 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,10 +1,10 @@ +import generator.production import tests.production_fixtures import json import pathlib import tempfile import unittest -import generator.arrange.generate import tests.solo_fixtures import tests.test_musicxml @@ -16,7 +16,7 @@ class CatalogTests(unittest.TestCase): source, output = pathlib.Path(temporary) / 'songs', pathlib.Path(temporary) / 'output' source.mkdir() (source / 'song.xml').write_text(tests.test_musicxml.xml_score('E-1major' + tests.test_musicxml.xml_note('E', alter=-1), '-3')) - self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output), 3) + self.assertEqual(generator.production.generate_tree(source, output), 3) self.assertEqual({p.name for p in output.iterdir()}, {'song_acoustic.json', 'song_electric.json', 'song_vocal.json'}) for path in output.iterdir(): metadata = json.loads(path.read_text())['metadata'] diff --git a/tests/test_chord_families.py b/tests/test_chord_families.py index b640378..8ebdf1e 100644 --- a/tests/test_chord_families.py +++ b/tests/test_chord_families.py @@ -1,18 +1,17 @@ """General chord families and one-voice acoustic rhythm, not song overrides.""" +import generator.composition +import generator.validation import copy import fractions import unittest import generator.acoustic -import generator.accompaniment import generator.chord_families import generator.comping import generator.comping_rhythm import generator.fretboard -import generator.guitar import generator.harmony import generator.model -import generator.production import generator.vocabulary import tests.solo_fixtures import tests.test_acoustic @@ -89,8 +88,8 @@ class ChordFamilies(unittest.TestCase): changed = copy.deepcopy(resources) changed['accompaniment']['instruments']['acoustic']['patterns'][0]['events'][0]['duration'] = '1/2' for role in ('electric_accompaniment', 'electric_melody', 'electric_solo', 'vocal_melody'): - self.assertEqual(generator.production.dependencies(role, resources), generator.production.dependencies(role, changed)) - self.assertNotEqual(generator.production.dependencies('acoustic_accompaniment', resources), generator.production.dependencies('acoustic_accompaniment', changed)) + self.assertEqual(generator.composition.dependencies(role, resources), generator.composition.dependencies(role, changed)) + self.assertNotEqual(generator.composition.dependencies('acoustic_accompaniment', resources), generator.composition.dependencies('acoustic_accompaniment', changed)) class CombinedAcoustic(unittest.TestCase): def document(self, bars): @@ -107,7 +106,7 @@ class CombinedAcoustic(unittest.TestCase): def convert(self, document): before = tests.test_acoustic.sounding(document) generator.acoustic.combine(document) - generator.model.validate_document(document) + generator.validation.validate_document(document) generator.fretboard.validate_document(document) self.assertEqual(tests.test_acoustic.sounding(document), before) self.assertTrue(all(len(m['voices']) == 1 for m in document['staves'][0]['measures'])) diff --git a/tests/test_comping_cycles.py b/tests/test_comping_cycles.py index 64dfb26..6d5e3ea 100644 --- a/tests/test_comping_cycles.py +++ b/tests/test_comping_cycles.py @@ -1,4 +1,8 @@ """Recurring source-position grooves, fills and chord-first bass behavior.""" +import generator.composition_types +import generator.composition +import generator.source +import generator.validation import concurrent.futures import copy import dataclasses @@ -213,7 +217,7 @@ class RecurringGrooves(unittest.TestCase): source['staves'][0].update(arrangement_part_type='acoustic', tablature=True) generator.acoustic.validate_comping(source) generator.acoustic.combine(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) generator.acoustic.validate_comping(source) first = source['staves'][0]['measures'][0] @@ -239,26 +243,26 @@ class RecurringGrooves(unittest.TestCase): r = generator.comping_rhythm.realize(pattern, inputs, 'acoustic', generator.comping_rhythm.melody_contexts(source)) self.assertTrue(all(e.lane == 'combined' for e in r.events)) def test_clean_cached_parallel_and_other_role_settings_keep_groove(self): - source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=10, chords=('C7', 'FM7'))) + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=10, chords=('C7', 'FM7'))) root = pathlib.Path('data').resolve() roles = ('acoustic_accompaniment', 'electric_accompaniment') - clean = {r: generator.production.generate_role(source, r, root) for r in roles} + clean = {r: generator.composition.generate_role(source, r, root) for r in roles} with tempfile.TemporaryDirectory() as directory: for r in roles: - cold = generator.production.generate_role(source, r, root, cache_dir=directory) - warm = generator.production.generate_role(source, r, root, cache_dir=directory) + cold = generator.composition.generate_role(source, r, root, cache_dir=directory) + warm = generator.composition.generate_role(source, r, root, cache_dir=directory) self.assertTrue(warm['cache_hit']) self.assertEqual(clean[r]['document'], cold['document']) self.assertEqual(clean[r]['document'], warm['document']) - generator.production.generate_role(source, roles[1], root, {'seed': 'different'}, directory) - self.assertEqual(clean[roles[0]]['document'], generator.production.generate_role(source, roles[0], root, cache_dir=directory)['document']) + generator.composition.generate_role(source, roles[1], root, {'seed': 'different'}, directory) + self.assertEqual(clean[roles[0]]['document'], generator.composition.generate_role(source, roles[0], root, cache_dir=directory)['document']) with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool: - futures = {r: pool.submit(generator.production._job, (source, r, root, {}, None)) for r in reversed(roles)} + futures = {r: pool.submit(generator.production._job, generator.composition_types.RoleRequest(source, r, root)) for r in reversed(roles)} for r, future in futures.items(): self.assertEqual(clean[r]['document'], future.result()['document']) def test_reuse_audit_checks_rests_and_all_six_roles(self): source = tests.solo_fixtures.song(count=2, chords=('C7',)) - for role in generator.production.ROLES: + for role in generator.composition_types.ROLES: part = role.split('_')[0] wanted = 'slash' if role.endswith('_solo') else 'vocal' if part == 'vocal' else 'melody' if role.endswith('_melody') else 'accompaniment' section = generator.arrange.form.ArrangementSection('Repeated role', 0, 2, 1, wanted) diff --git a/tests/test_comping_meters.py b/tests/test_comping_meters.py index 94a8a21..bd3db57 100644 --- a/tests/test_comping_meters.py +++ b/tests/test_comping_meters.py @@ -1,4 +1,7 @@ """Meter-specific vocabulary and source-derived quintuple grouping.""" +import generator.composition +import generator.harmony +import generator.source import concurrent.futures import copy import fractions @@ -159,15 +162,15 @@ class CompingMeters(unittest.TestCase): def test_unused_meter_and_other_role_data_do_not_reseed(self): resources = generator.vocabulary.load() measures = self.source(4)['staves'][0]['measures'] - baseline = generator.production.dependencies('acoustic_accompaniment', resources, measures) + baseline = generator.composition.dependencies('acoustic_accompaniment', resources, measures) changed = copy.deepcopy(resources) for role, policy in changed['accompaniment']['instruments'].items(): for p in policy['patterns']: if role == 'electric' or p['meter'] in ('3/4', '5/4'): p['events'] = [] - self.assertEqual(baseline, generator.production.dependencies('acoustic_accompaniment', changed, measures)) + self.assertEqual(baseline, generator.composition.dependencies('acoustic_accompaniment', changed, measures)) changed['accompaniment']['instruments']['acoustic']['patterns'][0]['events'] = [] - self.assertNotEqual(baseline, generator.production.dependencies('acoustic_accompaniment', changed, measures)) + self.assertNotEqual(baseline, generator.composition.dependencies('acoustic_accompaniment', changed, measures)) def test_chord_changes_anticipations_and_cross_bar_ties(self): for beats, grouping in ((4, ()), (3, ()), (5, (3, 2)), (5, (2, 3))): source = self.source(beats, 2, grouping) @@ -188,18 +191,18 @@ class CompingMeters(unittest.TestCase): self.assertTrue(anticipated) def test_roles_are_deterministic_alone_cached_and_parallel(self): for meter in (3, 5): - source = generator.production.SourceAnalysis.read(self.source(meter, 9)) + source = generator.source.SourceAnalysis.read(self.source(meter, 9)) roles = ('acoustic_accompaniment', 'electric_accompaniment') with tempfile.TemporaryDirectory() as cache: - clean = {r: generator.production.generate_role(source, r, pathlib.Path('data')) for r in roles} + clean = {r: generator.composition.generate_role(source, r, pathlib.Path('data')) for r in roles} for role in reversed(roles): - cold = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=cache) - warm = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=cache) + cold = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=cache) + warm = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=cache) self.assertEqual(clean[role]['document'], cold['document']) self.assertEqual(cold['document'], warm['document']) self.assertTrue(warm['cache_hit']) with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool: - jobs = {r: pool.submit(generator.production._job, (source, r, pathlib.Path('data'), {}, None)) for r in reversed(roles)} + jobs = {r: pool.submit(generator.production._job, generator.composition_types.RoleRequest(source, r, pathlib.Path('data'))) for r in reversed(roles)} for role, job in jobs.items(): self.assertEqual(clean[role]['document'], job.result()['document']) diff --git a/tests/test_comping_rhythm.py b/tests/test_comping_rhythm.py index 080e9f6..1defc6c 100644 --- a/tests/test_comping_rhythm.py +++ b/tests/test_comping_rhythm.py @@ -1,4 +1,8 @@ """Independent rhythm contracts and their complete sounding durations.""" +import generator.composition_types +import generator.arrange.assembly +import generator.composition +import generator.validation import copy import dataclasses import fractions @@ -14,9 +18,8 @@ import generator.fretboard import generator.harmonic_time import generator.harmony import generator.model -import generator.production import generator.score -import generator.solo.analysis +import generator.source import generator.solo.decoder import generator.solo.harmonic import generator.solo.learned @@ -187,17 +190,17 @@ class LayeredComping(unittest.TestCase): changed = copy.deepcopy(self.source) changed['metadata']['tempo'] = 240 changed['staves'][0]['measures'][0]['tempos'] = [{'onset': generator.model.rational(0), 'bpm': generator.model.rational(240), 'written': {'type': 'quarter', 'dots': 0}}] - a, b = map(generator.production.SourceAnalysis.read, (self.source, changed)) + a, b = map(generator.source.SourceAnalysis.read, (self.source, changed)) self.assertEqual(a.accompaniment_key(), b.accompaniment_key()) for role in ('acoustic_accompaniment', 'electric_accompaniment'): - first = generator.production.generate_role(a, role, pathlib.Path('data')) - second = generator.production.generate_role(b, role, pathlib.Path('data')) + first = generator.composition.generate_role(a, role, pathlib.Path('data')) + second = generator.composition.generate_role(b, role, pathlib.Path('data')) self.assertEqual(sounding(first['document']), sounding(second['document'])) self.assertEqual(first['details'], second['details']) self.assertNotEqual(first['trace']['cache_key'], second['trace']['cache_key']) def test_assembly_keeps_releases_and_canonical_reuse(self): - source = generator.production.SourceAnalysis.read(self.source) - results = {role: generator.production.generate_role(source, role, pathlib.Path('data')) for role in generator.production.ROLES if not role.endswith('_solo')} + source = generator.source.SourceAnalysis.read(self.source) + results = {role: generator.composition.generate_role(source, role, pathlib.Path('data')) for role in generator.composition_types.ROLES if not role.endswith('_solo')} # Silence in source-defined solo slots is sufficient to test union; # no generated solo participates in these accompaniment decisions. for part in ('acoustic', 'electric'): @@ -207,12 +210,12 @@ class LayeredComping(unittest.TestCase): for n in v['notes']: n.update(kind='rest', pitches=[]) results[part + '_solo'] = empty - assembled = generator.production.assemble(source, results) + assembled = generator.arrange.assembly.assemble(source, results) self.assertTrue(all(len(m['voices']) == 1 for m in assembled['acoustic']['staves'][0]['measures'])) audit = generator.arrange.accompaniment.audit(self.source, assembled) self.assertGreater(audit['acoustic']['reused_bars'], 0) for doc in assembled.values(): - generator.model.validate_document(doc) + generator.validation.validate_document(doc) generator.fretboard.validate_document(doc) def test_combined_voices_reject_conflicting_strings_and_fret_spans(self): for string, fret in ((5, 3), (2, 10)): @@ -266,7 +269,7 @@ class SoloRhythmConditioning(unittest.TestCase): if held: m = source['staves'][0]['measures'][0] m['voices'][0]['notes'] = [generator.model.make_event('held', fractions.Fraction(), fractions.Fraction(4), 'note', [{'letter': 'c', 'alter': 0, 'octave': 4}])] - analysis = generator.solo.analysis.analyze(source) + analysis = generator.source.analyze(source) harmony = generator.solo.harmonic.Harmony(generator.harmonic_time.read(source['staves'][0])) examples.append({'analysis': analysis, 'harmony': harmony, 'events': [(fractions.Fraction(), fractions.Fraction(duration), 64), (fractions.Fraction(duration), fractions.Fraction(4), 67)]}) model = generator.solo.learned.Model(generator.solo.learned.fit(examples, {'purpose': 'independently authored conditional-rhythm test'})) diff --git a/tests/test_comping_voicings.py b/tests/test_comping_voicings.py index 8b7c4c7..19d6bbc 100644 --- a/tests/test_comping_voicings.py +++ b/tests/test_comping_voicings.py @@ -1,4 +1,6 @@ """Accompaniment geometry and whole-form movement, using authored small cases.""" +import generator.composition +import generator.source import copy import fractions import itertools @@ -14,7 +16,6 @@ import generator.fretboard import generator.guitar import generator.harmony import generator.model -import generator.production import generator.score import generator.vocabulary import tests.solo_fixtures @@ -43,13 +44,13 @@ class CompingVoicings(unittest.TestCase): path, _ = generator.comping.select_grips(((low, high),), (costs,), 'electric', 'lower', left=high, right=high) self.assertEqual(path, (high,)) def test_electric_voicing_policy_does_not_reseed_rhythm(self): - source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('C7', 'FM7', 'Dm7', 'G7'))) + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('C7', 'FM7', 'Dm7', 'G7'))) root = pathlib.Path('data') - current = generator.production.generate_role(source, 'electric_accompaniment', root) + current = generator.composition.generate_role(source, 'electric_accompaniment', root) with tempfile.TemporaryDirectory() as cache: settings = {'voicing': {'register_weight': 0, 'high_position_weight': 0}} - other = generator.production.generate_role(source, 'electric_accompaniment', root, settings, cache) - warm = generator.production.generate_role(source, 'electric_accompaniment', root, settings, cache) + other = generator.composition.generate_role(source, 'electric_accompaniment', root, settings, cache) + warm = generator.composition.generate_role(source, 'electric_accompaniment', root, settings, cache) self.assertTrue(warm['cache_hit']) self.assertEqual(other['document'], warm['document']) self.assertNotEqual(current['trace']['cache_key'], other['trace']['cache_key']) @@ -99,7 +100,7 @@ class CompingVoicings(unittest.TestCase): with generator.vocabulary.using(resources, 'acoustic_accompaniment', {'voicing': {'register_weight': -1}}), self.assertRaises(ValueError): generator.comping.local_cost('acoustic', key, high, 53) def test_production_voicing_policy_changes_cache_not_rhythm_or_electric(self): - source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=6, chords=('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7'))) + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=6, chords=('Em7b5', 'A7b9', 'Dm7', 'G7', 'Cm7', 'F7'))) def rhythm(result): return [[(n['onset'], n['duration'], n['written'], n['kind'], len(n['pitches']), tuple((bool(p.get('tie_from')), bool(p.get('tie_to'))) for p in n['pitches'])) for n in generator.score.measure_notes(m)] for m in result['document']['staves'][0]['measures']] with tempfile.TemporaryDirectory() as directory: @@ -109,14 +110,14 @@ class CompingVoicings(unittest.TestCase): data = json.loads(role_path.read_text()) policy = data['roles']['acoustic_accompaniment'].pop('voicing') role_path.write_text(json.dumps(data)) - legacy = generator.production.generate_role(source, 'acoustic_accompaniment', root) - electric = generator.production.generate_role(source, 'electric_accompaniment', root) + legacy = generator.composition.generate_role(source, 'acoustic_accompaniment', root) + electric = generator.composition.generate_role(source, 'electric_accompaniment', root) data['roles']['acoustic_accompaniment']['voicing'] = policy role_path.write_text(json.dumps(data)) - current = generator.production.generate_role(source, 'acoustic_accompaniment', root) + current = generator.composition.generate_role(source, 'acoustic_accompaniment', root) settings = {'voicing': {'register_weight': 0, 'high_position_weight': 0, 'comfortable_max_fret': 9}} - old = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache') - warm = generator.production.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache') + old = generator.composition.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache') + warm = generator.composition.generate_role(source, 'acoustic_accompaniment', root, settings, pathlib.Path(directory) / 'cache') self.assertTrue(warm['cache_hit']) self.assertEqual(old['document'], warm['document']) self.assertNotEqual(current['document'], old['document']) @@ -125,7 +126,7 @@ class CompingVoicings(unittest.TestCase): self.assertEqual(legacy['document'], current['document']) self.assertEqual(rhythm(current), rhythm(old)) self.assertEqual(current['details']['accompaniment_rhythm'], old['details']['accompaniment_rhythm']) - self.assertEqual(electric['document'], generator.production.generate_role(source, 'electric_accompaniment', root)['document']) + self.assertEqual(electric['document'], generator.composition.generate_role(source, 'electric_accompaniment', root)['document']) def test_report_balanced_cost_matches_selected_path(self): canonical = generator.comping.Canonical(tests.solo_fixtures.song(count=4, chords=('Em7b5', 'A7b9', 'Dm7', 'G7')), 'acoustic', 'report') rows = [r for r in canonical.report if r['tones']] @@ -242,13 +243,13 @@ class CompingVoicings(unittest.TestCase): high = generator.comping.Canonical(source, part, 'fixed-rhythm') self.assertEqual(rhythm(low), rhythm(high)) def test_accompaniment_cache_matches_clean_generation(self): - source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=3)) + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=3)) for part in ('acoustic', 'electric'): with tempfile.TemporaryDirectory() as directory: role = part + '_accompaniment' - cold = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory) - warm = generator.production.generate_role(source, role, pathlib.Path('data'), cache_dir=directory) - clean = generator.production.generate_role(source, role, pathlib.Path('data')) + cold = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=directory) + warm = generator.composition.generate_role(source, role, pathlib.Path('data'), cache_dir=directory) + clean = generator.composition.generate_role(source, role, pathlib.Path('data')) self.assertTrue(warm['cache_hit']) self.assertEqual(cold['document'], warm['document']) self.assertEqual(cold['document'], clean['document']) diff --git a/tests/test_electric_voicing.py b/tests/test_electric_voicing.py index 6afbffd..4af4361 100644 --- a/tests/test_electric_voicing.py +++ b/tests/test_electric_voicing.py @@ -1,10 +1,10 @@ +import generator.validation import copy import fractions import pathlib import unittest import unittest.mock -import generator.arrange.form import generator.arrange.generate import generator.electric import generator.fretboard @@ -110,6 +110,6 @@ class ElectricVoicingTests(unittest.TestCase): before = copy.deepcopy(source) with unittest.mock.patch('generator.electric.reuse_voicings', side_effect=AssertionError('cross-part grip dependency')) as reuse: result = generator.arrange.generate.arrange_document(source, 'electric') - generator.model.validate_document(result) + generator.validation.validate_document(result) reuse.assert_not_called() self.assertEqual(source, before) diff --git a/tests/test_engraving_cache.py b/tests/test_engraving_cache.py index ec88c37..fadc5f8 100644 --- a/tests/test_engraving_cache.py +++ b/tests/test_engraving_cache.py @@ -1,3 +1,4 @@ +import generator.score_io import tests.production_fixtures import argparse import contextlib @@ -19,11 +20,11 @@ import generator.engraving.cache import generator.engraving.fonts import generator.engraving.layout import generator.engraving.pipeline +import generator.engraving.worker import generator.engraving.raster import generator.engraving.types import generator.engraving.validation import generator.pico_artwork -import generator.score import tests.solo_fixtures import tests.test_musicxml @@ -173,8 +174,8 @@ class EngravingCacheTests(unittest.TestCase): style = generator.engraving.types.Style() documents = [self.document, tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('D')))] pages = [generator.engraving.layout.layout_score(document, style, family).pages[0] for document in documents] - result = generator.engraving.pipeline.GroupResult() - bands = generator.engraving.pipeline.BodyBands(self.root / 'spool', None, result, limit=1) + result = generator.engraving.worker.GroupResult() + bands = generator.engraving.worker.BodyBands(self.root / 'spool', None, result, limit=1) self.addCleanup(bands.close) original = None for index in (0, 1, 0): @@ -338,7 +339,7 @@ class EngravingCacheTests(unittest.TestCase): reads.append(path) return read_text(path, *args, **kwargs) with unittest.mock.patch.object(pathlib.Path, 'read_text', read): - generator.score.validate_generated_tree(root) + generator.score_io.validate_generated_tree(root) self.assertCountEqual(reads, sorted(root.rglob('*.json'))) def test_invalid_worker_count_and_overlapping_cache_are_rejected(self): root = self.root / 'assets' diff --git a/tests/test_fret_policy.py b/tests/test_fret_policy.py index cfcaed9..425301a 100644 --- a/tests/test_fret_policy.py +++ b/tests/test_fret_policy.py @@ -1,3 +1,5 @@ +import generator.score_io +import generator.validation import copy import fractions import pathlib @@ -14,9 +16,6 @@ import generator.fretboard import generator.guitar import generator.harmony import generator.model -import generator.score -import generator.solo.analysis -import generator.solo.placement import tests.solo_fixtures import tests.test_acoustic_voicing import tests.test_musicxml @@ -97,7 +96,7 @@ class FretPolicyTests(unittest.TestCase): self.assertFalse(generator.fretboard.span_penalty((position(4, 5), position(3, 6)))) before = tests.score_events.timeline(source) generator.guitar.place_guitar(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) self.assertEqual(tests.score_events.timeline(source), before) self.assertEqual([p['tablature'] for p in measure['voices'][0]['notes'][0]['pitches']], [{'string': 4, 'fret': 5}, {'string': 2, 'fret': 2}]) @@ -114,7 +113,7 @@ class FretPolicyTests(unittest.TestCase): states = generator.guitar.advance_positions([n], fractions.Fraction(2 * i), tones, states, True) self.assertEqual(min(s.cost for s in states.values()).extended_grips, 1) generator.guitar.place_guitar(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) self.assertEqual([p['tablature'] for p in pieces[0]['pitches']], [p['tablature'] for p in pieces[1]['pitches']]) # Removing the low anchor early still leaves an illegal high single tail. @@ -151,7 +150,7 @@ class FretPolicyTests(unittest.TestCase): source = document(tuple(position(s, f) for s, f in zip((2, 1), frets))) if valid: generator.guitar.place_guitar(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) else: with self.assertRaises(ValueError): @@ -162,7 +161,7 @@ class FretPolicyTests(unittest.TestCase): source = document((position(2, 12), position(1, 14))) anchor = tests.test_musicxml.notes(source)[0]['pitches'][0] anchor.update(duration=generator.model.rational(2), written=generator.model.written_for(fractions.Fraction(2))) - generator.model.validate_document(source) + generator.validation.validate_document(source) with self.assertRaisesRegex(ValueError, 'held/tied tails'): generator.fretboard.validate_document(source) with self.assertRaisesRegex(ValueError, 'no guitar placement'): @@ -175,12 +174,12 @@ class FretPolicyTests(unittest.TestCase): a['tie_to'], b['tie_from'] = b['id'], a['id'] source['staves'][0]['measures'][0]['voices'][0]['notes'] = split generator.guitar.place_guitar(source) - generator.model.validate_document(source) + generator.validation.validate_document(source) generator.fretboard.validate_document(source) self.assertEqual([p['tablature'] for p in split[0]['pitches']], [p['tablature'] for p in split[1]['pitches']]) split[0]['pitches'][0].pop('tie_to') split[1]['pitches'].pop(0) - generator.model.validate_document(source) + generator.validation.validate_document(source) with self.assertRaisesRegex(ValueError, 'held/tied tails'): generator.fretboard.validate_document(source) def test_another_voice_cannot_supply_a_single_notes_anchor(self): @@ -221,9 +220,9 @@ class FretPolicyTests(unittest.TestCase): target = document(tuple(position(s, 12) for s in (3, 2, 1))) reference, note = tests.test_musicxml.notes(source)[0], tests.test_musicxml.notes(target)[0] generator.electric.reuse_voicings([generator.electric.VoicingLink(note, (reference,))]) - generator.model.validate_document(target) + generator.validation.validate_document(target) with tempfile.TemporaryDirectory() as directory: output = pathlib.Path(directory) / 'score.json' with self.assertRaisesRegex(ValueError, 'each sounding chord requires an anchor'): - generator.score.write_json(output, target) + generator.score_io.write_json(output, target) self.assertFalse(output.exists()) diff --git a/tests/test_generator_boundaries.py b/tests/test_generator_boundaries.py new file mode 100644 index 0000000..91f529e --- /dev/null +++ b/tests/test_generator_boundaries.py @@ -0,0 +1,64 @@ +"""Dependency direction, cache coverage and explicit composition inputs.""" +import ast +import json +import pathlib +import subprocess +import sys +import unittest +import unittest.mock + +import generator.cache +import generator.composition +import generator.role_cache +import generator.source +import generator.vocabulary +import tests.solo_fixtures + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +class GeneratorBoundaries(unittest.TestCase): + def test_generator_imports_are_acyclic(self): + files = {'.'.join(path.relative_to(ROOT).with_suffix('').parts): path for path in (ROOT / 'generator').rglob('*.py')} + graph = {name: set(generator.cache.imports(path.read_bytes())) & files.keys() for name, path in files.items()} + visited, active = set(), [] + def visit(name): + self.assertNotIn(name, active, ' -> '.join([*active, name])) + if name in visited: + return + active.append(name) + for dependency in graph[name]: + visit(dependency) + active.pop() + visited.add(name) + for name in graph: + visit(name) + def test_composition_import_does_not_load_rendering_or_publication(self): + code = "import generator.composition, sys, json; print(json.dumps(sorted(sys.modules)))" + result = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, check=True) + forbidden = ('PIL', 'fontTools', 'generator.engraving', 'generator.production', 'generator.score_io') + self.assertFalse([name for name in json.loads(result.stdout) if name.startswith(forbidden)]) + def test_all_previously_missing_role_dependencies_change_the_fingerprint(self): + baseline = generator.role_cache.code_key() + read = pathlib.Path.read_bytes + for name in ('solo/policy.py', 'source.py', 'accompaniment.py'): + changed = ROOT / 'generator' / name + with self.subTest(dependency=name): + def contents(path): + return read(path) + (b'\n# changed implementation\n' if path == changed else b'') + with unittest.mock.patch.object(pathlib.Path, 'read_bytes', contents): + self.assertNotEqual(generator.role_cache.code_key(), baseline) + def test_context_owns_immutable_resources_and_survives_caller_mutation(self): + resources = generator.vocabulary.load() + context = generator.vocabulary.CompositionContext.create(resources, 'electric_accompaniment') + expected = context.role()['center'] + resources['roles']['roles']['electric_accompaniment']['center'] = 999 + self.assertEqual(context.role()['center'], expected) + with self.assertRaises(TypeError): + context.data('roles')['roles']['electric_accompaniment']['center'] = 999 + self.assertNotEqual(generator.vocabulary.load()['roles']['roles']['electric_accompaniment']['center'], 999) + def test_role_generation_does_not_consult_ambient_policy(self): + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',))) + with unittest.mock.patch('generator.vocabulary.current', side_effect=AssertionError('implicit policy dependency')): + for role in generator.composition_types.ROLES: + with self.subTest(role=role): + generator.composition.generate_role(source, role, ROOT / 'data', {'predict': True}) diff --git a/tests/test_guitar.py b/tests/test_guitar.py index 098260c..e57b400 100644 --- a/tests/test_guitar.py +++ b/tests/test_guitar.py @@ -1,3 +1,5 @@ +import generator.production +import generator.validation import tests.production_fixtures import argparse import copy @@ -74,7 +76,7 @@ class GuitarTests(unittest.TestCase): self.assertEqual(document, before) document['staves'][0]['tablature'] = True with self.assertRaisesRegex(ValueError, 'invalid generated tablature'): - generator.model.validate_document(document) + generator.validation.validate_document(document) def test_theory_controls_omissions_and_bass(self): for symbol in ('CM', 'G7b9', 'FM7#11/A', 'CmM7', 'Cdim7', 'C13', 'Csus', 'Cmadd9'): with self.subTest(symbol=symbol): @@ -135,7 +137,7 @@ class GuitarTests(unittest.TestCase): before = copy.deepcopy(source) electric = tests.arrangement_fixtures.section_document(source, 'electric') vocal = tests.arrangement_fixtures.section_document(source, 'vocal') - generator.model.validate_document(electric) + generator.validation.validate_document(electric) self.assertEqual(source, before) self.assertEqual([(e['onset'], e['duration'], generator.model.fraction(e['pitch']) - 12) for e in tests.score_events.timeline(electric)['events']], [(e['onset'], e['duration'], generator.model.fraction(e['pitch'])) for e in tests.score_events.timeline(vocal)['events']]) self.assertEqual([(e['pitch'], e['onset'], e['duration']) for e in tests.score_events.timeline(electric)['events']], [(e['pitch'], e['onset'], e['duration']) for e in tests.score_events.timeline(source)['events']]) @@ -156,7 +158,7 @@ class GuitarTests(unittest.TestCase): self.assertEqual(generator.model.pitch_number(tone), 12 * (octave + 1) + alter) self.assertEqual(tone['tablature'], tab) self.assertEqual(generator.model.fraction(tests.score_events.timeline(document)['events'][0]['pitch']), expected) - generator.model.validate_document(document) + generator.validation.validate_document(document) def test_guitar_normalization_is_idempotent_and_respects_timed_transposition(self): change = '0' source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=2, kind='half') + change + tests.test_musicxml.xml_note(duration=2, kind='half'))) @@ -173,7 +175,7 @@ class GuitarTests(unittest.TestCase): for part in generator.parts.PART_TYPES: with self.subTest(generator=generate.__name__, part=part): document = generate(source, part) - generator.model.validate_document(document) + generator.validation.validate_document(document) written = {p['id']: generator.model.pitch_number(p) for m in document['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] for p in n['pitches']} events = tests.score_events.timeline(document)['events'] self.assertTrue(events) @@ -231,7 +233,7 @@ class GuitarTests(unittest.TestCase): chord['pitches'] = [] chord['_harmony'] = generator.harmony.parse_symbol('CM') generator.guitar.place_guitar(document) - generator.model.validate_document(document) + generator.validation.validate_document(document) self.assertEqual(melody['pitches'][0]['tablature'], {'string': 1, 'fret': 12}) self.assertNotIn(1, [p['tablature']['string'] for p in chord['pitches']]) self.assertEqual(len(tests.score_events.timeline(document)['events']), 3) @@ -256,9 +258,9 @@ class GuitarTests(unittest.TestCase): source, output = pathlib.Path(temporary) / 'source', pathlib.Path(temporary) / 'output' source.mkdir() (source / '0000.xml').write_text(tests.test_musicxml.xml_score('E-1major' + tests.test_musicxml.xml_note('E', alter=-1), '-3')) - self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output), 3) + self.assertEqual(generator.production.generate_tree(source, output), 3) first = {p.name: p.read_bytes() for p in output.rglob('*.json')} - generator.arrange.generate.generate_performance_scores(source, output) + generator.production.generate_tree(source, output) self.assertEqual(first, {p.name: p.read_bytes() for p in output.rglob('*.json')}) def test_unplayable_generation_preserves_previous_assets(self): with tempfile.TemporaryDirectory() as temporary: diff --git a/tests/test_guitar_parts.py b/tests/test_guitar_parts.py index 3bd6c3e..01c3b08 100644 --- a/tests/test_guitar_parts.py +++ b/tests/test_guitar_parts.py @@ -1,15 +1,14 @@ +import generator.score +import generator.validation import copy import fractions import unittest -import generator.accompaniment import generator.acoustic_voicing -import generator.arrange.generate import generator.fretboard import generator.guitar import generator.harmony import generator.model -import generator.parts import tests.score_events import tests.arrangement_fixtures import tests.test_accompaniment @@ -20,7 +19,7 @@ class GuitarPartTests(unittest.TestCase): def section(self, content, part='electric', divisions=1): source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=divisions)) result = tests.arrangement_fixtures.section_document(source, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) return result def test_profile_shapes_cover_defining_chord_tones(self): for symbol in ('CM', 'Cm', 'C7', 'CmM7', 'Csus', 'Cdim7', 'Caug7', 'C7b9', 'C13', 'CM/E'): @@ -50,7 +49,7 @@ class GuitarPartTests(unittest.TestCase): self.assertTrue(all({p.midi % 12 for p in c} == {0, 7} for c in candidates)) def test_acoustic_uses_bass_and_three_upper_notes(self): result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic') - generator.model.validate_document(result) + generator.validation.validate_document(result) generator.fretboard.validate_document(result) sounding = [n for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m) if n['pitches']] self.assertTrue(sounding) @@ -99,7 +98,7 @@ class GuitarPartTests(unittest.TestCase): second = tests.test_parts.xml_harmony(root) + tests.test_musicxml.xml_note('E', octave=4, extra='') source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([first, second])) result = tests.arrangement_fixtures.section_document(source, 'electric') - generator.model.validate_document(result) + generator.validation.validate_document(result) notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes']] self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes)) self.assertEqual(bool(notes[0]['pitches'][0].get('tie_to')), root == 'C') diff --git a/tests/test_harmonic_recognition.py b/tests/test_harmonic_recognition.py index 92244c4..70e638b 100644 --- a/tests/test_harmonic_recognition.py +++ b/tests/test_harmonic_recognition.py @@ -7,15 +7,13 @@ import unittest import generator.train import generator.training_io -import generator.vocabulary import generator.harmonic_time import generator.harmony import generator.solo.harmonic import generator.solo.types import generator.solo.decoder import generator.solo.learned -import generator.solo.analysis -import generator.solo.expressions +import generator.source import tests.solo_fixtures import generator.musicxml import generator.parts @@ -128,7 +126,7 @@ class ContextualHarmony(unittest.TestCase): class PredictionReachability(unittest.TestCase): def engine(self, chords): doc = tests.solo_fixtures.song(count=2, chords=chords) - return generator.solo.decoder.Decoder(generator.solo.analysis.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8)) + return generator.solo.decoder.Decoder(generator.source.analyze(doc), generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(chords)), generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8)) def test_joint_resolved_color_reaches_the_real_action_builder(self): d = self.engine(('C7', 'FM7')) n = d.note(fractions.Fraction(), fractions.Fraction(3, 2), 66) diff --git a/tests/test_harmony.py b/tests/test_harmony.py index f5e3dcb..ad5a235 100644 --- a/tests/test_harmony.py +++ b/tests/test_harmony.py @@ -6,7 +6,6 @@ import generator.engraving.fonts import generator.engraving.layout import generator.engraving.types import generator.harmony -import generator.model import generator.musicxml import generator.musicxml_writer import generator.pitch diff --git a/tests/test_independent_generation.py b/tests/test_independent_generation.py index 013d2bf..9480b6f 100644 --- a/tests/test_independent_generation.py +++ b/tests/test_independent_generation.py @@ -1,4 +1,9 @@ """Dependency and music contracts use synthetic sources, never audition scores.""" +import generator.composition_types +import generator.arrange.assembly +import generator.composition +import generator.source +import generator.validation import concurrent.futures import copy import contextlib @@ -13,15 +18,10 @@ import unittest.mock import generator.production import generator.model -import generator.vocabulary -import generator.solo.expressions import generator.solo.types import generator.solo.harmonic import generator.solo.placement import generator.solo.notation -import generator.solo.composer -import tests.solo_fixtures -import tests.solo_fixtures import tests.solo_fixtures def music(result): @@ -30,23 +30,23 @@ def music(result): class IndependentRoles(unittest.TestCase): @classmethod def setUpClass(cls): - cls.source = generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',))) + cls.source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',))) cls.root = pathlib.Path('data').resolve() def test_alone_order_parallel_and_assembly(self): - serial = {role: generator.production.generate_role(self.source, role, self.root) for role in generator.production.ROLES} + serial = {role: generator.composition.generate_role(self.source, role, self.root) for role in generator.composition_types.ROLES} with concurrent.futures.ProcessPoolExecutor(max_workers=2, mp_context=multiprocessing.get_context('spawn')) as pool: - jobs = {role: pool.submit(generator.production._job, (self.source, role, self.root, {}, None)) for role in reversed(generator.production.ROLES)} + jobs = {role: pool.submit(generator.production._job, generator.composition_types.RoleRequest(self.source, role, self.root)) for role in reversed(generator.composition_types.ROLES)} parallel = {role: future.result() for role, future in jobs.items()} - for role in generator.production.ROLES: + for role in generator.composition_types.ROLES: self.assertEqual(music(serial[role]), music(parallel[role]), role) - for doc in generator.production.assemble(self.source, serial).values(): - generator.model.validate_document(doc) + for doc in generator.arrange.assembly.assemble(self.source, serial).values(): + generator.validation.validate_document(doc) def test_role_setting_and_cache_are_independent(self): with tempfile.TemporaryDirectory() as directory: - a = generator.production.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory) - b = generator.production.generate_role(self.source, 'electric_solo', self.root, {'seed': 'changed'}, directory) - again = generator.production.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory) - clean = generator.production.generate_role(self.source, 'acoustic_solo', self.root) + a = generator.composition.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory) + b = generator.composition.generate_role(self.source, 'electric_solo', self.root, {'seed': 'changed'}, directory) + again = generator.composition.generate_role(self.source, 'acoustic_solo', self.root, cache_dir=directory) + clean = generator.composition.generate_role(self.source, 'acoustic_solo', self.root) self.assertTrue(again['cache_hit']) self.assertEqual(music(a), music(again)) self.assertEqual(music(a), music(clean)) @@ -60,7 +60,7 @@ class IndependentRoles(unittest.TestCase): self.assertNotIn('generated_assets', str(path)) return original(path, *args, **kwargs) with unittest.mock.patch.object(pathlib.Path, 'open', guarded): - result = generator.production.generate_role(self.source, 'acoustic_solo', self.root) + result = generator.composition.generate_role(self.source, 'acoustic_solo', self.root) self.assertNotIn('reference', json.dumps(result['trace'])) def test_titles_and_export_ids_do_not_supply_musical_identity(self): document = tests.solo_fixtures.song(count=1, chords=('CM7',)) @@ -69,7 +69,7 @@ class IndependentRoles(unittest.TestCase): for m in other['staves'][0]['measures']: m['id'] = 'exporter_123' m['number'] = '99' - self.assertEqual(generator.production.SourceAnalysis.read(document), generator.production.SourceAnalysis.read(other)) + self.assertEqual(generator.source.SourceAnalysis.read(document), generator.source.SourceAnalysis.read(other)) def test_reference_presence_and_changes_do_not_change_clean_music(self): # No repository reference is a fixture or prerequisite. These deliberately # invalid optional files test the absence of a discovery/fallback branch. @@ -78,16 +78,16 @@ class IndependentRoles(unittest.TestCase): reference_dir.mkdir() references = [reference_dir / (source_name + '-solo-' + str(index) + suffix + '.xml') for source_name in ('all-of-me', 'autumn-leaves') for index in (1, 2) for suffix in ('', '(4)')] - before = generator.production.generate_role(self.source, 'electric_solo', self.root) + before = generator.composition.generate_role(self.source, 'electric_solo', self.root) for reference in references: reference.write_text('') - present = generator.production.generate_role(self.source, 'electric_solo', self.root) + present = generator.composition.generate_role(self.source, 'electric_solo', self.root) for reference in references: reference.write_text('changed optional target, still not MusicXML') - changed = generator.production.generate_role(self.source, 'electric_solo', self.root) + changed = generator.composition.generate_role(self.source, 'electric_solo', self.root) for reference in references: reference.unlink() - absent = generator.production.generate_role(self.source, 'electric_solo', self.root) + absent = generator.composition.generate_role(self.source, 'electric_solo', self.root) self.assertTrue(all(music(before) == music(v) for v in (present, changed, absent))) def test_original_melody_changes_the_theme_with_unchanged_harmony(self): source = tests.solo_fixtures.song(count=4, chords=('CM7',)) @@ -97,11 +97,11 @@ class IndependentRoles(unittest.TestCase): for n in v['notes']: for p in n['pitches']: p['letter'] = {'c': 'e', 'd': 'f', 'e': 'g', 'f': 'a'}[p['letter']] - a = generator.production.generate_role(generator.production.SourceAnalysis.read(source), 'acoustic_solo', self.root) - b = generator.production.generate_role(generator.production.SourceAnalysis.read(changed), 'acoustic_solo', self.root) + a = generator.composition.generate_role(generator.source.SourceAnalysis.read(source), 'acoustic_solo', self.root) + b = generator.composition.generate_role(generator.source.SourceAnalysis.read(changed), 'acoustic_solo', self.root) self.assertNotEqual(a['details']['phrases'][0]['notes'], b['details']['phrases'][0]['notes']) def test_source_tuplet_lanes_survive_assembly(self): - serial = {r: generator.production.generate_role(self.source, r, self.root) for r in generator.production.ROLES} + serial = {r: generator.composition.generate_role(self.source, r, self.root) for r in generator.composition_types.ROLES} # Exercise a different original lane name, as in the imported Autumn head. doc = serial['electric_melody']['document'] staff = doc['staves'][0] @@ -117,10 +117,10 @@ class IndependentRoles(unittest.TestCase): n['voice'] = 'voice_source' for t in staff['tuplets']: t['voice'] = 'voice_source' - generator.model.validate_document(doc) - generator.model.validate_document(generator.production.assemble(self.source, serial)['electric']) + generator.validation.validate_document(doc) + generator.validation.validate_document(generator.arrange.assembly.assemble(self.source, serial)['electric']) def test_sparse_parts_reuse_the_canonical_form(self): - result = generator.production.generate_role(generator.production.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('Dm7', 'G7', 'CM7'))), 'acoustic_accompaniment', self.root) + result = generator.composition.generate_role(generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=4, chords=('Dm7', 'G7', 'CM7'))), 'acoustic_accompaniment', self.root) measures = result['document']['staves'][0]['measures'] def signature(m): return [(n['onset'], n['duration'], [(generator.model.pitch_number(p), p['tablature']) for p in n['pitches']]) for v in m['voices'] for n in v['notes']] diff --git a/tests/test_key_metadata.py b/tests/test_key_metadata.py index 3ae674d..48dbf2b 100644 --- a/tests/test_key_metadata.py +++ b/tests/test_key_metadata.py @@ -1,3 +1,4 @@ +import generator.validation import copy import fractions import pathlib @@ -43,7 +44,7 @@ class KeyCancellationTests(unittest.TestCase): source = key_change(old, new, local) before = copy.deepcopy(source) result = instrument_score(source, *({'A': (1, 2), 'Ab': (-1, -2), 'G': (1, 2), 'F': (-1, -2)}[target])) - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] self.assertEqual(measures[0]['key'], generator.model.key_from_fifths(expected_old)) change = measures[0]['attributes'][0] if local else measures[1] diff --git a/tests/test_learned_solos.py b/tests/test_learned_solos.py index fa4b596..5cd6ffa 100644 --- a/tests/test_learned_solos.py +++ b/tests/test_learned_solos.py @@ -1,4 +1,5 @@ """Learned inference and explicit authored reuse have different contracts.""" +import generator.composition import copy import dataclasses import json @@ -8,7 +9,6 @@ import tempfile import unittest import unittest.mock -import generator.production import generator.parts import generator.musicxml import generator.model @@ -21,7 +21,7 @@ import generator.solo.learned import generator.solo.composer import generator.solo.decoder import generator.solo.harmonic -import generator.solo.analysis +import generator.source import tests.score_events import tests.solo_fixtures import tests.test_musicxml @@ -53,9 +53,9 @@ class TrainingAndRepertoire(unittest.TestCase): with self.subTest(pair=entry['id']): part = 'acoustic' if entry['slot'] == 1 else 'electric' doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source'])) - source = generator.production.SourceAnalysis.read(doc) + source = generator.source.SourceAnalysis.read(doc) with unittest.mock.patch('generator.solo.composer.compose', side_effect=AssertionError('authored pair must not be predicted')): - result = generator.production.generate_role(source, part + '_solo', ROOT / 'data') + result = generator.composition.generate_role(source, part + '_solo', ROOT / 'data') measures = result['document']['staves'][0]['measures'] first = next(i for i, m in enumerate(measures) if m.get('section_label') == part.capitalize() + ' Solo') last = next((i for i in range(first + 1, len(measures)) if measures[i].get('section_label')), len(measures)) @@ -82,13 +82,13 @@ class TrainingAndRepertoire(unittest.TestCase): def test_lookup_uses_source_music_and_slot_not_title_or_path(self): entry = json.loads((ROOT / 'data/training/manifest.json').read_text())['examples'][0] doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(ROOT / 'data/training' / entry['source'])) - source = generator.production.SourceAnalysis.read(doc) + source = generator.source.SourceAnalysis.read(doc) part = 'acoustic' if entry['slot'] == 1 else 'electric' self.assertIsNotNone(generator.solo.authored.lookup(source, part, ROOT / 'data')) doc['metadata'].update(title='Renamed standard', composer='Display only') - self.assertEqual(source, generator.production.SourceAnalysis.read(doc)) + self.assertEqual(source, generator.source.SourceAnalysis.read(doc)) doc['staves'][0]['measures'][0]['voices'][0]['notes'][0]['pitches'][0]['octave'] += 1 - self.assertIsNone(generator.solo.authored.lookup(generator.production.SourceAnalysis.read(doc), part, ROOT / 'data')) + self.assertIsNone(generator.solo.authored.lookup(generator.source.SourceAnalysis.read(doc), part, ROOT / 'data')) def test_adding_a_new_pair_requires_no_code_or_existing_song_names(self): with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) @@ -102,7 +102,7 @@ class TrainingAndRepertoire(unittest.TestCase): examples, _ = generator.train.load_examples(manifest) self.assertEqual(len(examples), 1) doc = generator.parts.prepare_document(generator.musicxml.load_musicxml(root / 'data/songs' / source.name)) - context = generator.production.SourceAnalysis.read(doc) + context = generator.source.SourceAnalysis.read(doc) self.assertIsNotNone(generator.solo.authored.lookup(context, 'acoustic', root / 'data')) self.assertIsNone(generator.solo.authored.lookup(context, 'electric', root / 'data')) self.assertEqual(solo.read_bytes(), (manifest.parent / solo.name).read_bytes()) @@ -130,7 +130,7 @@ class LearnedInference(unittest.TestCase): generator.solo.learned.Model.load('/missing-model-file') def test_decoder_runs_without_training_files_and_is_deterministic(self): source = tests.solo_fixtures.song(count=1, chords=('C7',)) - a = generator.solo.analysis.analyze(source) + a = generator.source.analyze(source) predictor = generator.solo.learned.Model.load(ROOT / 'data/models/solo.json') with generator.vocabulary.using(generator.vocabulary.load(), 'acoustic_solo', {'attempts': 2}): original = pathlib.Path.open @@ -146,7 +146,7 @@ class LearnedInference(unittest.TestCase): self.assertEqual(first.diagnostics['construction'], 'learned_joint_events') def test_event_score_does_not_depend_on_action_grouping(self): source = tests.solo_fixtures.song(count=1, chords=('C7',)) - a = generator.solo.analysis.analyze(source) + a = generator.source.analyze(source) h = generator.solo.harmonic.Harmony(generator.harmonic_time.read(source['staves'][0])) predictor = generator.solo.learned.Model.load(ROOT / 'data/models/solo.json') engine = generator.solo.decoder.Decoder(a, h, predictor, 'acoustic', fractions.Fraction(), fractions.Fraction(4)) @@ -157,7 +157,7 @@ class LearnedInference(unittest.TestCase): self.assertEqual(combined, split) def test_source_features_include_actual_melodic_intervals(self): source = tests.solo_fixtures.song(count=1, chords=('C7',)) - a = generator.solo.analysis.analyze(source) + a = generator.source.analyze(source) changed = dataclasses.replace(a, melody=tuple(dataclasses.replace(n, pitch=dataclasses.replace(n.pitch, octave=n.pitch.octave + (i == 1))) for i, n in enumerate(a.melody))) self.assertNotEqual(generator.solo.learned.source_feature(a, fractions.Fraction(1)), generator.solo.learned.source_feature(changed, fractions.Fraction(1))) def test_generated_long_nonchord_notes_still_need_support(self): @@ -167,7 +167,7 @@ class LearnedInference(unittest.TestCase): self.assertTrue(h.evaluate((tests.solo_fixtures.note(0, fractions.Fraction(1, 2), 'g'), tests.solo_fixtures.note(fractions.Fraction(1, 2), 1, 'f'), tests.solo_fixtures.note(1, 2, 'e'))).valid) def test_held_chord_member_can_become_a_supported_color(self): source = tests.solo_fixtures.song(count=2, chords=('C', 'Dm')) - a = generator.solo.analysis.analyze(source) + a = generator.source.analyze(source) h = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('C', 'Dm'))) engine = generator.solo.decoder.Decoder(a, h, generator.solo.learned.Model.load(ROOT / 'data/models/solo.json'), 'acoustic', fractions.Fraction(), fractions.Fraction(8)) n = engine.note(fractions.Fraction(3), fractions.Fraction(6), 64) # E: C's third becomes D minor's ninth. diff --git a/tests/test_music_model.py b/tests/test_music_model.py index 5ca26e4..bf60d23 100644 --- a/tests/test_music_model.py +++ b/tests/test_music_model.py @@ -1,3 +1,4 @@ +import generator.validation import tests.production_fixtures import argparse import fractions @@ -73,16 +74,16 @@ class MusicModelTests(unittest.TestCase): document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) tests.test_musicxml.notes(document)[0]['pitches'][0]['tie_to'] = 'missing' with self.assertRaisesRegex(ValueError, 'dangling tie'): - generator.model.validate_document(document) + generator.validation.validate_document(document) document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) tests.test_musicxml.notes(document)[0]['duration'] = {'numerator': 8, 'denominator': 2} with self.assertRaisesRegex(ValueError, 'reduced'): - generator.model.validate_document(document) + generator.validation.validate_document(document) def test_optional_turnaround_and_form(self): source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/all-of-me.xml')) for part in ('acoustic', 'electric', 'vocal'): result = generator.arrange.generate.arrange_document(source, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] self.assertEqual([m['section_label'] for m in measures if m.get('section_label')], ['Rhythm Lead-in', 'Vocal Melody', 'Guitar Melody', 'Acoustic Solo', 'Electric Solo', 'Vocal Melody', 'Ending']) self.assertFalse(measures[-1]['harmonies']) @@ -96,12 +97,12 @@ class MusicModelTests(unittest.TestCase): source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) source['staves'][0]['measures'][0][field] = {} with self.assertRaisesRegex(ValueError, 'repeat notation'): - generator.model.validate_document(source) + generator.validation.validate_document(source) for field in ('sound_ties', 'printed_ties'): source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) tests.test_musicxml.notes(source)[0]['pitches'][0][field] = [] with self.assertRaisesRegex(ValueError, 'pass-specific tie'): - generator.model.validate_document(source) + generator.validation.validate_document(source) def test_failure_preserves_generated_assets(self): with tempfile.TemporaryDirectory() as temp: root = pathlib.Path(temp) @@ -149,7 +150,7 @@ class MusicModelTests(unittest.TestCase): source = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=1, kind='quarter', extra=''), implicit=True) source = source.replace('', '' + tests.test_musicxml.xml_note(extra='') + '') result = generator.arrange.generate.arrange_document(tests.test_musicxml.parse(source), 'vocal') - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] start = measures[0]['voices'][0]['notes'][-1]['pitches'][0] end = measures[1]['voices'][0]['notes'][0]['pitches'][0] diff --git a/tests/test_output_contract.py b/tests/test_output_contract.py new file mode 100644 index 0000000..117a6ff --- /dev/null +++ b/tests/test_output_contract.py @@ -0,0 +1,20 @@ +"""Exact role scores protect musical choices, notation, IDs and tablature.""" +import generator.composition_types +import generator.composition +import generator.source +import json +import pathlib +import unittest + +import tests.solo_fixtures + +class OutputContract(unittest.TestCase): + def test_complete_roles_including_learned_fallback_match_reviewed_output(self): + root = pathlib.Path(__file__).resolve().parents[1] + expected = json.loads((root / 'tests/fixtures/role_outputs.json').read_text()) + source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=2, chords=('Dm7', 'G7'))) + for role in generator.composition_types.ROLES: + with self.subTest(role=role): + settings = {'predict': True} if role.endswith('_solo') else {} + actual = generator.composition.generate_role(source, role, root / 'data', settings)['document'] + self.assertEqual(actual, expected[role]) diff --git a/tests/test_parts.py b/tests/test_parts.py index 37e5de4..c61e143 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -1,3 +1,6 @@ +import generator.production +import generator.score +import generator.validation import tests.production_fixtures import argparse import copy @@ -10,11 +13,9 @@ import unittest.mock import xml.etree.ElementTree import generator.__main__ -import generator.accompaniment import generator.arrange.generate import generator.model import generator.musicxml -import generator.parts import generator.pitch import tests.score_events import tests.arrangement_fixtures @@ -37,7 +38,7 @@ class PartGenerationTests(unittest.TestCase): def test_dense_melody_keeps_rhythmic_accompaniment_without_sustaining_across_changes(self): source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + xml_harmony('F', offset=3) + ''.join(tests.test_musicxml.xml_note(duration=1, kind='eighth') for _ in range(8)), divisions=2)) for result in (tests.arrangement_fixtures.section_document(source, 'acoustic'), generator.arrange.generate.arrange_document(source, 'electric')): - generator.model.validate_document(result) + generator.validation.validate_document(result) for measure in result['staves'][0]['measures']: events = measure['voices'][0]['notes'] if measure.get('ending_sustain'): @@ -72,13 +73,13 @@ class PartGenerationTests(unittest.TestCase): self.assertTrue(all(generator.model.pitch_number(p) % 12 in {0, 5, 9} for n in carried for p in n['pitches'])) for index in (2, 3): self.assertTrue(all(n['kind'] == 'rest' and not n['pitches'] for n in tests.test_musicxml.notes(result, index))) - generator.model.validate_document(result) + generator.validation.validate_document(result) def test_sparse_accompaniment_preserves_form_and_intentional_space(self): bar = xml_harmony() + ''.join(tests.test_musicxml.xml_note(duration=1, kind='quarter') for _ in range(4)) xml = tests.test_musicxml.xml_score(bar).replace('', ''.join(f'{bar}' for i in range(2, 13)) + '') result = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(xml), 'acoustic') self.assertEqual(len(result['staves'][0]['measures']), 12) - generator.model.validate_document(result) + generator.validation.validate_document(result) self.assertTrue(tests.score_events.timeline(result)['events']) self.assertTrue(any(n['kind'] == 'rest' for m in result['staves'][0]['measures'] for n in generator.score.measure_notes(m))) def test_melody_written_pitch_rhythm_rests_and_cross_bar_ties_are_preserved(self): @@ -97,7 +98,7 @@ class PartGenerationTests(unittest.TestCase): for part in ('electric', 'vocal'): with self.subTest(builder=builder.__module__, part=part): result = builder(source, part) - generator.model.validate_document(result) + generator.validation.validate_document(result) measures = result['staves'][0]['measures'] starts = [0] if builder is tests.arrangement_fixtures.section_document else [i for i, m in enumerate(measures) if m.get('section_label') == ('Guitar Melody' if part == 'electric' else 'Vocal Melody')] self.assertTrue(starts) @@ -131,7 +132,7 @@ class PartGenerationTests(unittest.TestCase): for name in names: (source / (name + '.xml')).write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) with self.assertRaisesRegex(ValueError, 'conflicting output path') as raised: - generator.arrange.generate.generate_performance_scores(source, output) + generator.production.generate_tree(source, output) for name in names: self.assertIn(str(source / (name + '.xml')), str(raised.exception)) self.assertEqual([p.name for p in output.iterdir()], ['sentinel']) @@ -148,7 +149,7 @@ class PartGenerationTests(unittest.TestCase): unittest.mock.patch('generator.engraving.pipeline.engrave_tree'), unittest.mock.patch('generator.pico_artwork.generate_titles'), unittest.mock.patch('generator.catalog.build_catalog'), - unittest.mock.patch('generator.performance.copy_audio', return_value=[]), + unittest.mock.patch('generator.audio_assets.copy_audio', return_value=[]), ): generator.__main__.generate_assets(args) # Role catalog loading is replaced by this infrastructure fixture. The diff --git a/tests/test_performance.py b/tests/test_performance.py index e2d97f3..8757022 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -1,3 +1,4 @@ +import generator.audio_assets import argparse import copy import hashlib @@ -41,7 +42,7 @@ def fixture(root): with wave.open(str(path), 'wb') as output: output.setparams((2, 2, 48000, 0, 'NONE', '')) output.writeframes(value.to_bytes(2, 'little', signed=True) * 2 * frames) - entries.append(generator.performance.describe_wav(path, root, song_id=song_id, bpm=bpm)) + entries.append(generator.audio_assets.describe_wav(path, root, song_id=song_id, bpm=bpm)) (root / 'manifest.audio').write_text(json.dumps(entries)) class PerformanceTests(unittest.TestCase): @@ -49,7 +50,7 @@ class PerformanceTests(unittest.TestCase): source = pathlib.Path('package/assets') with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) - entries = generator.performance.copy_audio(source, root, {'all_of_me', 'autumn_leaves'}) + entries = generator.audio_assets.copy_audio(source, root, {'all_of_me', 'autumn_leaves'}) self.assertEqual(len(entries), 4) for entry in entries: self.assertEqual((source / entry['path']).read_bytes(), (root / entry['path']).read_bytes()) @@ -61,10 +62,10 @@ class PerformanceTests(unittest.TestCase): path = root / 'backing/fixture_120.wav' path.write_bytes(path.read_bytes()[:-2]) with self.assertRaisesRegex(ValueError, 'truncated'): - generator.performance.describe_wav(path, root) + generator.audio_assets.describe_wav(path, root) path.rename(root / 'backing/fixture_115.wav') with self.assertRaisesRegex(ValueError, 'unsupported'): - generator.performance.copy_audio(root, root / 'out', {'fixture'}) + generator.audio_assets.copy_audio(root, root / 'out', {'fixture'}) def test_short_backing_cannot_publish(self): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) @@ -73,10 +74,10 @@ class PerformanceTests(unittest.TestCase): bundle = json.loads((root / 'score.json').read_text()) (root / 'songs/fixture.json').write_text(json.dumps(bundle['music'])) entries = json.loads((root / 'manifest.audio').read_text()) - generator.performance.validate_recording_lengths(root, entries) + generator.audio_assets.validate_recording_lengths(root, entries) entries[-1]['frames'] = 1 with self.assertRaisesRegex(ValueError, 'shorter'): - generator.performance.validate_recording_lengths(root, entries) + generator.audio_assets.validate_recording_lengths(root, entries) def test_page_coverage_and_measure_accents(self): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) diff --git a/tests/test_printing.py b/tests/test_printing.py index 86a41a4..4ac4b17 100644 --- a/tests/test_printing.py +++ b/tests/test_printing.py @@ -1,4 +1,5 @@ """Build-only print layout, PDF contents and reproducible cache behavior.""" +import generator.score_io import copy import dataclasses import io @@ -14,11 +15,9 @@ import PIL.PdfParser import generator.arrange.generate import generator.engraving.audit import generator.engraving.fonts -import generator.engraving.types import generator.model import generator.musicxml import generator.printing -import generator.score import tests.solo_fixtures import tests.test_musicxml @@ -35,7 +34,7 @@ class PrintableParts(unittest.TestCase): def tree(self, name, document=None): root = self.root / name (root / 'songs').mkdir(parents=True) - generator.score.write_json(root / 'songs/part.json', document or self.source) + generator.score_io.write_json(root / 'songs/part.json', document or self.source) return root def test_letter_pagination_preserves_complete_order_without_transition_duplicates(self): source = tests.solo_fixtures.song(count=32, chords=('C7',)) @@ -137,7 +136,7 @@ class PrintableParts(unittest.TestCase): generator.printing.generate_tree(root, self.font_path, cache_dir=cache) changed = copy.deepcopy(self.source) changed['metadata']['title'] = 'Another title' - generator.score.write_json(root / 'songs/part.json', changed) + generator.score_io.write_json(root / 'songs/part.json', changed) with unittest.mock.patch('generator.printing.write_pdf', wraps=generator.printing.write_pdf) as render: generator.printing.generate_tree(root, self.font_path, cache_dir=cache) render.assert_called_once() diff --git a/tests/test_publish_package.py b/tests/test_publish_package.py index d01f8b2..39db59b 100644 --- a/tests/test_publish_package.py +++ b/tests/test_publish_package.py @@ -4,7 +4,9 @@ import unittest import unittest.mock import modulus.artifacts -import modulus.cli +import modulus.build_context +import modulus.packaging +import modulus.process import modulus.config import modulus.python @@ -13,17 +15,17 @@ class PackagePublishingTests(unittest.TestCase): root = pathlib.Path(__file__).resolve().parents[1] with tempfile.TemporaryDirectory() as temporary: directory = pathlib.Path(temporary) - context = modulus.cli.build_context(directory, modulus.config.load_repo_config(root)) + context = modulus.build_context.build_context(directory, modulus.config.load_repo_config(root)) def fake_build(context, command, **kwargs): if command[:2] == ['cmake', '--install']: stage = pathlib.Path(command[2]).parent / 'stage' (stage / 'manifest.json').write_text('{}\n') return 0 - with unittest.mock.patch.object(modulus.cli, 'in_docker', return_value=True), \ - unittest.mock.patch.object(modulus.cli, 'run', side_effect=fake_build), \ + with unittest.mock.patch.object(modulus.process, 'in_docker', return_value=True), \ + unittest.mock.patch.object(modulus.process, 'run', side_effect=fake_build), \ unittest.mock.patch.object(modulus.python, 'interpreter', return_value=pathlib.Path('/usr/bin/python3')), \ unittest.mock.patch.object(modulus.artifacts, 'main') as publish: - self.assertEqual(modulus.cli.package_artifacts(context, ['--no-remote']), 0) + self.assertEqual(modulus.packaging.package_artifacts(context, ['--no-remote']), 0) publish.assert_not_called() artifacts = modulus.artifacts.discover_artifacts(directory / 'build/packages', context.package['name'], context.artifact['targets']) self.assertEqual([p.name for p in artifacts], ['product_vamp_arm_64_gcc12_release.tar.gz', 'product_vamp_arm_64_gcc12_release.tar.gz.sha256']) diff --git a/tests/test_score_bundle.py b/tests/test_score_bundle.py index 476af05..5d4514b 100644 --- a/tests/test_score_bundle.py +++ b/tests/test_score_bundle.py @@ -1,3 +1,5 @@ +import generator.score_io +import generator.validation import tests.production_fixtures import argparse import copy @@ -14,7 +16,6 @@ import xml.etree.ElementTree import PIL.Image import PIL.ImageChops -import generator.__main__ import generator.arrange.generate import generator.engraving.fonts import generator.engraving.layout @@ -30,7 +31,6 @@ import generator.performance import generator.pitch import tests.score_events import tests.arrangement_fixtures -import generator.score import generator.theme import tests.musicxml_roundtrip import tests.test_musicxml @@ -48,7 +48,7 @@ class ScoreBundleTests(unittest.TestCase): self.assertEqual(set(document), {'metadata', 'staves'}) for malformed in (None, [], {}, {'metadata': {}, 'staves': []}, {**document, 'staves': None}, {**document, 'staves': [{}]}): with self.subTest(document=malformed), self.assertRaises(ValueError): - generator.model.validate_document(malformed) + generator.validation.validate_document(malformed) def test_multiple_voices_keep_independent_ties_and_transformations(self): def half(action): return tests.test_musicxml.xml_note(duration=2, kind='half', extra=f'1') @@ -59,9 +59,9 @@ class ScoreBundleTests(unittest.TestCase): self.assertEqual([v['id'] for v in measure['voices']], ['1', '2']) self.assertNotIn('tie_from', measure['voices'][1]['notes'][0]['pitches'][0]) for document in (source, generator.pitch.concert_document(source), generator.arrange.generate.arrange_document(source, 'vocal')): - generator.model.validate_document(document) + generator.validation.validate_document(document) restored = self.round_trip(document) - generator.model.validate_document(restored) + generator.validation.validate_document(restored) self.assertTrue(any(len(m['voices']) == 2 for m in restored['staves'][0]['measures'])) def test_contiguous_ties_continue_across_systems_without_repeat_state(self): text = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(extra='')) @@ -139,7 +139,7 @@ class ScoreBundleTests(unittest.TestCase): tests.musicxml_roundtrip.audit_roundtrip(before, after) source['staves'][0]['instruments'] = [] with self.assertRaisesRegex(ValueError, 'unknown score instrument'): - generator.model.validate_document(source) + generator.validation.validate_document(source) def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self): for path in sorted(pathlib.Path('data/songs').glob('*.xml')): with self.subTest(path=path): @@ -220,34 +220,34 @@ class ScoreBundleTests(unittest.TestCase): bundle['performance'] = generator.performance.timing(document) path = assets / 'score.json' path.write_text(json.dumps(bundle)) - self.assertEqual(generator.score.load_json(path), document) + self.assertEqual(generator.score_io.load_json(path), document) page.write_bytes(data + b'corrupt') with self.assertRaisesRegex(ValueError, 'checksum'): - generator.score.load_json(path) + generator.score_io.load_json(path) page.unlink() outside = root / 'outside.png' outside.write_bytes(data) page.symlink_to(outside) with self.assertRaisesRegex(ValueError, 'escapes'): - generator.score.load_json(path) + generator.score_io.load_json(path) def test_final_validation_reuses_content_checks_and_rechecks_files(self): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note())) for index in range(3): - generator.score.write_json(root / 'songs' / f'{index}.json', document) + generator.score_io.write_json(root / 'songs' / f'{index}.json', document) trusted = generator.engraving.pipeline.engrave_tree(root, pathlib.Path('package/assets/fonts/petaluma')) generator.pico_artwork.generate_titles(root, pathlib.Path('package/assets/fonts/petaluma')) with unittest.mock.patch('generator.engraving.validation.check_content', side_effect=AssertionError('already validated content')): - generator.score.validate_generated_tree(root, trusted) + generator.score_io.validate_generated_tree(root, trusted) page = root / 'songs/2.pages/0001.png' original = page.read_bytes() page.write_bytes(original + b'changed') with self.assertRaisesRegex(ValueError, 'checksum'): - generator.score.validate_generated_tree(root, trusted) + generator.score_io.validate_generated_tree(root, trusted) page.write_bytes(original) with unittest.mock.patch('generator.engraving.validation.check_content', wraps=generator.engraving.validation.check_content) as decode: - generator.score.validate_generated_tree(root) + generator.score_io.validate_generated_tree(root) decode.assert_called_once() PIL.Image.new('RGB', (976, 484), generator.theme.rgb('background')).save(page) path = root / 'songs/2.json' @@ -255,14 +255,14 @@ class ScoreBundleTests(unittest.TestCase): bundle['presentation']['pages'][0]['sha256'] = hashlib.sha256(page.read_bytes()).hexdigest() path.write_text(json.dumps(bundle)) with self.assertRaisesRegex(ValueError, 'blank page'): - generator.score.validate_generated_tree(root, trusted) + generator.score_io.validate_generated_tree(root, trusted) def test_python_engraver_visual_golden_and_determinism(self): document = generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/engraving.xml')) with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary) before = None for _ in range(2): - generator.score.write_json(root / 'fixture.json', document) + generator.score_io.write_json(root / 'fixture.json', document) generator.engraving.pipeline.engrave_tree(root, pathlib.Path('package/assets/fonts/petaluma')) bundle = json.loads((root / 'fixture.json').read_text()) self.assertEqual(bundle['music'], document) diff --git a/tests/test_score_loader.cpp b/tests/test_score_loader.cpp index 545253e..f9d52e1 100644 --- a/tests/test_score_loader.cpp +++ b/tests/test_score_loader.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -93,6 +94,38 @@ BOOST_AUTO_TEST_CASE(score_loader_starts_asynchronously_from_executor_handler) { BOOST_CHECK_EQUAL(*fixture.completions, 1U); } +BOOST_AUTO_TEST_CASE(score_loader_worker_keeps_control_executor_responsive_and_returns_completion) { + auto io = boost::asio::io_context{}; + auto worker = boost::asio::thread_pool{1}; + auto entered = std::promise{}; + auto release = std::promise{}; + auto resume = release.get_future().share(); + auto completed = false; + auto control_thread = std::this_thread::get_id(); + auto loader = vamp_zero::score_loader{io.get_executor(), + [&entered, resume](vamp_zero::score_reference reference) -> boost::asio::awaitable> { + entered.set_value(); + resume.wait(); + co_return vamp_zero::score_document{.id = reference.id}; + }, + [&] { completed = true; BOOST_CHECK(std::this_thread::get_id() == control_thread); }, worker.get_executor()}; + loader.select(std::make_shared(), performance("a")); + auto started = entered.get_future().wait_for(std::chrono::seconds{2}) == std::future_status::ready; + auto serviced = false; + boost::asio::post(io, [&] { serviced = true; }); + io.poll(); + BOOST_CHECK(serviced); + BOOST_CHECK(!completed); + release.set_value(); + worker.join(); + io.restart(); + io.run(); + BOOST_REQUIRE(started); + BOOST_CHECK(completed); + BOOST_REQUIRE(loader.snapshot().document); + BOOST_CHECK_EQUAL(loader.snapshot().document->id, "a"); +} + BOOST_AUTO_TEST_CASE(score_loader_coalesces_pending_and_discards_stale_results) { auto fixture = loader_fixture{}; fixture.loader.select(fixture.catalog, performance("a")); diff --git a/tests/test_six_parts.py b/tests/test_six_parts.py index 28f5323..8e1ce70 100644 --- a/tests/test_six_parts.py +++ b/tests/test_six_parts.py @@ -3,18 +3,11 @@ import dataclasses import fractions import unittest -import generator.arrange.generate -import generator.arrange.form import generator.model import generator.parts -import generator.solo.arrangement -import generator.solo.analysis +import generator.source import generator.solo.profiles import generator.solo.placement -import generator.solo.types -import generator.solo.harmonic -import tests.solo_fixtures -import tests.solo_fixtures import tests.solo_fixtures def events(measure): @@ -39,7 +32,7 @@ class SoloInstrumentAndIdentity(unittest.TestCase): self.assertTrue(any(p.string_number == 1 for p in generator.solo.placement.positions(65, electric))) self.assertTrue(any(p.string_number == 1 for p in generator.solo.placement.positions(65, acoustic))) def test_soft_overlapping_registers_do_not_inherit_a_mandatory_octave(self): - source = generator.solo.analysis.analyze(generator.parts.prepare_document(tests.solo_fixtures.song(count=4, chords=('CM7',)))) + source = generator.source.analyze(generator.parts.prepare_document(tests.solo_fixtures.song(count=4, chords=('CM7',)))) acoustic = generator.solo.profiles.for_part('acoustic', source) electric = generator.solo.profiles.for_part('electric', source) self.assertIsNone(electric.reference) diff --git a/tests/test_voicing.py b/tests/test_voicing.py index 0daeae8..06f600c 100644 --- a/tests/test_voicing.py +++ b/tests/test_voicing.py @@ -1,3 +1,4 @@ +import generator.validation import copy import pathlib import unittest @@ -209,7 +210,7 @@ class VoicingTests(unittest.TestCase): source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml')) before = copy.deepcopy(source) result = generator.arrange.generate.arrange_document(source, 'electric') - generator.model.validate_document(result) + generator.validation.validate_document(result) section, checked = '', set() source_index = generator.score.body_start_measure_index(source['staves'][0]['measures']) for measure in result['staves'][0]['measures']: