UNPKG

ableton-mcp-server-rag

Version:
479 lines 14.8 kB
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