ableton-js 3.6.0 → 3.7.0

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.
package/CHANGELOG.md CHANGED
@@ -4,8 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v3.7.0](https://github.com/leolabs/ableton.js/compare/v3.6.1...v3.7.0)
8
+
9
+ - Add more clip and note operation features in Ableton Live 11 and above versions [`#133`](https://github.com/leolabs/ableton.js/pull/133)
10
+ - Add new functions and fix bugs [`#130`](https://github.com/leolabs/ableton.js/pull/130)
11
+ - adds ability to launch v12 via yarn [`#131`](https://github.com/leolabs/ableton.js/pull/131)
12
+
13
+ #### [v3.6.1](https://github.com/leolabs/ableton.js/compare/v3.6.0...v3.6.1)
14
+
15
+ > 12 March 2025
16
+
17
+ - :bug: Fix zlib expecting a string on Python 2 (Live 10) [`8480ab4`](https://github.com/leolabs/ableton.js/commit/8480ab4a5b6c253a905e1ba61de53d6728c68411)
18
+ - :bug: Fix ableton-js not working with Live 10 anymore, due to syntax that Python 2 doesn't support [`04ba830`](https://github.com/leolabs/ableton.js/commit/04ba8309a4b7912c022aa604a90fed47a7a56ed9)
19
+
7
20
  #### [v3.6.0](https://github.com/leolabs/ableton.js/compare/v3.5.0...v3.6.0)
8
21
 
22
+ > 16 January 2025
23
+
9
24
  - :label: Mark all raw object state as readonly [`ace0efd`](https://github.com/leolabs/ableton.js/commit/ace0efde39e5b598dd0a2878e0e29ecffedd495a)
10
25
  - :sparkles: Add options to override the caching mechanism [`85405d4`](https://github.com/leolabs/ableton.js/commit/85405d4be89bdc052f53bb862c5fe904620c554a)
11
26
 
@@ -27,6 +27,39 @@ class Clip(Interface):
27
27
  def get_notes(self, ns, from_time=0, from_pitch=0, time_span=99999999999999, pitch_span=128):
28
28
  return ns.get_notes(from_time, from_pitch, time_span, pitch_span)
29
29
 
30
+ def get_notes_extended(self, ns, from_time=0, from_pitch=0, time_span=99999999999999, pitch_span=128):
31
+ midi_note_vector = ns.get_notes_extended(from_pitch, pitch_span, from_time, time_span)
32
+ return [
33
+ {
34
+ "duration": note.duration,
35
+ "mute": note.mute,
36
+ "note_id": note.note_id,
37
+ "pitch": note.pitch,
38
+ "probability": note.probability,
39
+ "release_velocity": note.release_velocity,
40
+ "start_time": note.start_time,
41
+ "velocity": note.velocity,
42
+ "velocity_deviation": note.velocity_deviation
43
+ }
44
+ for note in midi_note_vector
45
+ ]
46
+
47
+ def apply_note_modifications(self, ns, notes):
48
+ existing_notes = ns.get_notes_extended(0, 128, 0, 99999999999999)
49
+ existing_notes_map = {note.note_id: note for note in existing_notes}
50
+
51
+ for modified_note_data in notes:
52
+ note_id = modified_note_data.get("note_id")
53
+ if note_id is None:
54
+ raise ValueError("The note_id parameter is required to modify the note.")
55
+ if note_id in existing_notes_map:
56
+ note_to_update = existing_notes_map[note_id]
57
+ for key, value in modified_note_data.items():
58
+ if key != "note_id" and hasattr(note_to_update, key):
59
+ setattr(note_to_update, key, value)
60
+
61
+ return ns.apply_note_modifications(existing_notes)
62
+
30
63
  def get_warp_markers(self, ns):
31
64
  dict_markers = []
32
65
  for warp_marker in ns.warp_markers:
@@ -41,3 +74,4 @@ class Clip(Interface):
41
74
 
42
75
  def replace_selected_notes(self, ns, notes):
43
76
  return ns.replace_selected_notes(tuple(notes))
77
+
@@ -21,7 +21,7 @@ class Session(Interface):
21
21
  """
22
22
  with self.controlSurface.component_guard():
23
23
  logger.info(
24
- f"Setting up session box with {num_tracks} tracks and {num_scenes} scenes.")
24
+ "Setting up session box with " + str(num_tracks) + " tracks and " + str(num_scenes) + " scenes.")
25
25
  self.session = self.sessionComponent(num_tracks, num_scenes)
26
26
  self.session.set_offsets(0, 0)
27
27
  self.controlSurface.set_highlighting_session_component(
@@ -33,7 +33,7 @@ class Session(Interface):
33
33
  Sets the offset of the SessionComponent instance.
34
34
  """
35
35
  logger.info(
36
- f"Moving session box offset to {track_offset} and {scene_offset}.")
36
+ "Moving session box offset to " + str(track_offset) + " and " + scene_offset + ".")
37
37
 
38
38
  if hasattr(self, 'session'):
39
39
  self.session.set_offsets(track_offset, scene_offset)
@@ -4,6 +4,7 @@ import struct
4
4
  import zlib
5
5
  import os
6
6
  import tempfile
7
+ import sys
7
8
 
8
9
  from .Logging import logger
9
10
 
@@ -87,7 +88,8 @@ class Socket(object):
87
88
  if self._socket:
88
89
  self.send("connect", {"port": self._server_addr[1]})
89
90
  except Exception as e:
90
- self.log_error_once("Couldn't read remote port file: " + str(e.args))
91
+ self.log_error_once(
92
+ "Couldn't read remote port file: " + str(e.args))
91
93
 
92
94
  def shutdown(self):
93
95
  logger.info("Shutting down...")
@@ -125,13 +127,15 @@ class Socket(object):
125
127
  with open(server_port_path, "w") as file:
126
128
  file.write(str(port))
127
129
  except Exception as e:
128
- self.log_error_once("Couldn't save port in file: " + str(e.args))
130
+ self.log_error_once(
131
+ "Couldn't save port in file: " + str(e.args))
129
132
  raise e
130
133
 
131
134
  try:
132
135
  self.send("connect", {"port": self._server_addr[1]})
133
136
  except Exception as e:
134
- logger.error("Couldn't send connect to " + str(self._client_addr) + ":")
137
+ logger.error("Couldn't send connect to " +
138
+ str(self._client_addr) + ":")
135
139
  logger.exception(e)
136
140
 
137
141
  self.show_message("Started server on port " + str(port))
@@ -143,7 +147,8 @@ class Socket(object):
143
147
  str(self._server_addr) + ': ' + \
144
148
  str(e.args) + ', trying again. ' + \
145
149
  'If this keeps happening, try restarting your computer.'
146
- self.log_error_once(msg + " (Client address: " + str(self._client_addr) + ")")
150
+ self.log_error_once(
151
+ msg + " (Client address: " + str(self._client_addr) + ")")
147
152
  self.show_message(msg)
148
153
  t = Live.Base.Timer(
149
154
  callback=self.init_socket, interval=5000, repeat=False)
@@ -160,15 +165,18 @@ class Socket(object):
160
165
  message_id_byte = struct.pack("B", self._message_id)
161
166
 
162
167
  if len(compressed) < self._chunk_limit:
163
- self._socket.sendto(message_id_byte + b'\x00\x01' + compressed, self._client_addr)
168
+ self._socket.sendto(
169
+ message_id_byte + b'\x00\x01' + compressed, self._client_addr)
164
170
  else:
165
171
  chunks = list(split_by_n(compressed, self._chunk_limit))
166
172
  count = len(chunks)
167
173
  count_byte = struct.pack("B", count)
168
174
  for i, chunk in enumerate(chunks):
169
- logger.info("Sending packet " + str(self._message_id) + " - " + str(i) + "/" + str(count))
175
+ logger.info("Sending packet " + str(self._message_id) +
176
+ " - " + str(i) + "/" + str(count))
170
177
  packet_byte = struct.pack("B", i)
171
- self._socket.sendto(message_id_byte + packet_byte + count_byte + chunk, self._client_addr)
178
+ self._socket.sendto(
179
+ message_id_byte + packet_byte + count_byte + chunk, self._client_addr)
172
180
 
173
181
  def send(self, name, obj=None, uuid=None):
174
182
  def jsonReplace(o):
@@ -188,7 +196,8 @@ class Socket(object):
188
196
  except socket.error as e:
189
197
  logger.error("Socket error:")
190
198
  logger.exception(e)
191
- logger.error("Server: " + str(self._server_addr) + ", client: " + str(self._client_addr) + ", socket: " + str(self._socket))
199
+ logger.error("Server: " + str(self._server_addr) + ", client: " +
200
+ str(self._client_addr) + ", socket: " + str(self._socket))
192
201
  logger.error("Data:" + data)
193
202
  except Exception as e:
194
203
  logger.error("Error " + name + "(" + str(uuid) + "):")
@@ -205,7 +214,17 @@ class Socket(object):
205
214
  if (data[0] == b'\xFF' or data[0] == 255):
206
215
  packet = self._receive_buffer
207
216
  self._receive_buffer = bytearray()
217
+
218
+ # Handle Python 2/3 compatibility for zlib.decompress
219
+ if sys.version_info[0] < 3:
220
+ packet = str(packet)
221
+
208
222
  unzipped = zlib.decompress(packet)
223
+
224
+ # Handle bytes to string conversion for Python 3
225
+ if sys.version_info[0] >= 3 and isinstance(unzipped, bytes):
226
+ unzipped = unzipped.decode('utf-8')
227
+
209
228
  payload = json.loads(unzipped)
210
229
  self.input_handler(payload)
211
230
 
@@ -78,4 +78,8 @@ class Track(Interface):
78
78
  return MixerDevice.serialize_mixer_device(ns.mixer_device)
79
79
 
80
80
  def duplicate_clip_to_arrangement(self, ns, clip_id, time):
81
- return ns.duplicate_clip_to_arrangement(self.get_obj(clip_id), time)
81
+ clip = ns.duplicate_clip_to_arrangement(self.get_obj(clip_id), time)
82
+ return Clip.serialize_clip(clip)
83
+
84
+ def delete_clip(self, ns, clip_id):
85
+ return ns.delete_clip(self.get_obj(clip_id))
@@ -1 +1 @@
1
- version = "3.6.0"
1
+ version = "3.7.0"
package/ns/clip.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Ableton } from "..";
2
2
  import { Namespace } from ".";
3
3
  import { Color } from "../util/color";
4
4
  import { DeviceParameter } from "./device-parameter";
5
- import { Note, NoteTuple } from "../util/note";
5
+ import { Note, NoteExtended, NoteTuple } from "../util/note";
6
6
  export declare enum WarpMode {
7
7
  Beats = 0,
8
8
  Tones = 1,
@@ -201,8 +201,18 @@ export declare class Clip extends Namespace<GettableProperties, TransformedPrope
201
201
  fire(): Promise<void>;
202
202
  /**
203
203
  * Returns all notes that match the given range.
204
+ * @deprecated starting with Live 11, use `getNotesExtended` instead
204
205
  */
205
206
  getNotes(fromTime: number, fromPitch: number, timeSpan: number, pitchSpan: number): Promise<Note[]>;
207
+ /**
208
+ * Returns all notes matching the given range with extended properties.
209
+ * Compared to getNotes, this method returns additional note information.
210
+ */
211
+ getNotesExtended(fromTime: number, fromPitch: number, timeSpan: number, pitchSpan: number): Promise<NoteExtended[]>;
212
+ /**
213
+ * Available since Live 11.0. Replaces modifying notes with remove_notes followed by set_notes.
214
+ */
215
+ applyNoteModifications(notes: NoteExtended[]): Promise<any>;
206
216
  /**
207
217
  * Jump forward or backward by the specified relative amount in beats.
208
218
  * Will do nothing if the clip is not playing.
@@ -226,6 +236,11 @@ export declare class Clip extends Namespace<GettableProperties, TransformedPrope
226
236
  * Deletes all notes that start in the given area.
227
237
  */
228
238
  removeNotesExtended(fromTime: number, fromPitch: number, timeSpan: number, pitchSpan: number): Promise<any>;
239
+ /**
240
+ * Remove notes by given note ids.
241
+ * Available since Live 11.0.
242
+ */
243
+ removeNotesById(ids: number[]): Promise<any>;
229
244
  /**
230
245
  * Replaces selected notes with an array of new notes.
231
246
  */
package/ns/clip.js CHANGED
@@ -124,6 +124,7 @@ class Clip extends _1.Namespace {
124
124
  }
125
125
  /**
126
126
  * Returns all notes that match the given range.
127
+ * @deprecated starting with Live 11, use `getNotesExtended` instead
127
128
  */
128
129
  async getNotes(fromTime, fromPitch, timeSpan, pitchSpan) {
129
130
  const notes = await this.sendCommand("get_notes", {
@@ -134,6 +135,24 @@ class Clip extends _1.Namespace {
134
135
  });
135
136
  return notes.map(note_1.tupleToNote);
136
137
  }
138
+ /**
139
+ * Returns all notes matching the given range with extended properties.
140
+ * Compared to getNotes, this method returns additional note information.
141
+ */
142
+ async getNotesExtended(fromTime, fromPitch, timeSpan, pitchSpan) {
143
+ return this.sendCommand("get_notes_extended", {
144
+ from_pitch: fromPitch,
145
+ pitch_span: pitchSpan,
146
+ from_time: fromTime,
147
+ time_span: timeSpan,
148
+ });
149
+ }
150
+ /**
151
+ * Available since Live 11.0. Replaces modifying notes with remove_notes followed by set_notes.
152
+ */
153
+ applyNoteModifications(notes) {
154
+ return this.sendCommand("apply_note_modifications", { notes });
155
+ }
137
156
  /**
138
157
  * Jump forward or backward by the specified relative amount in beats.
139
158
  * Will do nothing if the clip is not playing.
@@ -171,12 +190,19 @@ class Clip extends _1.Namespace {
171
190
  */
172
191
  removeNotesExtended(fromTime, fromPitch, timeSpan, pitchSpan) {
173
192
  return this.sendCommand("remove_notes_extended", [
174
- fromTime,
175
193
  fromPitch,
176
- timeSpan,
177
194
  pitchSpan,
195
+ fromTime,
196
+ timeSpan,
178
197
  ]);
179
198
  }
199
+ /**
200
+ * Remove notes by given note ids.
201
+ * Available since Live 11.0.
202
+ */
203
+ removeNotesById(ids) {
204
+ return this.sendCommand("remove_notes_by_id", [ids]);
205
+ }
180
206
  /**
181
207
  * Replaces selected notes with an array of new notes.
182
208
  */
package/ns/track.d.ts CHANGED
@@ -158,5 +158,19 @@ export declare class Track extends Namespace<GettableProperties, TransformedProp
158
158
  * Duplicates the given clip into the arrangement of this track at the provided destination time and returns it.
159
159
  * When the type of the clip and the type of the track are incompatible, a runtime error is raised.
160
160
  */
161
- duplicateClipToArrangement(clipOrId: Clip | string, time: number): Promise<any>;
161
+ duplicateClipToArrangement(clipOrId: Clip | string, time: number): Promise<Clip>;
162
+ /**
163
+ * Deletes the given clip from the arrangement of this track.
164
+ * Raises a runtime error when the clip belongs to another track
165
+ */
166
+ deleteClip(clipOrId: Clip | string): Promise<any>;
167
+ /**
168
+ * Delete a device identified by the index in the 'devices' list of current track
169
+ */
170
+ deleteDevice(index: number): Promise<any>;
171
+ /**
172
+ * Given an absolute path to a valid audio file in a supported format, creates an audio clip that references the file in the clip slot.
173
+ * Throws an error if the clip slot doesn't belong to an audio track or if the track is frozen.
174
+ */
175
+ createAudioClip(filePath: string, position: number): Promise<any>;
162
176
  }
package/ns/track.js CHANGED
@@ -48,11 +48,34 @@ class Track extends _1.Namespace {
48
48
  * Duplicates the given clip into the arrangement of this track at the provided destination time and returns it.
49
49
  * When the type of the clip and the type of the track are incompatible, a runtime error is raised.
50
50
  */
51
- duplicateClipToArrangement(clipOrId, time) {
52
- return this.sendCommand("duplicate_clip_to_arrangement", {
51
+ async duplicateClipToArrangement(clipOrId, time) {
52
+ const rawClip = await this.sendCommand("duplicate_clip_to_arrangement", {
53
53
  clip_id: typeof clipOrId === "string" ? clipOrId : clipOrId.raw.id,
54
54
  time: time,
55
55
  });
56
+ return new clip_1.Clip(this.ableton, rawClip);
57
+ }
58
+ /**
59
+ * Deletes the given clip from the arrangement of this track.
60
+ * Raises a runtime error when the clip belongs to another track
61
+ */
62
+ deleteClip(clipOrId) {
63
+ return this.sendCommand("delete_clip", {
64
+ clip_id: typeof clipOrId === "string" ? clipOrId : clipOrId.raw.id,
65
+ });
66
+ }
67
+ /**
68
+ * Delete a device identified by the index in the 'devices' list of current track
69
+ */
70
+ deleteDevice(index) {
71
+ return this.sendCommand("delete_device", [index]);
72
+ }
73
+ /**
74
+ * Given an absolute path to a valid audio file in a supported format, creates an audio clip that references the file in the clip slot.
75
+ * Throws an error if the clip slot doesn't belong to an audio track or if the track is frozen.
76
+ */
77
+ createAudioClip(filePath, position) {
78
+ return this.sendCommand("create_audio_clip", [filePath, position]);
56
79
  }
57
80
  }
58
81
  exports.Track = Track;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ableton-js",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Control Ableton Live from Node",
5
5
  "main": "index.js",
6
6
  "author": "Leo Bernard <admin@leolabs.org>",
@@ -19,10 +19,12 @@
19
19
  "ableton:copy-script": "set -- ~/Music/Ableton/User\\ Library/Remote\\ Scripts && mkdir -p \"$1\" && rm -rf \"$1/AbletonJS\" && cp -r \"$(pwd)/midi-script\" \"$1/AbletonJS\" && rm -rf \"$1/AbletonJS/_Framework\"",
20
20
  "ableton10:launch": "set -- /Applications/Ableton*10* && open \"$1\"",
21
21
  "ableton11:launch": "set -- /Applications/Ableton*11* && open \"$1\"",
22
+ "ableton12:launch": "set -- /Applications/Ableton*12* && open \"$1\"",
22
23
  "ableton:logs": "tail -n 50 -f ~/Library/Preferences/Ableton/*/Log.txt | grep --line-buffered -i -e AbletonJS",
23
24
  "ableton:kill": "pkill -KILL -f \"Ableton Live\"",
24
25
  "ableton10:start": "yarn ableton:kill; yarn ableton:clean && yarn ableton:copy-script && yarn ableton10:launch && yarn ableton:logs",
25
26
  "ableton11:start": "yarn ableton:kill; yarn ableton:clean && yarn ableton:copy-script && yarn ableton11:launch && yarn ableton:logs",
27
+ "ableton12:start": "yarn ableton:kill; yarn ableton:clean && yarn ableton:copy-script && yarn ableton11:launch && yarn ableton:logs",
26
28
  "prepublishOnly": "yarn build",
27
29
  "build:doc": "jsdoc2md --files src/**/*.ts --configure ./jsdoc2md.json > ./API.md",
28
30
  "version": "node hooks/prepublish.js && git add midi-script/version.py && auto-changelog -p -l 100 && git add CHANGELOG.md",
package/util/note.d.ts CHANGED
@@ -12,5 +12,16 @@ export interface Note {
12
12
  velocity: number;
13
13
  muted: boolean;
14
14
  }
15
+ export interface NoteExtended {
16
+ note_id: number;
17
+ duration: number;
18
+ mute: boolean;
19
+ pitch: number;
20
+ probability: number;
21
+ release_velocity: number;
22
+ start_time: number;
23
+ velocity: number;
24
+ velocity_deviation: number;
25
+ }
15
26
  export declare const tupleToNote: (tuple: NoteTuple) => Note;
16
27
  export declare const noteToTuple: (note: Note) => NoteTuple;