LA2L for developers

LA2L: Send to Live

This app sends sounds to Ableton Live with LA2L (Lloyd's App2Live), through the npm package @lloydstellar/la2l. Follow these notes whenever you build or change that part. Full docs: https://lloydstellar.nl/la2l/llms-full.txt

How it works

The user has LA2L, a Max for Live device, in Ableton Live. They click Pair app in it once and type the six-character code it shows into this app. After that the app sends WAV files with a description of each sound. When the user clicks Import in LA2L, each sound becomes a track in Live, warped to the tempo the app gave. No API key, no account.

Install

npm install @lloydstellar/la2l

The button

<la2l-send-button> asks for the code the first time, sends, and shows the status. Give it a function that returns the sounds at the moment of the click. In React with TypeScript:

import { useEffect, useRef } from 'react';
import '@lloydstellar/la2l/button';
import type { La2lSendButton } from '@lloydstellar/la2l/button';
import { wavFromAudioBuffer } from '@lloydstellar/la2l';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'la2l-send-button': React.DetailedHTMLProps<React.HTMLAttributes<La2lSendButton>, La2lSendButton> & { 'app-name'?: string; server?: string };
    }
  }
}

export function SendToLive({ bpm, render }: { bpm: number; render: () => Promise<AudioBuffer> }) {
  const ref = useRef<La2lSendButton>(null);
  useEffect(() => {
    if (!ref.current) return;
    ref.current.sounds = async () => ({
      title: `Beat at ${bpm} BPM`,
      bpm,
      sounds: [{ name: 'Beat', audio: wavFromAudioBuffer(await render()), kind: 'loop', role: 'drums', lengthBeats: 4 }],
    });
  }, [bpm, render]);
  return <la2l-send-button ref={ref} app-name="My app" />;
}

Set sounds through a ref as above; React 18 cannot pass a function to a custom element as a prop. In Vue, tell the compiler about the element: isCustomElement: (tag) => tag.startsWith('la2l-').

Without the button

import { LA2L, La2lError, wavFromAudioBuffer } from '@lloydstellar/la2l';

const la2l = new LA2L({ appName: 'My app' }); // Live shows "Paired with My app"
// Pair once, from a field with this line above it:
// "In Live, click Pair app in LA2L and type the code it shows."
await la2l.pair(code);
const delivery = await la2l.send({ title: 'Beat', bpm: 120, sounds: [{ name: 'Beat', audio: wav, kind: 'loop', role: 'drums' }] });
// show "Waiting for Import in Live", then:
await delivery.imported(); // show "In Live"

Every error is a La2lError with a code and a message to show as it is. On the code not_paired, show the code field again.

Rendering a loop

Render with an OfflineAudioContext exactly a whole number of beats: one bar of 4/4 is 4 * 60 / bpm seconds, so Math.round(48000 * 240 / bpm) frames at 48 kHz. Render a second more, and add what rings past the end of the bar to its start, so the loop has no click and no gap:

const rate = 48000;
const bar = Math.round(rate * 240 / bpm);
const ctx = new OfflineAudioContext(2, bar + rate, rate);
// schedule every hit at step * 60 / bpm / 4 seconds, connected to ctx.destination
const out = await ctx.startRendering();
const loop = new AudioBuffer({ numberOfChannels: 2, length: bar, sampleRate: rate });
for (let c = 0; c < 2; c++) {
  const from = out.getChannelData(c), to = loop.getChannelData(c);
  to.set(from.subarray(0, bar));
  for (let i = bar; i < from.length; i++) to[(i - bar) % bar] += from[i];
}
const wav = wavFromAudioBuffer(loop);

Rules

Testing it

Open https://lloydstellar.nl/la2l/receiver in another tab: it stands in for Live. Pair the app with the code it shows and send. The report must say "Loop, warped in Beats mode at 120 BPM, 4 beats" for a loop of one bar at 120 BPM, not "One-shot".