START saves every song every week (old one parked z/zz), logs SAVED/PARKED on the key; SONG KEYS no longer warns ONLY <key>; install.sh and the location kit ship ableton_start.py (NA had the 10/22 copy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -153,7 +153,7 @@ everything on the page at once.
|
|||||||
| FOH MIX | **NOT MIXED** (amber): every track of a song is still at 0 dB. **TO MASTER**: a stem goes to Master instead of out to the console | looks again |
|
| FOH MIX | **NOT MIXED** (amber): every track of a song is still at 0 dB. **TO MASTER**: a stem goes to Master instead of out to the console | looks again |
|
||||||
| LIGHTS | the `LIGHTING CUES` track has a clip under every song | looks again |
|
| LIGHTS | the `LIGHTING CUES` track has a clip under every song | looks again |
|
||||||
| LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens around it (less than half a 16th note either way) for the strongest hit. **3 CUES OFF** (amber): a strong hit more than 25 ms before or after the cue while the cue itself sits on a quiet spot; the details say `JEHOVAH bar 329.2.1: Blinder Flash 2 is 37 ms before the hit` (bars as Live shows them, 4/4). Listened to once per saved set. | looks again (move the note in Live by hand: Live's control script cannot edit notes in the arrangement) |
|
| LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens around it (less than half a 16th note either way) for the strongest hit. **3 CUES OFF** (amber): a strong hit more than 25 ms before or after the cue while the cue itself sits on a quiet spot; the details say `JEHOVAH bar 329.2.1: Blinder Flash 2 is 37 ms before the hit` (bars as Live shows them, 4/4). Listened to once per saved set. | looks again (move the note in Live by hand: Live's control script cannot edit notes in the arrangement) |
|
||||||
| SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. **JEHOVAH ONLY Db** (amber): Finished Songs has the song, but not in this key (save the new version there). A song that is not in Finished Songs at all is only noted. | looks again |
|
| SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. Finished Songs having the song only in another key is fine (START transposes it). A song that is not in Finished Songs at all is only noted. | looks again |
|
||||||
| W TUNE | the `WAVES TUNE` clip that starts each song (a clip from `Ableton/Waves Tune - midi Program Change`, sent to Waves Tune as a program change) is the song's PCO key; a minor song is fine on its relative major. **JEHOVAH TUNE Db** names the song; the details say which clip to put at which bar. **TUNE PGM**: a renamed clip that sends another key's program. | looks again (Live's control script cannot change a clip in the arrangement: swap the clip by hand) |
|
| W TUNE | the `WAVES TUNE` clip that starts each song (a clip from `Ableton/Waves Tune - midi Program Change`, sent to Waves Tune as a program change) is the song's PCO key; a minor song is fine on its relative major. **JEHOVAH TUNE Db** names the song; the details say which clip to put at which bar. **TUNE PGM**: a renamed clip that sends another key's program. | looks again (Live's control script cannot change a clip in the arrangement: swap the clip by hand) |
|
||||||
| ROUTING | read **live** from Ableton (AbletonOSC, so tracks just dragged in count before the set is saved): every stem goes where `ready.routing` says (Piano, Keys, Organ → `Ext. Out 5/6 Keys`; AG, EG, Bass → `7/8 GTRS`; Drums, Perc, FX, Synth Bass → `9/10 PERC / FX`; BGVS, Choir, Vox FX → `11/12 VOX`). The table was taken from 261025 NA Project and is edited on the settings page; "EG 13" goes where every other EG goes. **PIANO TO MASTER** (red) names the stem; **NEW STEM BANJO** (amber): a stem name the table does not know. | puts every stem back where the table says (and IN EARS' sends to 0 dB). Runs by itself in the auto-fix hours. |
|
| ROUTING | read **live** from Ableton (AbletonOSC, so tracks just dragged in count before the set is saved): every stem goes where `ready.routing` says (Piano, Keys, Organ → `Ext. Out 5/6 Keys`; AG, EG, Bass → `7/8 GTRS`; Drums, Perc, FX, Synth Bass → `9/10 PERC / FX`; BGVS, Choir, Vox FX → `11/12 VOX`). The table was taken from 261025 NA Project and is edited on the settings page; "EG 13" goes where every other EG goes. **PIANO TO MASTER** (red) names the stem; **NEW STEM BANJO** (amber): a stem name the table does not know. | puts every stem back where the table says (and IN EARS' sends to 0 dB). Runs by itself in the auto-fix hours. |
|
||||||
| IN EARS | read live: no stem in a song group has its in-ear send (send A, to `A-IN EAR MIX`) at -inf, as songs dragged in from another set do. Levels someone set are left alone; `RESI TRACKS` and `MIDI GROUP` (`ready.ears_skip_groups`) are skipped. | sets those sends to 0 dB. Runs by itself in the auto-fix hours. |
|
| IN EARS | read live: no stem in a song group has its in-ear send (send A, to `A-IN EAR MIX`) at -inf, as songs dragged in from another set do. Levels someone set are left alone; `RESI TRACKS` and `MIDI GROUP` (`ready.ears_skip_groups`) are skipped. | sets those sends to 0 dB. Runs by itself in the auto-fix hours. |
|
||||||
@@ -194,9 +194,10 @@ other special service `Projects/NA/<date> NA <plan title> Project`.
|
|||||||
`ableton_start.py` then runs on the Tracks Mac (detached, at low priority), doing the workflow PDF's steps:
|
`ableton_start.py` then runs on the Tracks Mac (detached, at low priority), doing the workflow PDF's steps:
|
||||||
1. **Last weekend's songs to Finished Songs.** Each song group of the newest weekend set goes to
|
1. **Last weekend's songs to Finished Songs.** Each song group of the newest weekend set goes to
|
||||||
`Finished Songs/TITLE [KEY]` (key from its locator), with its arrangement markers, lighting cues and guide inside
|
`Finished Songs/TITLE [KEY]` (key from its locator), with its arrangement markers, lighting cues and guide inside
|
||||||
the group and its audio copied in. A song that matches a saved version (any key, parked or not) is left alone.
|
the group and its audio copied in. Every song is saved again every week, changed or not: it replaces the version
|
||||||
A changed one replaces the version it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called
|
it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called `CAN'T HELP BUT PRAISE`), which is parked
|
||||||
`CAN'T HELP BUT PRAISE`), which is parked first as `zCAN'T HELP BUT PRAISE LM [Eb]`.
|
first as `zCAN'T HELP BUT PRAISE LM [Eb]` (`zz...` when that name is taken), so the old ones sort to the bottom.
|
||||||
|
START's details list each `SAVED` and `PARKED` folder.
|
||||||
2. **A new set from the newest weekend set:** its songs, pads, Waves Tune cues and marker, guide, loop-guide and
|
2. **A new set from the newest weekend set:** its songs, pads, Waves Tune cues and marker, guide, loop-guide and
|
||||||
lighting clips go; the groups, the numbered locators (and their computer keys 1-9), the loop locators and the loop
|
lighting clips go; the groups, the numbered locators (and their computer keys 1-9), the loop locators and the loop
|
||||||
cues stay. Empty loop cues are dropped.
|
cues stay. Empty loop cues are dropped.
|
||||||
|
|||||||
+7
-8
@@ -5,8 +5,8 @@ does them by hand on Mondays. readiness.py sends this file to the Tracks Mac and
|
|||||||
every special service (First / Together Tuesday, Morning Worship, ...).
|
every special service (First / Together Tuesday, Morning Worship, ...).
|
||||||
|
|
||||||
1 the previous weekend set's songs go to Finished Songs as "TITLE [KEY]": the song group with its
|
1 the previous weekend set's songs go to Finished Songs as "TITLE [KEY]": the song group with its
|
||||||
arrangement markers, lighting cues and guide inside it, the audio copied in. A song that changed parks
|
arrangement markers, lighting cues and guide inside it, the audio copied in. Every song, every week:
|
||||||
the old folder as "zTITLE [KEY]" first; an unchanged one is left as it is.
|
the old folder is parked as "zTITLE [KEY]" ("zzTITLE [KEY]" when that is taken) first.
|
||||||
2 each new set ("261101 NA Project", "261103 NA First Tuesday Project") starts from the newest weekend
|
2 each new set ("261101 NA Project", "261103 NA First Tuesday Project") starts from the newest weekend
|
||||||
set: its songs, pads, Waves Tune cues and marker, guide and lighting clips go; the dedicated groups,
|
set: its songs, pads, Waves Tune cues and marker, guide and lighting clips go; the dedicated groups,
|
||||||
the numbered locators (with their computer-key mappings), the loop locators and the loop cues stay.
|
the numbered locators (with their computer-key mappings), the loop locators and the loop cues stay.
|
||||||
@@ -604,7 +604,7 @@ def helper_copy(src, gid, new_id, keep_from, keep_to, name):
|
|||||||
|
|
||||||
|
|
||||||
def export_songs(prev_path, cfg, status, log):
|
def export_songs(prev_path, cfg, status, log):
|
||||||
"""Step 1: every song of the previous weekend set to Finished Songs (new or changed ones only)."""
|
"""Step 1: every song of the previous weekend set to Finished Songs, the old version parked."""
|
||||||
root = load(prev_path)
|
root = load(prev_path)
|
||||||
ls = root.find("LiveSet")
|
ls = root.find("LiveSet")
|
||||||
proj = os.path.dirname(prev_path)
|
proj = os.path.dirname(prev_path)
|
||||||
@@ -623,11 +623,9 @@ def export_songs(prev_path, cfg, status, log):
|
|||||||
continue
|
continue
|
||||||
mine = stems_signature(_wrap(fam), proj)
|
mine = stems_signature(_wrap(fam), proj)
|
||||||
versions = list(saved_versions(cfg, dest_dir, title))
|
versions = list(saved_versions(cfg, dest_dir, title))
|
||||||
# Saved already, under this name or a near one ("CAN'T HELP BUT PRAISE LM [Eb]" for a group called
|
# Every song is saved again every week (user, 2026-10-27): it replaces the version it was made from (most
|
||||||
# "CAN'T HELP BUT PRAISE"), parked or not: then it came from there and is left as it is.
|
# of the same audio, same key: "CAN'T HELP BUT PRAISE LM [Eb]" for a group called "CAN'T HELP BUT PRAISE"),
|
||||||
if any(sig == mine for _, sig in versions):
|
# else "TITLE [KEY]"; the old folder is parked as "zTITLE [KEY]" ("zz..." when that is taken).
|
||||||
continue
|
|
||||||
# Changed: it replaces the version it was made from (most of the same audio, same key), else a new name.
|
|
||||||
def overlap(sig):
|
def overlap(sig):
|
||||||
a, b = set(sig), set(mine)
|
a, b = set(sig), set(mine)
|
||||||
return len(a & b) / float(len(a | b) or 1)
|
return len(a & b) / float(len(a | b) or 1)
|
||||||
@@ -684,6 +682,7 @@ def export_songs(prev_path, cfg, status, log):
|
|||||||
os.rename(building, dest)
|
os.rename(building, dest)
|
||||||
index[folder.upper()] = folder
|
index[folder.upper()] = folder
|
||||||
done.append(folder)
|
done.append(folder)
|
||||||
|
log.append("SAVED %s" % folder)
|
||||||
if files.missing:
|
if files.missing:
|
||||||
log.append("%s: %d FILES MISSING" % (folder, len(files.missing)))
|
log.append("%s: %d FILES MISSING" % (folder, len(files.missing)))
|
||||||
return done
|
return done
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ SETTINGS_LABEL=org.waterschurch.status-settings
|
|||||||
DEST="$HOME/Library/Application Support/status-panel"
|
DEST="$HOME/Library/Application Support/status-panel"
|
||||||
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||||
DOMAIN="gui/$(id -u)"
|
DOMAIN="gui/$(id -u)"
|
||||||
CODE=(status_panel.py readiness.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py)
|
CODE=(status_panel.py readiness.py ableton_start.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py)
|
||||||
KEEP_CONFIG=0
|
KEEP_CONFIG=0
|
||||||
SITE=""
|
SITE=""
|
||||||
SITE_NEXT=0
|
SITE_NEXT=0
|
||||||
|
|||||||
+4
-4
@@ -1182,9 +1182,8 @@ def eval_keys(rc, plan, look):
|
|||||||
elif any(same_key(k, want) for _, k in versions):
|
elif any(same_key(k, want) for _, k in versions):
|
||||||
checked += 1
|
checked += 1
|
||||||
else:
|
else:
|
||||||
line.fault(WARN, "%s ONLY %s" % (name, "/".join(sorted({k for _, k in versions}))),
|
# No warning (user, 2026-10-27): START transposes a song saved in another key.
|
||||||
"%s has %s, none in %s" % (" / ".join(folders), ", ".join(n for n, _ in versions), want))
|
checked += 1
|
||||||
bad = True
|
|
||||||
if not checked and not bad:
|
if not checked and not bad:
|
||||||
line.fault(WARN, "KEY UNCHECKED " + short(title), "no locator with a key, no pad under it and nothing in Finished Songs "
|
line.fault(WARN, "KEY UNCHECKED " + short(title), "no locator with a key, no pad under it and nothing in Finished Songs "
|
||||||
"for %s (%s in PCO)" % (title, want))
|
"for %s (%s in PCO)" % (title, want))
|
||||||
@@ -2202,7 +2201,8 @@ def eval_start(rc, services, look, now):
|
|||||||
% (svc["name"], song, n, "" if n == 1 else "s"))
|
% (svc["name"], song, n, "" if n == 1 else "s"))
|
||||||
if st.get("state") == "done" and now - (st.get("time") or 0) < 86400:
|
if st.get("state") == "done" and now - (st.get("time") or 0) < 86400:
|
||||||
for note in st.get("log") or []:
|
for note in st.get("log") or []:
|
||||||
if "CONSOLIDATE IN LIVE:" in note or note.startswith("OPEN ") or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note:
|
if "CONSOLIDATE IN LIVE:" in note or note.startswith(("OPEN ", "SAVED ", "ALREADY SAVED ", "PARKED ", "NOT SAVED ", "RENAMED ")) \
|
||||||
|
or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note:
|
||||||
line.notes.append(note)
|
line.notes.append(note)
|
||||||
line.info = " ".join(x["label"] for x in services) or "NO SERVICES"
|
line.info = " ".join(x["label"] for x in services) or "NO SERVICES"
|
||||||
return line
|
return line
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ TRIGGER_NAME = "flash_tick_500"
|
|||||||
LOOK_EVERY = 600
|
LOOK_EVERY = 600
|
||||||
SEEN, LAST = "discover-seen.json", "discover-last.json"
|
SEEN, LAST = "discover-seen.json", "discover-last.json"
|
||||||
# What the program kit carries (no settings, keys or history of the location that serves it).
|
# What the program kit carries (no settings, keys or history of the location that serves it).
|
||||||
KIT_FILES = ("status_panel.py", "readiness.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py",
|
KIT_FILES = ("status_panel.py", "readiness.py", "ableton_start.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py",
|
||||||
"nav_keys.py", "atem_watch.js", "atem_state.js", "nav_templates.json", "prop_guard.py")
|
"nav_keys.py", "atem_watch.js", "atem_state.js", "nav_templates.json", "prop_guard.py")
|
||||||
KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup")
|
KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup")
|
||||||
|
|
||||||
|
|||||||
@@ -302,10 +302,9 @@ class TestKeys(unittest.TestCase):
|
|||||||
def test_the_set_in_another_key(self):
|
def test_the_set_in_another_key(self):
|
||||||
keys = dict(GOOD, **{"Holy Forever": "C"})
|
keys = dict(GOOD, **{"Holy Forever": "C"})
|
||||||
line = rd.eval_keys(RC, keyed(keys), with_pads_and_finished())
|
line = rd.eval_keys(RC, keyed(keys), with_pads_and_finished())
|
||||||
# Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb)
|
# Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb): no warning, START transposes it
|
||||||
self.assertEqual(msgs(line), ["REJOICE AGAIN ONLY B", "HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C",
|
self.assertEqual(msgs(line), ["HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C"])
|
||||||
"HOLY FOREVER ONLY B"])
|
self.assertEqual([lv for lv, _, _ in line.faults], [rd.ERR, rd.ERR])
|
||||||
self.assertEqual([lv for lv, _, _ in line.faults], [rd.WARN, rd.ERR, rd.ERR, rd.WARN])
|
|
||||||
|
|
||||||
def test_a_song_pco_has_no_key_for(self):
|
def test_a_song_pco_has_no_key_for(self):
|
||||||
line = rd.eval_keys(RC, keyed(dict(GOOD, **{"Holy Forever": ""})), with_pads_and_finished())
|
line = rd.eval_keys(RC, keyed(dict(GOOD, **{"Holy Forever": ""})), with_pads_and_finished())
|
||||||
|
|||||||
+7
-13
@@ -349,22 +349,16 @@ class TestBuild(unittest.TestCase):
|
|||||||
self.assertEqual(st["step"], "NOTHING TO DO")
|
self.assertEqual(st["step"], "NOTHING TO DO")
|
||||||
self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), [])
|
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):
|
def test_every_song_is_saved_every_week_and_the_old_ones_parked(self):
|
||||||
|
"""User, 2026-10-27: every song saved new every week; the old one goes to z, zz, ... (the bottom of the folder)."""
|
||||||
self.run_job()
|
self.run_job()
|
||||||
|
for parked in ("zOLD SONG [Eb]", "zzOLD SONG [Eb]"):
|
||||||
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
|
shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project"))
|
||||||
st = self.run_job()
|
st = self.run_job() # unchanged, saved again all the same
|
||||||
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.assertEqual(st["saved"], ["OLD SONG [Eb]"])
|
||||||
self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG [Eb]")))
|
self.assertIn("SAVED OLD SONG [Eb]", st["log"])
|
||||||
|
self.assertTrue(os.path.isdir(os.path.join(self.fin, parked)))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]", "OLD SONG.als")))
|
||||||
|
|
||||||
def test_a_changed_song_replaces_the_version_it_was_made_from(self):
|
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).
|
# Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped).
|
||||||
|
|||||||
Reference in New Issue
Block a user