rushdaw 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rushdaw/__init__.py +3 -0
- rushdaw/__main__.py +4 -0
- rushdaw/cli.py +92 -0
- rushdaw/export.py +73 -0
- rushdaw/mcp.py +699 -0
- rushdaw/model.py +489 -0
- rushdaw/server.py +292 -0
- rushdaw/static/app.js +2113 -0
- rushdaw/static/index.html +124 -0
- rushdaw/static/samples/NOTICE.txt +26 -0
- rushdaw/static/samples/acoustic_guitar_steel/40.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/43.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/46.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/49.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/52.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/55.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/58.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/61.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/64.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/67.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/70.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/73.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/76.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/79.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/82.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/85.mp3 +0 -0
- rushdaw/static/samples/acoustic_guitar_steel/88.mp3 +0 -0
- rushdaw/static/samples/drums/chh-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/chh-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/chh-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/chh-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/chh-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/chh-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/chh-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/chh-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/chh-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/crash-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/crash-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/crash-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/crash-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/crash-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/crash-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/crash-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/crash-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/crash-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/kick-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/kick-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/kick-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/kick-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/kick-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/kick-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/kick-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/kick-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/kick-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/ohh-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/ride-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/ride-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/ride-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/ride-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/ride-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/ride-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/ride-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/ride-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/ride-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/snare-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/snare-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/snare-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/snare-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/snare-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/snare-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/snare-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/snare-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/snare-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomh-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/toml-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/toml-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/toml-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/toml-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/toml-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/toml-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/toml-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/toml-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/toml-3-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-1-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-1-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-1-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-2-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-2-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-2-3.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-3-1.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-3-2.mp3 +0 -0
- rushdaw/static/samples/drums/tomm-3-3.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/28.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/31.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/34.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/37.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/40.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/43.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/46.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/49.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/52.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/55.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/58.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/61.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/64.mp3 +0 -0
- rushdaw/static/samples/electric_bass_finger/67.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/28.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/31.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/34.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/37.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/40.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/43.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/46.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/49.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/52.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/55.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/58.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/61.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/64.mp3 +0 -0
- rushdaw/static/samples/electric_bass_pick/67.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/40.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/43.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/46.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/49.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/52.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/55.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/58.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/61.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/64.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/67.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/70.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/73.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/76.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/79.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/82.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/85.mp3 +0 -0
- rushdaw/static/samples/electric_guitar_clean/88.mp3 +0 -0
- rushdaw/static/style.css +211 -0
- rushdaw/storage.py +230 -0
- rushdaw-0.1.0.dist-info/METADATA +170 -0
- rushdaw-0.1.0.dist-info/RECORD +161 -0
- rushdaw-0.1.0.dist-info/WHEEL +5 -0
- rushdaw-0.1.0.dist-info/entry_points.txt +2 -0
- rushdaw-0.1.0.dist-info/licenses/LICENSE +21 -0
- rushdaw-0.1.0.dist-info/top_level.txt +1 -0
rushdaw/__init__.py
ADDED
rushdaw/__main__.py
ADDED
rushdaw/cli.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Command line entry point: ``rushdaw`` / ``python -m rushdaw``."""
|
|
2
|
+
import argparse
|
|
3
|
+
import errno
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.request
|
|
8
|
+
import threading
|
|
9
|
+
import webbrowser
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .server import make_server
|
|
13
|
+
|
|
14
|
+
DEFAULT_PORT = 8765
|
|
15
|
+
PORT_TRIES = 20 # when the default port is busy, try 8766, 8767, ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _url(host, port):
|
|
19
|
+
return f"http://{'localhost' if host in ('127.0.0.1', '0.0.0.0') else host}:{port}/"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _running_rushdaw(host, port):
|
|
23
|
+
"""Return /api/info of a RushDAW server already listening on host:port, else None."""
|
|
24
|
+
h = "127.0.0.1" if host in ("0.0.0.0", "") else host
|
|
25
|
+
try:
|
|
26
|
+
with urllib.request.urlopen(f"http://{h}:{port}/api/info", timeout=1.5) as r:
|
|
27
|
+
if not r.headers.get("Server", "").startswith("RushDAW/"):
|
|
28
|
+
return None
|
|
29
|
+
return json.loads(r.read().decode("utf-8"))
|
|
30
|
+
except Exception:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _bind(args):
|
|
35
|
+
"""Create the server. If the port is busy: reuse an already running RushDAW (same settings),
|
|
36
|
+
otherwise (default port only) move on to the next free port. Returns (httpd, None) or (None, url_of_running)."""
|
|
37
|
+
port = args.port or DEFAULT_PORT
|
|
38
|
+
ports = [port] if args.port else range(port, port + PORT_TRIES)
|
|
39
|
+
for pt in ports:
|
|
40
|
+
try:
|
|
41
|
+
return make_server(args.host, pt, args.home, args.api_key), None
|
|
42
|
+
except OSError as e:
|
|
43
|
+
if e.errno != errno.EADDRINUSE:
|
|
44
|
+
raise
|
|
45
|
+
info = _running_rushdaw(args.host, pt)
|
|
46
|
+
if info is not None and not args.home and not args.api_key:
|
|
47
|
+
return None, _url(args.host, pt) # RushDAW is already running there: just use it
|
|
48
|
+
why = "another RushDAW with different settings" if info is not None else "another program"
|
|
49
|
+
if args.port:
|
|
50
|
+
sys.exit(f"rushdaw: port {pt} is already in use by {why}. Start with a different --port, e.g. --port {pt + 1}.")
|
|
51
|
+
print(f"Port {pt} is in use by {why}, trying {pt + 1}…", flush=True)
|
|
52
|
+
sys.exit(f"rushdaw: no free port between {port} and {port + PORT_TRIES - 1}. Use --port.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv=None):
|
|
56
|
+
p = argparse.ArgumentParser(prog="rushdaw", description="RushDAW web based DAW")
|
|
57
|
+
p.add_argument("--host", default="127.0.0.1", help="bind address (default 127.0.0.1)")
|
|
58
|
+
p.add_argument("--port", type=int, default=None,
|
|
59
|
+
help=f"port (default {DEFAULT_PORT}; if busy, the next free port is used automatically)")
|
|
60
|
+
p.add_argument("--no-browser", action="store_true", help="do not open a browser automatically")
|
|
61
|
+
p.add_argument("--home", default=None, help="data folder (default ~/.rushdaw or $RUSHDAW_HOME)")
|
|
62
|
+
p.add_argument("--api-key", default=os.environ.get("RUSHDAW_API_KEY"),
|
|
63
|
+
help="optional: enable the MCP server (HTTP+SSE at /sse) protected by "
|
|
64
|
+
"'Authorization: Bearer <KEY>' (default: $RUSHDAW_API_KEY; MCP is off when not set)")
|
|
65
|
+
p.add_argument("--version", action="version", version=f"rushdaw {__version__}")
|
|
66
|
+
args = p.parse_args(argv)
|
|
67
|
+
|
|
68
|
+
httpd, running = _bind(args)
|
|
69
|
+
if running:
|
|
70
|
+
print(f"RushDAW is already running at {running} – opening it.", flush=True)
|
|
71
|
+
if not args.no_browser:
|
|
72
|
+
webbrowser.open(running)
|
|
73
|
+
return
|
|
74
|
+
url = _url(args.host, httpd.server_address[1])
|
|
75
|
+
print(f"RushDAW {__version__} running at {url} (Ctrl+C to stop)", flush=True)
|
|
76
|
+
print(f"Data: {httpd.RequestHandlerClass.storage.home} (db.sql + file/)", flush=True)
|
|
77
|
+
if args.api_key:
|
|
78
|
+
print(f"MCP: {url}sse (HTTP+SSE transport, header 'Authorization: Bearer <api key>')", flush=True)
|
|
79
|
+
else:
|
|
80
|
+
print("MCP: off (optional – start with --api-key KEY to enable)", flush=True)
|
|
81
|
+
if not args.no_browser:
|
|
82
|
+
threading.Timer(0.8, lambda: webbrowser.open(url)).start()
|
|
83
|
+
try:
|
|
84
|
+
httpd.serve_forever()
|
|
85
|
+
except KeyboardInterrupt:
|
|
86
|
+
print("\nStopping RushDAW.")
|
|
87
|
+
finally:
|
|
88
|
+
httpd.server_close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
rushdaw/export.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Convert a rendered WAV mixdown to other formats with ffmpeg."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
# format -> (extension, mime, ffmpeg output args)
|
|
8
|
+
FORMATS = {
|
|
9
|
+
"wav": ("wav", "audio/wav", None),
|
|
10
|
+
"mp3": ("mp3", "audio/mpeg", ["-codec:a", "libmp3lame", "-b:a", "320k"]),
|
|
11
|
+
"ogg": ("ogg", "audio/ogg", ["-codec:a", "libvorbis", "-q:a", "6"]),
|
|
12
|
+
"flac": ("flac", "audio/flac", ["-codec:a", "flac"]),
|
|
13
|
+
"m4a": ("m4a", "audio/mp4", ["-codec:a", "aac", "-b:a", "256k"]),
|
|
14
|
+
"aac": ("aac", "audio/aac", ["-codec:a", "aac", "-b:a", "256k"]),
|
|
15
|
+
"opus": ("opus", "audio/ogg", ["-codec:a", "libopus", "-b:a", "192k"]),
|
|
16
|
+
"mp4": ("mp4", "video/mp4", "VIDEO"),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_ffmpeg():
|
|
21
|
+
"""Return path to an ffmpeg binary (system one first, then imageio-ffmpeg)."""
|
|
22
|
+
exe = shutil.which("ffmpeg")
|
|
23
|
+
if exe:
|
|
24
|
+
return exe
|
|
25
|
+
try:
|
|
26
|
+
import imageio_ffmpeg
|
|
27
|
+
|
|
28
|
+
return imageio_ffmpeg.get_ffmpeg_exe()
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def available_formats():
|
|
34
|
+
if find_ffmpeg():
|
|
35
|
+
return list(FORMATS)
|
|
36
|
+
return ["wav"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def convert(wav_bytes, fmt, title="RushDAW"):
|
|
40
|
+
"""Convert WAV bytes into ``fmt``. Returns (bytes, ext, mime)."""
|
|
41
|
+
fmt = fmt.lower()
|
|
42
|
+
if fmt not in FORMATS:
|
|
43
|
+
raise ValueError(f"unsupported format: {fmt}")
|
|
44
|
+
ext, mime, args = FORMATS[fmt]
|
|
45
|
+
if args is None:
|
|
46
|
+
return wav_bytes, ext, mime
|
|
47
|
+
ffmpeg = find_ffmpeg()
|
|
48
|
+
if not ffmpeg:
|
|
49
|
+
raise RuntimeError("ffmpeg not available; install imageio-ffmpeg or ffmpeg")
|
|
50
|
+
safe_title = "".join(c for c in title if c.isalnum() or c in " -_.")[:80] or "RushDAW"
|
|
51
|
+
with tempfile.TemporaryDirectory(prefix="rushdaw_") as tmp:
|
|
52
|
+
src = os.path.join(tmp, "in.wav")
|
|
53
|
+
dst = os.path.join(tmp, "out." + ext)
|
|
54
|
+
with open(src, "wb") as f:
|
|
55
|
+
f.write(wav_bytes)
|
|
56
|
+
cmd = [ffmpeg, "-y", "-hide_banner", "-loglevel", "error", "-i", src]
|
|
57
|
+
if args == "VIDEO":
|
|
58
|
+
# music video: animated waveform on dark background
|
|
59
|
+
cmd += [
|
|
60
|
+
"-filter_complex",
|
|
61
|
+
"[0:a]showwaves=s=1280x720:mode=cline:rate=30:colors=0x00e5ff|0xffffff,format=yuv420p[v]",
|
|
62
|
+
"-map", "[v]", "-map", "0:a",
|
|
63
|
+
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
|
|
64
|
+
"-c:a", "aac", "-b:a", "256k", "-shortest", "-movflags", "+faststart",
|
|
65
|
+
]
|
|
66
|
+
else:
|
|
67
|
+
cmd += args
|
|
68
|
+
cmd += ["-metadata", f"title={safe_title}", "-metadata", "encoder=RushDAW", dst]
|
|
69
|
+
proc = subprocess.run(cmd, capture_output=True, timeout=900)
|
|
70
|
+
if proc.returncode != 0 or not os.path.exists(dst):
|
|
71
|
+
raise RuntimeError(proc.stderr.decode("utf-8", "replace")[-800:] or "ffmpeg failed")
|
|
72
|
+
with open(dst, "rb") as f:
|
|
73
|
+
return f.read(), ext, mime
|