ableton-mcp-server-rag
Version:
Ableton Live MCP depend on Ableton JS
479 lines • 14.8 kB
JavaScript
import { Track } from 'ableton-js/ns/track.js';
import { ClipGettableProp, ClipSettableProp, ClipSlotGettableProperties, DeviceParameterGettableProperties, MixerDeviceGettableProp, SceneGettableProperties, SongSettableProp, SongViewSettableProp, TrackSettableProp } from '../types/zod-types.js';
import { z } from 'zod';
import { Clip } from 'ableton-js/ns/clip.js';
import { ableton } from '../ableton.js';
import { DeviceParameter } from 'ableton-js/ns/device-parameter.js';
import { Device, DeviceType } from 'ableton-js/ns/device.js';
import { MixerDevice } from 'ableton-js/ns/mixer-device.js';
import { CuePoint } from 'ableton-js/ns/cue-point.js';
import { Scene } from 'ableton-js/ns/scene.js';
import { ClipSlot } from 'ableton-js/ns/clip-slot.js';
import { BrowserItem } from 'ableton-js/ns/browser-item.js';
import { ErrorTypes } from '../mcp/error-handler.js';
import { getAllNotes } from './clip-utils.js';
// Import the browserItemCache
import { browserItemCache } from './browser-utils.js';
export function modifyObjProps(obj, property, zodSchema) {
const promiseArray = [];
const schema = zodSchema.shape;
for (const key of Object.keys(property)) {
const typedKey = key;
if (!Object.keys(schema).includes(typedKey)) {
continue;
}
const value = property[typedKey];
if (value !== undefined) {
promiseArray.push(obj.set(typedKey, value));
}
}
return Promise.allSettled(promiseArray);
}
export async function getObjProps(obj, scheme) {
const promiseArray = [];
const result = {};
const schema = scheme instanceof z.ZodObject ? scheme.shape : scheme;
for (const key of Object.keys(schema)) {
const typedKey = key;
promiseArray.push(obj.get(typedKey).then(value => result[typedKey] = value));
}
await Promise.allSettled(promiseArray);
return result;
}
/**
* modify track property
*/
export function modifyTrackProp(track, property) {
return modifyObjProps(track, property, TrackSettableProp);
}
export async function batchModifyTrackProp(tracks) {
const promises = tracks.map(async ({ track_id, property }) => {
const raw_track = getRawTrackById(track_id);
const track = new Track(ableton, raw_track);
const { mixer_device, ...restProperty } = property;
await modifyTrackProp(track, restProperty);
if (mixer_device) {
for (const key of Object.keys(mixer_device)) {
const typedKey = key;
const value = mixer_device[typedKey];
if (value === undefined || value === null) {
continue;
}
const mixerDevice = await track.get('mixer_device');
const parameter = await mixerDevice.get(typedKey);
if (parameter !== undefined && parameter !== null) {
if (parameter instanceof DeviceParameter) {
await modifyDeviceParameterVal(parameter, value);
}
else {
mixerDevice.set(typedKey, value);
}
}
}
}
});
await Promise.all(promises);
}
export async function getTrackProps(track, scheme) {
const props = await getObjProps(track, scheme);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (Array.isArray(value) && value.length > 0) {
if (value[0] instanceof Device) {
props[typedKey] = value.map(device => device.raw);
}
else if (value[0] instanceof Clip) {
props[typedKey] = value.map(clip => clip.raw);
}
}
else if (value instanceof MixerDevice) {
props[typedKey] = await getMixerDeviceProps(value);
}
}
return props;
}
/**
* modify clip property
*/
export function modifyClipProp(clip, property) {
return modifyObjProps(clip, property, ClipSettableProp);
}
export async function batchModifyClipProp(clips) {
const promises = clips.map(async ({ clip_id, property }) => {
const clip = await getClipById(clip_id);
await modifyClipProp(clip, property);
});
await Promise.all(promises);
}
export async function getClipProps(clip, scheme) {
let notes = [];
if (scheme.notes) {
delete scheme.notes;
notes = await getAllNotes(clip);
}
const res = await getObjProps(clip, scheme);
return {
...res,
notes
};
}
/**
* get song property
*/
export async function getSongProperties(song, scheme) {
const props = await getObjProps(song, scheme);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (Array.isArray(value) && value.length > 0) {
const firstValue = value[0];
if (firstValue instanceof Track) {
props[typedKey] = value.map(track => track.raw);
}
else if (firstValue instanceof CuePoint) {
props[typedKey] = value.map(cuePoint => cuePoint.raw);
}
else if (firstValue instanceof Scene) {
props[typedKey] = await Promise.all(value.map(scene => getSceneProps(scene)));
}
}
else if (value instanceof Track) {
props[typedKey] = value.raw;
}
}
return props;
}
export async function getSongViewProps(song, scheme) {
const props = await getObjProps(song.view, scheme);
if (scheme.detail_clip && (props.detail_clip === null || props.detail_clip === undefined)) {
throw new Error('please open piano roll');
}
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (value instanceof Track) {
props[typedKey] = value.raw;
}
else if (value instanceof Scene) {
props[typedKey] = value.raw;
}
else if (value instanceof ClipSlot) {
props[typedKey] = value.raw;
}
else if (value instanceof Clip) {
props[typedKey] = value.raw;
}
else if (value instanceof DeviceParameter) {
props[typedKey] = await getDeviceParamterProps(value);
}
}
return props;
}
export async function getDeviceParamterProps(parameter) {
const props = await getObjProps(parameter, DeviceParameterGettableProperties);
return {
...props,
id: parameter.raw.id
};
}
/**
* get device propertys
*/
export async function getDeviceProps(device, schema) {
const props = await getObjProps(device, schema);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (Array.isArray(value) && value.length > 0) {
const firstValue = value[0];
if (firstValue instanceof DeviceParameter) {
props[typedKey] = await Promise.all(value.map(parameter => {
return getDeviceParamterProps(parameter);
}));
}
}
}
return props;
}
/**
* modify song propertys
*/
export function modifySongProp(song, property) {
return modifyObjProps(song, property, SongSettableProp);
}
/**
* modify song view propertys
*/
export function modifySongViewProp(song, property) {
return modifyObjProps(song.view, property, SongViewSettableProp);
}
export async function modifyDeviceParameterVal(parameter, value) {
const isEnabled = await parameter.get('is_enabled');
const name = await parameter.get('name');
if (!isEnabled) {
throw new Error(`parameter ${name} is disabled`);
}
const max = await parameter.get('max');
const min = await parameter.get('min');
if (value > max || value < min) {
throw new Error(`parameter ${name} value ${value} is out of range (min: ${min}, max: ${max})`);
}
await parameter.set('value', value);
}
export async function getMixerDeviceProps(device) {
const props = await getObjProps(device, MixerDeviceGettableProp);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (value instanceof DeviceParameter) {
props[typedKey] = value.raw;
}
}
return props;
}
export async function getSceneProps(scene) {
const props = await getObjProps(scene, SceneGettableProperties);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (Array.isArray(value) && value.length > 0) {
const firstValue = value[0];
if (firstValue instanceof ClipSlot) {
props[typedKey] = value.map((clipSlot) => clipSlot.raw);
}
else {
props[typedKey] = value;
}
}
else {
props[typedKey] = value;
}
}
return props;
}
export async function getClipSlotProps(clipSlot) {
const props = await getObjProps(clipSlot, ClipSlotGettableProperties);
for (const key of Object.keys(props)) {
const typedKey = key;
const value = props[typedKey];
if (value instanceof Clip) {
props[typedKey] = getClipProps(value, ClipGettableProp);
}
else {
props[typedKey] = value;
}
}
return props;
}
/**
* Get raw Track object
* @param trackId Track ID
* @returns Raw Track object
*/
export function getRawTrackById(trackId) {
if (!trackId) {
throw ErrorTypes.INVALID_ARGUMENT('Track ID is required');
}
return {
id: trackId,
name: '',
color: 0,
color_index: 0,
is_foldable: false,
is_grouped: false,
mute: false,
solo: false,
};
}
/**
* Get Track object
* @param trackId Track ID
* @returns Track object
*/
export function getTrackById(trackId) {
const rawTrack = getRawTrackById(trackId);
return new Track(ableton, rawTrack);
}
/**
* Get raw Clip object
* @param clipId Clip ID
* @returns Raw Clip object
*/
export function getRawClipById(clipId) {
if (!clipId) {
throw ErrorTypes.INVALID_ARGUMENT('Clip ID is required');
}
return {
id: clipId,
name: '',
color: 0,
color_index: 0,
is_midi_clip: false,
is_audio_clip: false,
// Properties below are for the Clip object, not RawClip
// looping: false,
// length: 0,
// start_marker: 0,
// end_marker: 0,
// loop_start: 0,
// loop_end: 0,
// position: 0,
// gain: 0,
// is_playing: false,
// launch_mode: 0,
// launch_quantization: 0,
muted: false,
// pitch_coarse: 0,
// pitch_fine: 0,
// ram_mode: false,
// signature_denominator: 0,
// signature_numerator: 0,
// velocity_amount: 0,
// warp_mode: 0,
start_time: 0,
end_time: 0,
};
}
/**
* Get Clip object
* @param clipId Clip ID
* @returns Clip object
*/
export function getClipById(clipId) {
const rawClip = getRawClipById(clipId);
return new Clip(ableton, rawClip);
}
/**
* Get raw browser item object
* @param id Item ID
* @returns Raw browser item object
*/
export function getRawBrowserItemById(id) {
if (!id) {
throw ErrorTypes.INVALID_ARGUMENT('Browser item ID is required');
}
// Attempt to retrieve from cache first
const cachedItem = browserItemCache.get(id);
if (cachedItem) {
return cachedItem;
}
// Fallback to mock if not found in cache (should not happen if listResources was called)
return {
id: id,
children: [],
name: '',
is_loadable: false,
is_selected: false,
is_device: false,
is_folder: false,
source: '',
uri: '',
};
}
/**
* Get browser item object
* @param id Item ID
* @returns Browser item object
*/
export function getBrowserItemById(id) {
const rawBrowserItem = getRawBrowserItemById(id);
return new BrowserItem(ableton, rawBrowserItem);
}
/**
* Get raw device object
* @param id Device ID
* @returns Raw device object
*/
export function getRawDeviceById(id) {
if (!id) {
throw ErrorTypes.INVALID_ARGUMENT('Device ID is required');
}
return {
id: id,
name: '',
type: DeviceType.Undefined,
class_name: '',
};
}
/**
* Get raw device parameter object
* @param id Parameter ID
* @returns Raw device parameter object
*/
export function getRawDeviceParameterById(id) {
if (!id) {
throw ErrorTypes.INVALID_ARGUMENT('Device parameter ID is required');
}
return {
id: id,
name: '',
value: 0,
is_quantized: false,
};
}
/**
* Get device parameter object
* @param id Parameter ID
* @returns Device parameter object
*/
export function getDeviceParameterById(id) {
const rawDeviceParameter = getRawDeviceParameterById(id);
return new DeviceParameter(ableton, rawDeviceParameter);
}
/**
* Get device object
* @param id Device ID
* @returns Device object
*/
export function getDeviceById(id) {
const rawDevice = getRawDeviceById(id);
return new Device(ableton, rawDevice);
}
/**
* Check if a note is an extended note
* @param note Note object
* @returns Returns true if it's an extended note, false otherwise
*/
export function isNoteExtended(note) {
return note.note_id !== undefined;
}
/**
* Convert a NoteExtended object to a Note object
* @param note NoteExtended object
* @returns Note object
*/
export function NoteExtendedToNote(note) {
return {
pitch: note.pitch,
time: note.start_time,
duration: note.duration,
velocity: note.velocity,
muted: note.mute || false
};
}
export function NoteToNoteExtended(note) {
return {
note_id: -1, // No ID for new notes
pitch: note.pitch,
start_time: note.time,
duration: note.duration,
velocity: note.velocity,
mute: note.muted || false,
probability: 1.0,
release_velocity: 64,
velocity_deviation: 0
};
}
export function isNoteExtendedArray(notes) {
return notes.every(note => note.note_id !== undefined);
}
/**
* get application properties
*/
export async function getAppProperties(application, scheme) {
const props = await getObjProps(application, scheme);
// Application properties are simple values, no need for special processing like tracks or scenes
return props;
}
//# sourceMappingURL=obj-utils.js.map