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.
Files changed (161) hide show
  1. rushdaw/__init__.py +3 -0
  2. rushdaw/__main__.py +4 -0
  3. rushdaw/cli.py +92 -0
  4. rushdaw/export.py +73 -0
  5. rushdaw/mcp.py +699 -0
  6. rushdaw/model.py +489 -0
  7. rushdaw/server.py +292 -0
  8. rushdaw/static/app.js +2113 -0
  9. rushdaw/static/index.html +124 -0
  10. rushdaw/static/samples/NOTICE.txt +26 -0
  11. rushdaw/static/samples/acoustic_guitar_steel/40.mp3 +0 -0
  12. rushdaw/static/samples/acoustic_guitar_steel/43.mp3 +0 -0
  13. rushdaw/static/samples/acoustic_guitar_steel/46.mp3 +0 -0
  14. rushdaw/static/samples/acoustic_guitar_steel/49.mp3 +0 -0
  15. rushdaw/static/samples/acoustic_guitar_steel/52.mp3 +0 -0
  16. rushdaw/static/samples/acoustic_guitar_steel/55.mp3 +0 -0
  17. rushdaw/static/samples/acoustic_guitar_steel/58.mp3 +0 -0
  18. rushdaw/static/samples/acoustic_guitar_steel/61.mp3 +0 -0
  19. rushdaw/static/samples/acoustic_guitar_steel/64.mp3 +0 -0
  20. rushdaw/static/samples/acoustic_guitar_steel/67.mp3 +0 -0
  21. rushdaw/static/samples/acoustic_guitar_steel/70.mp3 +0 -0
  22. rushdaw/static/samples/acoustic_guitar_steel/73.mp3 +0 -0
  23. rushdaw/static/samples/acoustic_guitar_steel/76.mp3 +0 -0
  24. rushdaw/static/samples/acoustic_guitar_steel/79.mp3 +0 -0
  25. rushdaw/static/samples/acoustic_guitar_steel/82.mp3 +0 -0
  26. rushdaw/static/samples/acoustic_guitar_steel/85.mp3 +0 -0
  27. rushdaw/static/samples/acoustic_guitar_steel/88.mp3 +0 -0
  28. rushdaw/static/samples/drums/chh-1-1.mp3 +0 -0
  29. rushdaw/static/samples/drums/chh-1-2.mp3 +0 -0
  30. rushdaw/static/samples/drums/chh-1-3.mp3 +0 -0
  31. rushdaw/static/samples/drums/chh-2-1.mp3 +0 -0
  32. rushdaw/static/samples/drums/chh-2-2.mp3 +0 -0
  33. rushdaw/static/samples/drums/chh-2-3.mp3 +0 -0
  34. rushdaw/static/samples/drums/chh-3-1.mp3 +0 -0
  35. rushdaw/static/samples/drums/chh-3-2.mp3 +0 -0
  36. rushdaw/static/samples/drums/chh-3-3.mp3 +0 -0
  37. rushdaw/static/samples/drums/crash-1-1.mp3 +0 -0
  38. rushdaw/static/samples/drums/crash-1-2.mp3 +0 -0
  39. rushdaw/static/samples/drums/crash-1-3.mp3 +0 -0
  40. rushdaw/static/samples/drums/crash-2-1.mp3 +0 -0
  41. rushdaw/static/samples/drums/crash-2-2.mp3 +0 -0
  42. rushdaw/static/samples/drums/crash-2-3.mp3 +0 -0
  43. rushdaw/static/samples/drums/crash-3-1.mp3 +0 -0
  44. rushdaw/static/samples/drums/crash-3-2.mp3 +0 -0
  45. rushdaw/static/samples/drums/crash-3-3.mp3 +0 -0
  46. rushdaw/static/samples/drums/kick-1-1.mp3 +0 -0
  47. rushdaw/static/samples/drums/kick-1-2.mp3 +0 -0
  48. rushdaw/static/samples/drums/kick-1-3.mp3 +0 -0
  49. rushdaw/static/samples/drums/kick-2-1.mp3 +0 -0
  50. rushdaw/static/samples/drums/kick-2-2.mp3 +0 -0
  51. rushdaw/static/samples/drums/kick-2-3.mp3 +0 -0
  52. rushdaw/static/samples/drums/kick-3-1.mp3 +0 -0
  53. rushdaw/static/samples/drums/kick-3-2.mp3 +0 -0
  54. rushdaw/static/samples/drums/kick-3-3.mp3 +0 -0
  55. rushdaw/static/samples/drums/ohh-1-1.mp3 +0 -0
  56. rushdaw/static/samples/drums/ohh-1-2.mp3 +0 -0
  57. rushdaw/static/samples/drums/ohh-1-3.mp3 +0 -0
  58. rushdaw/static/samples/drums/ohh-2-1.mp3 +0 -0
  59. rushdaw/static/samples/drums/ohh-2-2.mp3 +0 -0
  60. rushdaw/static/samples/drums/ohh-2-3.mp3 +0 -0
  61. rushdaw/static/samples/drums/ohh-3-1.mp3 +0 -0
  62. rushdaw/static/samples/drums/ohh-3-2.mp3 +0 -0
  63. rushdaw/static/samples/drums/ohh-3-3.mp3 +0 -0
  64. rushdaw/static/samples/drums/ride-1-1.mp3 +0 -0
  65. rushdaw/static/samples/drums/ride-1-2.mp3 +0 -0
  66. rushdaw/static/samples/drums/ride-1-3.mp3 +0 -0
  67. rushdaw/static/samples/drums/ride-2-1.mp3 +0 -0
  68. rushdaw/static/samples/drums/ride-2-2.mp3 +0 -0
  69. rushdaw/static/samples/drums/ride-2-3.mp3 +0 -0
  70. rushdaw/static/samples/drums/ride-3-1.mp3 +0 -0
  71. rushdaw/static/samples/drums/ride-3-2.mp3 +0 -0
  72. rushdaw/static/samples/drums/ride-3-3.mp3 +0 -0
  73. rushdaw/static/samples/drums/snare-1-1.mp3 +0 -0
  74. rushdaw/static/samples/drums/snare-1-2.mp3 +0 -0
  75. rushdaw/static/samples/drums/snare-1-3.mp3 +0 -0
  76. rushdaw/static/samples/drums/snare-2-1.mp3 +0 -0
  77. rushdaw/static/samples/drums/snare-2-2.mp3 +0 -0
  78. rushdaw/static/samples/drums/snare-2-3.mp3 +0 -0
  79. rushdaw/static/samples/drums/snare-3-1.mp3 +0 -0
  80. rushdaw/static/samples/drums/snare-3-2.mp3 +0 -0
  81. rushdaw/static/samples/drums/snare-3-3.mp3 +0 -0
  82. rushdaw/static/samples/drums/tomh-1-1.mp3 +0 -0
  83. rushdaw/static/samples/drums/tomh-1-2.mp3 +0 -0
  84. rushdaw/static/samples/drums/tomh-1-3.mp3 +0 -0
  85. rushdaw/static/samples/drums/tomh-2-1.mp3 +0 -0
  86. rushdaw/static/samples/drums/tomh-2-2.mp3 +0 -0
  87. rushdaw/static/samples/drums/tomh-2-3.mp3 +0 -0
  88. rushdaw/static/samples/drums/tomh-3-1.mp3 +0 -0
  89. rushdaw/static/samples/drums/tomh-3-2.mp3 +0 -0
  90. rushdaw/static/samples/drums/tomh-3-3.mp3 +0 -0
  91. rushdaw/static/samples/drums/toml-1-1.mp3 +0 -0
  92. rushdaw/static/samples/drums/toml-1-2.mp3 +0 -0
  93. rushdaw/static/samples/drums/toml-1-3.mp3 +0 -0
  94. rushdaw/static/samples/drums/toml-2-1.mp3 +0 -0
  95. rushdaw/static/samples/drums/toml-2-2.mp3 +0 -0
  96. rushdaw/static/samples/drums/toml-2-3.mp3 +0 -0
  97. rushdaw/static/samples/drums/toml-3-1.mp3 +0 -0
  98. rushdaw/static/samples/drums/toml-3-2.mp3 +0 -0
  99. rushdaw/static/samples/drums/toml-3-3.mp3 +0 -0
  100. rushdaw/static/samples/drums/tomm-1-1.mp3 +0 -0
  101. rushdaw/static/samples/drums/tomm-1-2.mp3 +0 -0
  102. rushdaw/static/samples/drums/tomm-1-3.mp3 +0 -0
  103. rushdaw/static/samples/drums/tomm-2-1.mp3 +0 -0
  104. rushdaw/static/samples/drums/tomm-2-2.mp3 +0 -0
  105. rushdaw/static/samples/drums/tomm-2-3.mp3 +0 -0
  106. rushdaw/static/samples/drums/tomm-3-1.mp3 +0 -0
  107. rushdaw/static/samples/drums/tomm-3-2.mp3 +0 -0
  108. rushdaw/static/samples/drums/tomm-3-3.mp3 +0 -0
  109. rushdaw/static/samples/electric_bass_finger/28.mp3 +0 -0
  110. rushdaw/static/samples/electric_bass_finger/31.mp3 +0 -0
  111. rushdaw/static/samples/electric_bass_finger/34.mp3 +0 -0
  112. rushdaw/static/samples/electric_bass_finger/37.mp3 +0 -0
  113. rushdaw/static/samples/electric_bass_finger/40.mp3 +0 -0
  114. rushdaw/static/samples/electric_bass_finger/43.mp3 +0 -0
  115. rushdaw/static/samples/electric_bass_finger/46.mp3 +0 -0
  116. rushdaw/static/samples/electric_bass_finger/49.mp3 +0 -0
  117. rushdaw/static/samples/electric_bass_finger/52.mp3 +0 -0
  118. rushdaw/static/samples/electric_bass_finger/55.mp3 +0 -0
  119. rushdaw/static/samples/electric_bass_finger/58.mp3 +0 -0
  120. rushdaw/static/samples/electric_bass_finger/61.mp3 +0 -0
  121. rushdaw/static/samples/electric_bass_finger/64.mp3 +0 -0
  122. rushdaw/static/samples/electric_bass_finger/67.mp3 +0 -0
  123. rushdaw/static/samples/electric_bass_pick/28.mp3 +0 -0
  124. rushdaw/static/samples/electric_bass_pick/31.mp3 +0 -0
  125. rushdaw/static/samples/electric_bass_pick/34.mp3 +0 -0
  126. rushdaw/static/samples/electric_bass_pick/37.mp3 +0 -0
  127. rushdaw/static/samples/electric_bass_pick/40.mp3 +0 -0
  128. rushdaw/static/samples/electric_bass_pick/43.mp3 +0 -0
  129. rushdaw/static/samples/electric_bass_pick/46.mp3 +0 -0
  130. rushdaw/static/samples/electric_bass_pick/49.mp3 +0 -0
  131. rushdaw/static/samples/electric_bass_pick/52.mp3 +0 -0
  132. rushdaw/static/samples/electric_bass_pick/55.mp3 +0 -0
  133. rushdaw/static/samples/electric_bass_pick/58.mp3 +0 -0
  134. rushdaw/static/samples/electric_bass_pick/61.mp3 +0 -0
  135. rushdaw/static/samples/electric_bass_pick/64.mp3 +0 -0
  136. rushdaw/static/samples/electric_bass_pick/67.mp3 +0 -0
  137. rushdaw/static/samples/electric_guitar_clean/40.mp3 +0 -0
  138. rushdaw/static/samples/electric_guitar_clean/43.mp3 +0 -0
  139. rushdaw/static/samples/electric_guitar_clean/46.mp3 +0 -0
  140. rushdaw/static/samples/electric_guitar_clean/49.mp3 +0 -0
  141. rushdaw/static/samples/electric_guitar_clean/52.mp3 +0 -0
  142. rushdaw/static/samples/electric_guitar_clean/55.mp3 +0 -0
  143. rushdaw/static/samples/electric_guitar_clean/58.mp3 +0 -0
  144. rushdaw/static/samples/electric_guitar_clean/61.mp3 +0 -0
  145. rushdaw/static/samples/electric_guitar_clean/64.mp3 +0 -0
  146. rushdaw/static/samples/electric_guitar_clean/67.mp3 +0 -0
  147. rushdaw/static/samples/electric_guitar_clean/70.mp3 +0 -0
  148. rushdaw/static/samples/electric_guitar_clean/73.mp3 +0 -0
  149. rushdaw/static/samples/electric_guitar_clean/76.mp3 +0 -0
  150. rushdaw/static/samples/electric_guitar_clean/79.mp3 +0 -0
  151. rushdaw/static/samples/electric_guitar_clean/82.mp3 +0 -0
  152. rushdaw/static/samples/electric_guitar_clean/85.mp3 +0 -0
  153. rushdaw/static/samples/electric_guitar_clean/88.mp3 +0 -0
  154. rushdaw/static/style.css +211 -0
  155. rushdaw/storage.py +230 -0
  156. rushdaw-0.1.0.dist-info/METADATA +170 -0
  157. rushdaw-0.1.0.dist-info/RECORD +161 -0
  158. rushdaw-0.1.0.dist-info/WHEEL +5 -0
  159. rushdaw-0.1.0.dist-info/entry_points.txt +2 -0
  160. rushdaw-0.1.0.dist-info/licenses/LICENSE +21 -0
  161. rushdaw-0.1.0.dist-info/top_level.txt +1 -0
rushdaw/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """RushDAW - a web based digital audio workstation served by Python."""
2
+
3
+ __version__ = "0.1.0"
rushdaw/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
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