nostoi-livecli 0.2.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.
livecli/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """LiveCLI: drive Ableton Live from the command line (and from AI agents)."""
2
+ __version__ = "0.2.0"
livecli/audiofile.py ADDED
@@ -0,0 +1,115 @@
1
+ """Read WAV and AIFF/AIFC files with the standard library only (no ffmpeg): integer PCM 8/16/24/32 bit and 32/64-bit
2
+ float, any channel count, mixed down to mono. Enough for the files Live records and bounces."""
3
+ import array
4
+ import math
5
+ import struct
6
+ import sys
7
+
8
+
9
+ class AudioFileError(Exception):
10
+ pass
11
+
12
+
13
+ def _ext80(b):
14
+ """IEEE 754 80-bit extended (AIFF sample rate) -> float."""
15
+ exp = ((b[0] & 0x7F) << 8) | b[1]
16
+ mant = int.from_bytes(b[2:10], "big")
17
+ if exp == 0 and mant == 0:
18
+ return 0.0
19
+ sign = -1.0 if b[0] & 0x80 else 1.0
20
+ return sign * mant * math.pow(2.0, exp - 16383 - 63)
21
+
22
+
23
+ def _chunks(data, start, big_endian):
24
+ pos, fmt = start, ">4sI" if big_endian else "<4sI"
25
+ while pos + 8 <= len(data):
26
+ cid, size = struct.unpack_from(fmt, data, pos)
27
+ yield cid, data[pos + 8: pos + 8 + size]
28
+ pos += 8 + size + (size & 1)
29
+
30
+
31
+ def _decode(raw, bits, is_float, big_endian, channels):
32
+ """Interleaved frames -> list of mono floats in [-1, 1]."""
33
+ width = bits // 8
34
+ if is_float:
35
+ a = array.array("f" if bits == 32 else "d")
36
+ a.frombytes(raw[: len(raw) // a.itemsize * a.itemsize])
37
+ if big_endian != (sys.byteorder == "big"):
38
+ a.byteswap()
39
+ vals = a
40
+ elif width in (2, 4):
41
+ a = array.array("h" if width == 2 else "i")
42
+ if a.itemsize != width:
43
+ raise AudioFileError("unsupported integer width on this platform")
44
+ a.frombytes(raw[: len(raw) // width * width])
45
+ if big_endian != (sys.byteorder == "big"):
46
+ a.byteswap()
47
+ scale = float(1 << (bits - 1))
48
+ vals = [v / scale for v in a]
49
+ elif width == 3:
50
+ n = len(raw) // 3
51
+ order = "big" if big_endian else "little"
52
+ vals = [int.from_bytes(raw[3 * k: 3 * k + 3], order, signed=True) / 8388608.0 for k in range(n)]
53
+ elif width == 1:
54
+ # WAV 8-bit is unsigned, AIFF 8-bit is signed
55
+ vals = [((v - 128) if not big_endian else (v - 256 if v > 127 else v)) / 128.0 for v in raw]
56
+ else:
57
+ raise AudioFileError("unsupported sample size: %d bits" % bits)
58
+ if channels == 1:
59
+ return list(vals)
60
+ n = len(vals) // channels
61
+ return [sum(vals[k * channels: k * channels + channels]) / channels for k in range(n)]
62
+
63
+
64
+ def read_mono(path, offset_s=0.0, duration_s=None):
65
+ """(samples, sample_rate) of a WAV/AIFF file, mono, from offset_s for duration_s seconds."""
66
+ with open(path, "rb") as f:
67
+ data = f.read()
68
+ head = data[:4]
69
+ if head == b"RIFF" and data[8:12] == b"WAVE":
70
+ fmt = pcm = None
71
+ for cid, body in _chunks(data, 12, False):
72
+ if cid == b"fmt ":
73
+ tag, ch, sr, _, _, bits = struct.unpack_from("<HHIIHH", body, 0)
74
+ if tag == 0xFFFE and len(body) >= 26: # WAVE_FORMAT_EXTENSIBLE: real tag in the sub-format GUID
75
+ tag = struct.unpack_from("<H", body, 24)[0]
76
+ fmt = (tag, ch, sr, bits)
77
+ elif cid == b"data":
78
+ pcm = body
79
+ if not fmt or pcm is None:
80
+ raise AudioFileError("WAV without fmt or data chunk")
81
+ tag, ch, sr, bits = fmt
82
+ if tag not in (1, 3):
83
+ raise AudioFileError("unsupported WAV encoding (format tag %d)" % tag)
84
+ is_float, big = tag == 3, False
85
+ elif head == b"FORM" and data[8:12] in (b"AIFF", b"AIFC"):
86
+ comm = ssnd = None
87
+ for cid, body in _chunks(data, 12, True):
88
+ if cid == b"COMM":
89
+ comm = body
90
+ elif cid == b"SSND":
91
+ ssnd = body
92
+ if comm is None or ssnd is None:
93
+ raise AudioFileError("AIFF without COMM or SSND chunk")
94
+ ch, _, bits = struct.unpack_from(">hIh", comm, 0)
95
+ sr = _ext80(comm[8:18])
96
+ comp = comm[18:22] if data[8:12] == b"AIFC" and len(comm) >= 22 else b"NONE"
97
+ if comp in (b"NONE", b"twos"):
98
+ is_float, big = False, True
99
+ elif comp == b"sowt":
100
+ is_float, big = False, False
101
+ elif comp in (b"fl32", b"FL32"):
102
+ is_float, big, bits = True, True, 32
103
+ elif comp in (b"fl64", b"FL64"):
104
+ is_float, big, bits = True, True, 64
105
+ else:
106
+ raise AudioFileError("unsupported AIFC compression %r" % comp)
107
+ off = struct.unpack_from(">I", ssnd, 0)[0]
108
+ pcm = ssnd[8 + off:]
109
+ else:
110
+ raise AudioFileError("not a WAV or AIFF file")
111
+ sr = int(round(sr))
112
+ frame = (bits // 8) * ch
113
+ first = int(round(offset_s * sr)) * frame
114
+ last = len(pcm) if duration_s is None else min(len(pcm), first + int(round(duration_s * sr)) * frame)
115
+ return _decode(pcm[first:last], bits, is_float, big, ch), sr