READY: START builds the week's Ableton sets (Sunday and special services) from Planning Center

Hold START on Monday: last weekend's songs go to Finished Songs (a changed one parks the version it
came from as z...), then each service PCO lists this week (weekend + Special Services / Morning
Worship...; First / Together Tuesday in Projects/FIRST TUESDAY) gets 'YYMMDD NA Project' built from
the newest weekend set: locators, tempo map and TEMPO clips, Waves Tune cues, songs from Finished
Songs (transposed Complex Pro when only another key exists), markers / guide / lighting cues on the
dedicated tracks, pads, routing and send A at 0 dB, audio copied in with an Ableton Project Info folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 12:38:09 -04:00
co-authored by Claude Opus 5.5
parent e2add3b27e
commit 0d655bff3c
10 changed files with 2027 additions and 7 deletions
+377
View File
@@ -0,0 +1,377 @@
"""START (the week's Ableton sets): which PCO services get a set and what they are called, what the key
shows, and ableton_start.py building a real (small) set on disk: last week's songs saved to Finished Songs,
the new set laid out from the base set, songs, pads, Waves Tune cues and audio copied in.
Run: python3 -m unittest discover -s tests"""
import gzip
import json
import os
import shutil
import struct
import sys
import tempfile
import unittest
import wave
import xml.etree.ElementTree as ET
from datetime import datetime
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
import ableton_start as A # noqa: E402
import readiness as rd # noqa: E402
MONDAY = datetime(2026, 10, 26, 9, 0).timestamp()
RC = {"site_folder": "NA", "folder_format": "{date} {loc} Project", "reset_day": "mon", "tracks_host": "tracks",
"projects": "~/P", "locations": ["SC"]}
def pco_item(title, kind="song", pos="during", key=None, arr=None):
return {"id": title, "attributes": {"title": title, "item_type": kind, "service_position": pos, "key_name": key},
"relationships": {"arrangement": {"data": {"id": arr} if arr else None}}}
class TestServices(unittest.TestCase):
def test_songs_and_salvation_from_a_plan(self):
items = [pco_item("Worship", "header"), pco_item("Call To Worship", "media"),
pco_item("Thank God I'm Free", key="E", arr="a1"), pco_item("The Lord Is Here", key="B", arr="a2"),
pco_item("Prayer Graphic", pos="pre"), pco_item("Standing Salvation Moment", "media"),
pco_item("Owe You Praise TAG", key="E", arr="a3"), pco_item("Postservice", pos="post", key=None)]
inc = [{"type": "Arrangement", "id": "a1", "attributes": {"bpm": 128}}, {"type": "Arrangement", "id": "a2", "attributes": {"bpm": 94}},
{"type": "Arrangement", "id": "a3", "attributes": {"bpm": 61}}]
orig = rd.pco_get
rd.pco_get = lambda auth, path, pages=10: (items, inc)
try:
songs, sal = rd.plan_service("x", "1", "p", {})
finally:
rd.pco_get = orig
self.assertEqual([(x["title"], x["key"], x["bpm"]) for x in songs], [("Thank God I'm Free", "E", 128), ("The Lord Is Here", "B", 94)])
self.assertEqual((sal["title"], sal["key"], sal["bpm"]), ("Owe You Praise TAG", "E", 61))
def test_names_sunday_and_first_tuesday(self):
sun = rd.service_entry(RC, True, "North Attleboro | Services", {"id": "1", "attributes": {"title": None}},
datetime(2026, 11, 1, 9, 0).timestamp(), [{"title": "x", "key": "E"}], None)
self.assertEqual((sun["folder"], sun["name"], sun["label"]), ("NA", "261101 NA Project", "SUN 11/1"))
tue = rd.service_entry(RC, False, "North Attleboro | Special Services", {"id": "2", "attributes": {"title": "First Tuesday"}},
datetime(2026, 11, 3, 18, 30).timestamp(), [{"title": "x", "key": "E"}], None)
self.assertEqual((tue["folder"], tue["name"], tue["label"]), ("FIRST TUESDAY", "261103 NA First Tuesday Project", "TUE 11/3"))
other = rd.service_entry(RC, False, "North Attleboro | Morning Worship", {"id": "3", "attributes": {"title": ""}},
datetime(2026, 11, 2, 7, 0).timestamp(), [{"title": "x", "key": "E"}], None)
self.assertEqual((other["folder"], other["name"]), ("NA", "261102 NA Morning Worship Project"))
def test_finds_every_service_with_songs_this_week(self):
def t(id_, name, parent="730820"):
return {"id": id_, "attributes": {"name": name}, "relationships": {"parent": {"data": {"id": parent}}}}
types = [t("275974", "North Attleboro | Services"), t("329152", "North Attleboro | Special Services"),
t("655130", "North Attleboro | Waters Youth"), t("730831", "Woonsocket | Services", "714933")]
plans = {"275974": [("2026-11-01T13:00:00Z", "p1"), ("2026-11-08T13:00:00Z", "p2")], "329152": [("2026-11-03T22:30:00Z", "p3")],
"655130": [("2026-10-30T23:00:00Z", "p4")], "730831": [("2026-11-01T14:00:00Z", "p5")]}
def get(auth, path, pages=10):
if path.startswith("/service_types?"):
return types, []
st = path.split("/")[2]
if "/items" in path:
return [pco_item("Song " + st, key="E")], []
return [{"id": pid, "attributes": {"sort_date": d, "title": "First Tuesday" if st == "329152" else None}}
for d, pid in plans[st]], []
orig = rd.pco_get, rd.pco_login
rd.pco_get, rd.pco_login = get, lambda rc: ("auth", "275974")
rd.SERVICES.clear()
try:
got = rd.upcoming_services(RC, MONDAY)
finally:
rd.pco_get, rd.pco_login = orig
self.assertEqual([x["name"] for x in got], ["261101 NA Project", "261103 NA First Tuesday Project"]) # not youth, not WS, not 11/8
def test_the_key(self):
svc = [{"name": "261101 NA Project", "label": "SUN 11/1", "folder": "NA", "title": ""},
{"name": "261103 NA First Tuesday Project", "label": "TUE 11/3", "folder": "FIRST TUESDAY", "title": "First Tuesday"}]
line = rd.eval_start(RC, svc, {"survey": {"261101 NA Project": {"exists": True}}, "status": None}, MONDAY)
self.assertEqual([m for _, m, _ in line.faults], ["NO SET TUE 11/3"])
both = {"261101 NA Project": {"exists": True}, "261103 NA First Tuesday Project": {"exists": True}}
line = rd.eval_start(RC, svc, {"survey": both, "status": {"state": "done", "time": MONDAY, "results": [
{"name": "261101 NA Project", "notes": ["TRANSPOSE THE LORD IS HERE E>B: CONSOLIDATE IN LIVE", "NEW SONG GRACE: BUILD IT BY HAND"]}]}}, MONDAY + 60)
self.assertEqual([m for _, m, _ in line.faults], ["CONSOLIDATE THE LORD IS.", "NEW SONG GRACE"])
line = rd.eval_start(RC, svc, {"survey": both, "status": None}, MONDAY)
self.assertEqual((line.faults, line.info), ([], "SUN 11/1 TUE 11/3"))
line = rd.eval_start(RC, svc, {"survey": {}, "status": {"state": "running", "time": MONDAY, "step": "COPYING 3/40"}}, MONDAY + 5)
self.assertEqual([m for _, m, _ in line.faults], ["COPYING 3/40"]) # no "NO SET" while it builds
def test_only_where_tracks_are_made(self):
self.assertTrue(rd.start_on(RC))
self.assertFalse(rd.start_on(dict(RC, locations=[]))) # a location that receives
self.assertTrue(rd.start_on(dict(RC, locations=[], start=True)))
# --- a small Live set, written the way Live 11 writes one ---------------------------------------------------
def v(tag, value):
return '<%s Value="%s" />' % (tag, value)
PTR = [100]
def ptr():
PTR[0] += 1
return PTR[0]
def mixer(sends=1):
holders = "".join('<TrackSendHolder Id="%d"><Send>%s<AutomationTarget Id="%d"><LockEnvelope Value="0" /></AutomationTarget>'
'<ModulationTarget Id="%d"><LockEnvelope Value="0" /></ModulationTarget></Send>%s</TrackSendHolder>'
% (i, v("Manual", "0.0003162277571"), ptr(), ptr(), v("Active", "true")) for i in range(sends))
return ('<Mixer><Sends>%s</Sends><Volume>%s<AutomationTarget Id="%d"><LockEnvelope Value="0" /></AutomationTarget></Volume></Mixer>'
% (holders, v("Manual", "1"), ptr()))
def routing(upper="Master", lower="", target="AudioOut/Master"):
return "<AudioOutputRouting>%s%s%s</AudioOutputRouting>" % (v("Target", target), v("UpperDisplayString", upper), v("LowerDisplayString", lower))
def slots(n=2):
return "<ClipSlotList>%s</ClipSlotList>" % "".join('<ClipSlot Id="%d"><ClipSlot><Value /></ClipSlot></ClipSlot>' % i for i in range(n))
def audio_clip(start, end, name, path, rel="", warped=False, secs=None, size=1000):
secs = secs if secs is not None else end - start
loop = "<Loop>%s%s%s%s%s%s%s</Loop>" % (v("LoopStart", 0), v("LoopEnd", secs), v("StartRelative", 0), v("LoopOn", "false"),
v("OutMarker", secs), v("HiddenLoopStart", 0), v("HiddenLoopEnd", secs))
fr = "<FileRef>%s%s%s%s%s</FileRef>" % (v("RelativePathType", 3 if rel else 0), v("RelativePath", rel), v("Path", path),
v("OriginalFileSize", size), v("OriginalCrc", 1))
return ('<AudioClip Id="0" Time="%s">%s%s%s%s%s%s<SampleRef>%s%s%s%s</SampleRef>%s%s%s%s%s<WarpMarkers /></AudioClip>'
% (start, v("CurrentStart", start), v("CurrentEnd", end), loop, v("Name", name), v("Disabled", "false"),
v("IsWarped", "true" if warped else "false"), fr, v("LastModDate", 0), v("DefaultDuration", 100), v("DefaultSampleRate", 48000),
v("WarpMode", 0), v("ComplexProFormants", 100), v("PitchCoarse", 0), v("Fade", "true"), v("Sync", "true")))
def midi_clip(start, end, name, notes=(), program=None):
kt = ('<Notes><KeyTracks><KeyTrack Id="0"><Notes>%s</Notes>%s</KeyTrack></KeyTracks></Notes>'
% ("".join('<MidiNoteEvent Time="%s" Duration="0.25" Velocity="100" IsEnabled="true" />' % n for n in notes), v("MidiKey", 66))
if notes else "<Notes><KeyTracks /></Notes>")
return ('<MidiClip Id="0" Time="%s">%s%s<Loop>%s%s</Loop>%s%s%s%s</MidiClip>'
% (start, v("CurrentStart", start), v("CurrentEnd", end), v("LoopStart", 0), v("LoopEnd", end - start), v("Name", name),
v("Disabled", "false"), kt, v("ProgramChange", program if program is not None else -1)))
def track(kind, id_, name, group="-1", clips="", out=("Master", "", "AudioOut/Master")):
seq = ("<Sample><ArrangerAutomation><Events>%s</Events></ArrangerAutomation></Sample>" if kind == "AudioTrack" else
"<ClipTimeable><ArrangerAutomation><Events>%s</Events></ArrangerAutomation></ClipTimeable>") % clips
if kind == "GroupTrack":
return ('<GroupTrack Id="%s"><Name>%s%s</Name>%s<Slots><GroupTrackSlot Id="0" /><GroupTrackSlot Id="1" /></Slots>'
'<DeviceChain>%s%s</DeviceChain></GroupTrack>' % (id_, v("EffectiveName", name), v("UserName", name), v("TrackGroupId", group),
mixer(), routing(*out)))
return ('<%s Id="%s"><Name>%s%s</Name>%s<DeviceChain>%s%s<MainSequencer>%s%s</MainSequencer></DeviceChain>'
'<AutomationEnvelopes><Envelopes /></AutomationEnvelopes></%s>'
% (kind, id_, v("EffectiveName", name), v("UserName", name), v("TrackGroupId", group), mixer(), routing(*out), slots(), seq, kind))
def live_set(tracks_xml, locators, tempo=72):
locs = "".join('<Locator Id="%d">%s%s%s<Mapping>%s%s</Mapping></Locator>' % (
i, v("Time", t), v("Name", n), v("Annotation", ""), v("PersistentKeyString", k), v("NoteOrController", -1))
for i, (t, n, k) in enumerate(locators))
return ('<?xml version="1.0" encoding="UTF-8"?><Ableton MajorVersion="5" Creator="Ableton Live 11.3.43"><LiveSet>%s'
'<Tracks>%s<ReturnTrack Id="900"><Name>%s%s</Name>%s<DeviceChain>%s%s</DeviceChain></ReturnTrack></Tracks>'
'<MasterTrack><DeviceChain><Mixer><Tempo>%s<AutomationTarget Id="8"><LockEnvelope Value="0" /></AutomationTarget></Tempo></Mixer></DeviceChain>'
'<AutomationEnvelopes><Envelopes><AutomationEnvelope Id="0"><EnvelopeTarget>%s</EnvelopeTarget><Automation><Events>'
'<FloatEvent Id="1" Time="-63072000" Value="%s" /></Events></Automation></AutomationEnvelope></Envelopes></AutomationEnvelopes></MasterTrack>'
'<Scenes><Scene Id="0" /><Scene Id="1" /></Scenes><Locators><Locators>%s</Locators></Locators></LiveSet></Ableton>'
% (v("NextPointeeId", 5000), tracks_xml, v("EffectiveName", "A-IN EAR MIX"), v("UserName", "A-IN EAR MIX"), v("TrackGroupId", -1),
mixer(0), routing(), v("Manual", tempo), v("PointeeId", 8), tempo, locs))
def write_als(path, xml):
os.makedirs(os.path.dirname(path), exist_ok=True)
with gzip.open(path, "wb") as f:
f.write(xml.encode())
def write_wav(path, seconds=2.0, rate=8000):
os.makedirs(os.path.dirname(path), exist_ok=True)
w = wave.open(path, "wb")
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(rate)
w.writeframes(b"\0\0" * int(seconds * rate))
w.close()
class TestBuild(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.dir)
d = self.dir
self.proj = os.path.join(d, "Projects")
base = os.path.join(self.proj, "NA", "261025 NA Project")
write_wav(os.path.join(base, "Samples/Imported/Piano.wav"))
write_wav(os.path.join(base, "Samples/Imported/D# - WARM - CHURCHFRONT PADS.wav"), 600)
write_wav(os.path.join(base, "Samples/Imported/LTC.wav"))
write_wav(os.path.join(base, "Samples/Imported/Guide.wav"))
p = lambda n: os.path.join(base, "Samples/Imported", n)
ext = ("Ext. Out", "5/6 Keys", "AudioOut/External/S2")
last_week = (
track("AudioTrack", 57, "TEMPO", clips=audio_clip(0, 1132, "143.00 BPM", "Samples/Recorded/x.aif", warped=True)) +
track("AudioTrack", 58, "LTC", clips=audio_clip(0, 4000, "LTC", p("LTC.wav"), "Samples/Imported/LTC.wav", True)) +
track("MidiTrack", 59, "SECTIONS", clips=midi_clip(0, 196, "CALL TO WORSHIP") + midi_clip(196, 236, "INTRO")) +
track("GroupTrack", 60, "MIDI GROUP") +
track("MidiTrack", 61, "Loop Cue", "60", midi_clip(138, 138.5, "", [0]) + midi_clip(699, 700, "") + midi_clip(1120.5, 1121, "", [0])) +
track("MidiTrack", 62, "WAVES TUNE", "60", midi_clip(0, 1, "Ebmaj", program=7) + midi_clip(196, 197, "Ebmaj", program=7)) +
track("MidiTrack", 63, "LIGHTING CUES", "60", midi_clip(196, 900, "LIGHTING CUES 56")) +
track("GroupTrack", 64, "RESI TRACKS") +
track("AudioTrack", 65, "+GUIDE", clips=audio_clip(196, 900, "Guide", p("Guide.wav"), "Samples/Imported/Guide.wav", True)) +
track("AudioTrack", 66, "+LOOPGUIDE") +
track("AudioTrack", 67, "AMBIENT PADS", clips=audio_clip(0, 237, "D# - WARM - CHURCHFRONT PADS", p("D# - WARM - CHURCHFRONT PADS.wav"),
"Samples/Imported/D# - WARM - CHURCHFRONT PADS.wav", secs=99.4)) +
track("GroupTrack", 70, "OLD SONG") +
track("AudioTrack", 71, "21-Piano", "70", audio_clip(196, 900, "Piano", p("Piano.wav"), "Samples/Imported/Piano.wav", secs=295.1), ext) +
track("AudioTrack", 72, "22-EG 1", "70", audio_clip(196, 900, "EG 1", p("Piano.wav"), "Samples/Imported/Piano.wav", secs=295.1),
("Ext. Out", "7/8 GTRS", "AudioOut/External/S3")))
locators = [(0, "1- CALL TO WORSHIP ", "1"), (76, "LOOP", ""), (196, "2 - OLD SONG- Eb", "2"), (1090, "LOOP", ""),
(1132, "3- SALVATION- Eb", "3"), (1500, "LOOP", "")]
write_als(os.path.join(base, "261025 NA Project.als"), live_set(last_week, locators, 143))
# Finished Songs: one song in E (to be used in E and, transposed, in B).
self.fin = os.path.join(d, "Finished Songs")
song = os.path.join(self.fin, "GRACE [E]")
write_wav(os.path.join(song, "Samples/Processed/Consolidate/Keys [x].aif".replace(".aif", ".wav")))
write_wav(os.path.join(song, "Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav"))
c = lambda n: os.path.join(song, "Samples/Processed/Consolidate", n)
song_tracks = (
track("GroupTrack", 0, "GRACE") +
track("MidiTrack", 1, "2-MIDI", "0", midi_clip(1992, 2024, "INTRO") + midi_clip(2024, 2056, "VERSE 1")) +
track("MidiTrack", 2, "3-MIDI", "0", midi_clip(1992, 2300, "LIGHTING CUES 47")) +
track("AudioTrack", 3, "4-Guide [2026-01-01 000000]", "0", audio_clip(1992, 2300, "Guide", c("Guide [2026-01-01 000000].wav"),
"Samples/Processed/Consolidate/Guide [2026-01-01 000000].wav", True)) +
track("AudioTrack", 4, "5-Keys 1 [x]", "0", audio_clip(1992, 2368, "Keys 1", c("Keys [x].wav"), "Samples/Processed/Consolidate/Keys [x].wav",
secs=313.3333333), ext))
write_als(os.path.join(song, "GRACE.als"), live_set(song_tracks, [], 143))
self.pads = os.path.join(d, "PADS")
for k in ("E", "B", "D#"):
write_wav(os.path.join(self.pads, "%s - WARM - CHURCHFRONT PADS.wav" % k), 600)
self.tune = os.path.join(d, "Waves Tune")
for name, prog in (("Emaj", 4), ("Bmaj", 11), ("Ebmaj", 7)):
write_als(os.path.join(self.tune, name + ".alc"), '<Ableton><MidiClip>%s</MidiClip></Ableton>' % v("ProgramChange", prog))
A.STATUS = os.path.join(d, "status.json")
self.cfg = {"projects": self.proj, "site_folder": "NA", "folder_format": "{date} {loc} Project",
"finished_folders": [self.fin], "pads_folder": self.pads, "tune_library": self.tune,
"routing": {"KEYS 1": ["Ext. Out", "5/6 Keys"], "EG 1": ["Ext. Out", "7/8 GTRS"]},
"keep_groups": ["MIDI GROUP", "RESI TRACKS"], "pad_track": "AMBIENT PADS", "tune_track": "WAVES TUNE",
"lighting_track": "LIGHTING CUES", "sections_track": "SECTIONS", "guide_track": "+GUIDE",
"loopguide_track": "+LOOPGUIDE", "loop_cue_track": "Loop Cue", "tempo_track": "TEMPO", "send_a": 1.0,
"services": [{"date": "261101", "folder": "NA", "name": "261101 NA Project", "label": "SUN 11/1",
"songs": [{"title": "Grace", "key": "E", "bpm": 72}, {"title": "Grace", "key": "B", "bpm": 72},
{"title": "Brand New", "key": "D", "bpm": 120}],
"salvation": {"title": "Grace TAG", "key": "B", "bpm": 72}}]}
def run_job(self):
A.run(self.cfg)
with open(A.STATUS) as f:
return json.load(f)
def test_builds_the_weeks_set(self):
st = self.run_job()
self.assertEqual(st["state"], "done", st.get("trace"))
# 1. Last week's song went to Finished Songs, with its markers, lights and guide inside the group.
self.assertEqual(st["saved"], ["OLD SONG [Eb]"])
old = A.load(os.path.join(self.fin, "OLD SONG [Eb]", "OLD SONG.als")).find("LiveSet")
self.assertEqual([A.tname(t) for t in A.tracks(old) if t.tag != "ReturnTrack"],
["OLD SONG", "SECTIONS", "LIGHTING CUES", "Guide", "21-Piano", "22-EG 1"])
self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]", "Samples/Imported/Piano.wav")))
# 2-6. The new set.
path = os.path.join(self.proj, "NA", "261101 NA Project", "261101 NA Project.als")
root = A.load(path)
ls = root.find("LiveSet")
self.assertEqual(A.check(root), [])
locs = sorted((A.fval(l, "Time"), A.val(l, "Name"), A.val(l, "Mapping/PersistentKeyString")) for l in ls.findall("Locators/Locators/Locator"))
names = [(n, k) for _, n, k in locs if not n.startswith("LOOP")]
self.assertEqual(names, [("1- CALL TO WORSHIP - E", "1"), ("2- GRACE - E", "2"), ("3- GRACE - B", "3"),
("4- BRAND NEW - D", "4"), ("5- SALVATION - B", "5")])
by = {A.tname(t): t for t in A.tracks(ls)}
# The 376-beat song pushed the 1090 loop along: the loop starts after the song ends.
loops = [t for t, n, _ in locs if n.startswith("LOOP")]
self.assertEqual(loops[:2], [76.0, 1090.0])
self.assertEqual([A.span(c)[0] for c in A.clips(by["Loop Cue"])][:2], [138.0, 1120.5])
self.assertNotIn(699.0, [A.span(c)[0] for c in A.clips(by["Loop Cue"])]) # an empty cue does nothing
tunes = [(A.span(c)[0], A.val(c, "Name"), A.val(c, "ProgramChange")) for c in A.clips(by["WAVES TUNE"])]
self.assertEqual([t[1:] for t in tunes], [("Emaj", "4"), ("Emaj", "4"), ("Bmaj", "11"), ("Bmaj", "11")]) # no D cue in the library
pads = [(A.val(c, "Name")[:2], A.span(c)) for c in A.clips(by["AMBIENT PADS"])]
self.assertEqual([p[0] for p in pads], ["E ", "E ", "B ", "B "]) # no D pad: named in the notes
self.assertEqual([A.tname(t) for t in A.tracks(ls) if t.tag == "GroupTrack" and A.group_of(t) == "-1"],
["MIDI GROUP", "RESI TRACKS", "GRACE", "GRACE"])
g2 = [t for t in A.tracks(ls) if A.tname(t) == "GRACE"][1]
keys = [t for t in A.family(ls, g2) if t.tag == "AudioTrack"][0]
c = A.clips(keys)[0]
self.assertEqual((A.val(c, "IsWarped"), A.val(c, "WarpMode"), A.val(c, "ComplexProFormants"), A.val(c, "PitchCoarse")),
("true", "6", "0", "-5"))
self.assertEqual(A.val(keys, "DeviceChain/Mixer/Sends/TrackSendHolder/Send/Manual"), "1")
self.assertEqual(A.val(keys, "DeviceChain/AudioOutputRouting/Target"), "AudioOut/External/S2")
self.assertEqual([A.val(c, "Name") for c in A.clips(by["SECTIONS"])], ["CALL TO WORSHIP", "INTRO", "VERSE 1", "INTRO", "VERSE 1"])
self.assertEqual(len(A.clips(by["+GUIDE"])), 2)
self.assertEqual(A.clips(by["+LOOPGUIDE"]), [])
fr = c.find("SampleRef/FileRef")
self.assertEqual(A.val(fr, "RelativePath"), "Samples/Imported/GRACE [E]/Keys [x].wav")
self.assertTrue(os.path.exists(os.path.join(os.path.dirname(path), "Samples/Imported/GRACE [E]/Keys [x].wav")))
self.assertTrue(os.path.exists(os.path.join(os.path.dirname(path), "Samples/Imported/LTC.wav")))
tempo = [(A.span(c)[0], A.val(c, "Name")) for c in A.clips(by["TEMPO"])]
self.assertEqual(tempo[0], (0.0, "72.00 BPM"))
self.assertIn((tempo[-1][0], "72.00 BPM"), tempo)
notes = st["results"][0]["notes"]
self.assertIn("TRANSPOSE GRACE E>B: CONSOLIDATE IN LIVE", notes)
self.assertIn("NEW SONG BRAND NEW: BUILD IT BY HAND", notes)
self.assertIn("NO PAD IN D", notes)
self.assertFalse([n for n in os.listdir(os.path.join(self.proj, "NA")) if n.endswith(".building")])
self.assertTrue(os.path.isdir(os.path.join(os.path.dirname(path), "Ableton Project Info"))) # else Live finds no audio
self.assertTrue(os.path.isdir(os.path.join(self.fin, "OLD SONG [Eb]", "Ableton Project Info")))
def test_a_set_that_is_there_is_left_alone(self):
os.makedirs(os.path.join(self.proj, "NA", "261101 NA Project"))
st = self.run_job()
self.assertEqual(st["step"], "NOTHING TO DO")
self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), [])
def test_an_unchanged_song_is_not_saved_again_and_a_changed_one_parks_the_old(self):
self.run_job()
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
st = self.run_job()
self.assertEqual(st["saved"], [])
self.assertFalse(os.path.exists(os.path.join(self.fin, "zOLD SONG [Eb]")))
# A re-consolidated stem (new file) is a new version: the old folder is parked as z.
base = os.path.join(self.proj, "NA", "261025 NA Project", "261025 NA Project.als")
xml = gzip.open(base).read().decode()
i = xml.index("21-Piano")
xml = xml[:i] + xml[i:].replace('Value="1000"', 'Value="2000"', 1)
write_als(base, xml)
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
st = self.run_job()
self.assertEqual(st["saved"], ["OLD SONG [Eb]"])
self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG [Eb]")))
def test_a_changed_song_replaces_the_version_it_was_made_from(self):
# Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped).
song = os.path.join(self.fin, "OLD SONG LIVE [Eb]")
write_wav(os.path.join(song, "Samples/Piano.wav"))
write_wav(os.path.join(song, "Samples/Keys.wav"))
write_als(os.path.join(song, "OLD SONG LIVE.als"), live_set(
track("GroupTrack", 0, "OLD SONG LIVE") +
track("AudioTrack", 1, "Piano", "0", audio_clip(0, 10, "Piano", os.path.join(song, "Samples/Piano.wav"), "Samples/Piano.wav")) +
track("AudioTrack", 2, "Keys", "0", audio_clip(0, 10, "Keys", os.path.join(song, "Samples/Keys.wav"), "Samples/Keys.wav")), []))
st = self.run_job()
self.assertEqual(st["saved"], ["OLD SONG LIVE [Eb]"])
self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG LIVE [Eb]")))
self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG LIVE [Eb]", "OLD SONG LIVE.als")))
self.assertFalse(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]")))
def test_titles(self):
self.assertTrue(A.close_titles("CAN'T HELP BUT PRAISE", "Can't Help But Praise LM"))
self.assertFalse(A.close_titles("PRAISE", "Owe You Praise"))
self.assertEqual(A.semitones("E", "B"), -5)
self.assertEqual(A.semitones("G", "A"), 2)
self.assertEqual(A.pick_version([("/f/GRACE [E]", "GRACE", "E"), ("/f/GRACE [B]", "GRACE", "B")], "Grace", "B")[0][2], "B")
def test_audio_info(self):
p = os.path.join(self.dir, "x.wav")
write_wav(p, 3.0, 8000)
self.assertEqual(A.audio_info(p), (24000, 8000))
if __name__ == "__main__":
unittest.main()