- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频 (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine 状态机接口、MainActivity 全屏封面 + TalkBack - 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局 - 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速 - 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链 - 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/ asset-pipeline(资产流水线)/ games/*(三款游戏设计文档) - 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账, 全部 ogg 资产入库可复现 - env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
216 lines
7.5 KiB
Python
216 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
insert_audio.py — CLI tool to insert audio files into Ableton Live.
|
|
|
|
Usage:
|
|
python insert_audio.py # insert latest file in ~/Desktop
|
|
python insert_audio.py /path/to/file.wav # insert specific file
|
|
python insert_audio.py --watch ~/renders # watch folder, auto-insert on new file
|
|
python insert_audio.py --ping # check if Ableton is connected
|
|
|
|
Options:
|
|
--dir DIR Directory to search for latest file (default: ~/Desktop)
|
|
--ext EXT File extension filter, e.g. wav, aiff, mp3 (default: wav aiff mp3 flac)
|
|
--watch Watch mode: monitor DIR and auto-insert new files as they appear
|
|
--ping Just check if the Ableton script is running
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import socket
|
|
import argparse
|
|
import time
|
|
|
|
HOST = "127.0.0.1"
|
|
PORT = 9129
|
|
TIMEOUT = 5.0
|
|
AUDIO_EXTENSIONS = {".wav", ".aiff", ".aif", ".mp3", ".flac", ".ogg", ".m4a"}
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Socket communication
|
|
# ─────────────────────────────────────────────
|
|
|
|
def send_command(cmd: dict) -> dict:
|
|
"""Send a JSON command to the Ableton Remote Script and return the response."""
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(TIMEOUT)
|
|
s.connect((HOST, PORT))
|
|
s.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
|
|
response = b""
|
|
while True:
|
|
chunk = s.recv(4096)
|
|
if not chunk:
|
|
break
|
|
response += chunk
|
|
if b"\n" in chunk:
|
|
break
|
|
s.close()
|
|
return json.loads(response.decode("utf-8").strip())
|
|
except ConnectionRefusedError:
|
|
return {"ok": False, "error": "Cannot connect to Ableton. Make sure:\n"
|
|
" 1. Ableton Live is open\n"
|
|
" 2. AudioInserter is selected in Preferences → MIDI → Control Surface"}
|
|
except socket.timeout:
|
|
return {"ok": False, "error": "Ableton timed out — is a project open?"}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|
|
def ping():
|
|
result = send_command({"action": "ping"})
|
|
if result.get("ok"):
|
|
print("✓ Ableton is connected and ready")
|
|
return True
|
|
else:
|
|
print("✗ " + result.get("error", "Unknown error"))
|
|
return False
|
|
|
|
|
|
def insert_file(file_path: str) -> bool:
|
|
abs_path = os.path.abspath(file_path)
|
|
if not os.path.isfile(abs_path):
|
|
print(f"✗ File not found: {abs_path}")
|
|
return False
|
|
|
|
print(f"→ Inserting: {os.path.basename(abs_path)}")
|
|
track_name = os.environ.get("_SA3_LAST_PROMPT", "")
|
|
result = send_command({"action": "insert_audio", "file_path": abs_path, "track_name": track_name})
|
|
|
|
if result.get("ok"):
|
|
msg = result.get("message", "Done")
|
|
print(f"✓ {msg}")
|
|
return True
|
|
else:
|
|
print(f"✗ {result.get('error', 'Unknown error')}")
|
|
return False
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# File discovery
|
|
# ─────────────────────────────────────────────
|
|
|
|
def find_latest_audio(directory: str, extensions: set) -> str | None:
|
|
"""Return the most recently modified audio file in the given directory."""
|
|
directory = os.path.expanduser(directory)
|
|
if not os.path.isdir(directory):
|
|
print(f"✗ Directory not found: {directory}")
|
|
return None
|
|
|
|
candidates = []
|
|
for f in os.listdir(directory):
|
|
ext = os.path.splitext(f)[1].lower()
|
|
if ext in extensions:
|
|
full = os.path.join(directory, f)
|
|
candidates.append((os.path.getmtime(full), full))
|
|
|
|
if not candidates:
|
|
exts = " ".join(sorted(extensions))
|
|
print(f"✗ No audio files found in {directory}\n (looking for: {exts})")
|
|
return None
|
|
|
|
candidates.sort(reverse=True)
|
|
return candidates[0][1]
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# Watch mode
|
|
# ─────────────────────────────────────────────
|
|
|
|
def watch_directory(directory: str, extensions: set):
|
|
"""Monitor a directory and auto-insert new audio files as they appear."""
|
|
directory = os.path.expanduser(directory)
|
|
if not os.path.isdir(directory):
|
|
print(f"✗ Directory not found: {directory}")
|
|
sys.exit(1)
|
|
|
|
print(f"👁 Watching {directory} for new audio files... (Ctrl+C to stop)\n")
|
|
|
|
# Record existing files so we don't re-insert them on start
|
|
seen = set()
|
|
for f in os.listdir(directory):
|
|
if os.path.splitext(f)[1].lower() in extensions:
|
|
seen.add(os.path.join(directory, f))
|
|
|
|
try:
|
|
while True:
|
|
time.sleep(0.75)
|
|
for f in os.listdir(directory):
|
|
ext = os.path.splitext(f)[1].lower()
|
|
if ext not in extensions:
|
|
continue
|
|
full = os.path.join(directory, f)
|
|
if full not in seen:
|
|
seen.add(full)
|
|
# Short wait to ensure file write is complete
|
|
time.sleep(0.3)
|
|
print(f"\n[{time.strftime('%H:%M:%S')}] New file detected")
|
|
insert_file(full)
|
|
except KeyboardInterrupt:
|
|
print("\n\nStopped watching.")
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# CLI entry point
|
|
# ─────────────────────────────────────────────
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Insert audio files into Ableton Live at the current playhead position.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
parser.add_argument(
|
|
"file", nargs="?", help="Audio file to insert (omit to use latest file in --dir)"
|
|
)
|
|
parser.add_argument(
|
|
"--dir", default="~/Desktop", help="Directory to search for latest audio file"
|
|
)
|
|
parser.add_argument(
|
|
"--ext", nargs="+", default=None,
|
|
help="File extensions to consider, e.g. --ext wav aiff"
|
|
)
|
|
parser.add_argument(
|
|
"--watch", action="store_true",
|
|
help="Watch --dir and auto-insert new files as they appear"
|
|
)
|
|
parser.add_argument(
|
|
"--ping", action="store_true",
|
|
help="Check if Ableton is running and the script is active"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Build extension set
|
|
if args.ext:
|
|
extensions = {"." + e.lstrip(".").lower() for e in args.ext}
|
|
else:
|
|
extensions = AUDIO_EXTENSIONS
|
|
|
|
# ── Ping ──
|
|
if args.ping:
|
|
sys.exit(0 if ping() else 1)
|
|
|
|
# ── Watch mode ──
|
|
if args.watch:
|
|
watch_directory(args.dir, extensions)
|
|
return
|
|
|
|
# ── Single insert ──
|
|
if args.file:
|
|
file_path = args.file
|
|
else:
|
|
file_path = find_latest_audio(args.dir, extensions)
|
|
if not file_path:
|
|
sys.exit(1)
|
|
print(f"Latest file: {os.path.basename(file_path)}")
|
|
|
|
success = insert_file(file_path)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|