Giter VIP home page Giter VIP logo

aalink's Introduction

aalink

aalink is a Python wrapper for Ableton Link built for interactive applications using asyncio event loops.

It provides a simple programming interface for writing concurrent Python code synchronized to a beat. The beat can optionally be time-aligned with other peers in an Ableton Link session.

Installation

aalink requires at least Python 3.8. It can be installed using pip:

pip3 install aalink

It may be required to install the latest version of MSVC Runtime libraries on Windows to use the binary wheels currently hosted on PyPI.

Usage

aalink uses asyncio. To connect to a Link session, create a Link object, passing the asyncio event loop to the constructor, and await for Link.sync() as follows:

import asyncio

from aalink import Link

async def main():
    loop = asyncio.get_running_loop()

    link = Link(120, loop)
    link.enabled = True

    while True:
        await link.sync(1)
        print('bang!')

asyncio.run(main())

Link.sync(n) returns a Future scheduled to be done when Link time reaches next n-th beat on the timeline.

In the above example, awaiting for link.sync(1) will pause and resume the main coroutine at beats 1, 2, 3, and so on.

Keep in mind that awaiting for sync(n) does not cause a coroutine to sleep for the given number of beats. Regardless of the moment when the coroutine is suspended, it will resume when the next closest n-th beat is reached on the shared Link timeline, e.g. awaiting for sync(2) at beat 11.5 will resume at beat 12.

Non-integral beat syncing is supported. For example:

await link.sync(1/2) # resumes at beats 0.5, 1, 1.5...
await link.sync(3/2) # resumes at beats 1.5, 3, 4.5...

Sync events can be scheduled with an offset (also expressed in beats) by passing an offset argument to sync(). Use this to add groove to the coroutine rhythm.

async def arpeggiate():
    for i in range(16):
        swing = 0.25 if i % 2 == 1 else 0

        await link.sync(1/2, offset=swing)
        print('###', i)

        await link.sync(1/2, offset=0)
        print('@@@', i)

Combine synced coroutines to run in series or concurrently:

import asyncio
from aalink import Link

async def main():
    loop = asyncio.get_running_loop()

    link = Link(120, loop)
    link.enabled = True

    async def sequence(name):
        for i in range(4):
            await link.sync(1)
            print('bang!', name)

    await sequence('a')
    await sequence('b')

    await asyncio.gather(sequence('c'), sequence('d'))

asyncio.run(main())

Limitations

aalink aims to be punctual, but it is not 100% accurate due to the processing delay in the internal scheduler and the uncertainty of event loop iterations timing.

For convenience, the numerical values of futures returned from sync() aren't equal to the exact beat time from the moment the futures are done. They correspond to the previously estimated resume times instead.

b = await link.sync(1) # b will be 1.0, returned at beat 1.00190
b = await link.sync(1) # b will be 2.0, returned at beat 2.00027
b = await link.sync(1) # b will be 3.0, returned at beat 3.00005

License

Copyright (c) 2023 Artem Popov <[email protected]>

aalink is licensed under the GNU General Public License (GPL) version 3. You can find the full text of the GPL license in the LICENSE file included in this repository.

aalink includes code from pybind11 and Ableton Link.

pybind11

Copyright (c) 2016 Wenzel Jakob <[email protected]>, All rights reserved.

pybind11 license

Ableton Link

Copyright 2016, Ableton AG, Berlin. All rights reserved.

Ableton Link license

aalink's People

Contributors

artfwo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

ceebeel itsbrex

aalink's Issues

Creating the link object *sometimes* changes the tempo.

As far as I can see, I have to provide a float tempo value when creating the Link object, e.g.

    link = Link(120, loop)
    link.enabled = True

Most of the time, this doesn't change the tempo in the linked Ableton, but adjusts to the tempo that is currently set in Ableton. But sometimes it is the other way around, and the tempo in Ableton is changed to the value provided in the constructor, roughly 10% of my trials. Is there a way of controlling this?

I tried using strange values like None, float('nan'), but that just yields an error message or non-working code, respectively. I also tried proving -1 or 0 as tempo, but that causes the same behavior as using 20 (sometimes setting Ableton's tempo to 20, or not changing the tempo most of the time).

What can I do?

Thanks for any help,
Best, Boris

Terminate called without an active exception

After migrating from pybind11 to nanobind, this can happen when exiting aalink scripts, as the scheduler thread isn't always joined properly:

Traceback (most recent call last):
  File "/home/art/aalink/build/./test.py", line 25, in <module>
    asyncio.run(main())
  File "/usr/lib64/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/asyncio/runners.py", line 123, in run
    raise KeyboardInterrupt()
KeyboardInterrupt
terminate called without an active exception
Aborted (core dumped)

Detaching the thread instead of joining might lead to more segfaults, so the best solution here is probably resolving any potential cyclic dependencies to ensure that Scheduler destructor is invoked when link is deleted.

See https://nanobind.readthedocs.io/en/latest/typeslots.html for some docs on resolving cyclic references.

To reproduce consisently, build in debug mode and increase the sleep timeout in scheduler thread to 100-500ms.

How to determine the current position in the bar?

Hi, thanks for providing this library, the example worked out of the box for me.

Currently, my code listens to MIDI clock messages generated by Ableton to "slave" my code to Ableton.
Now I would like to use Link to replace MIDI clock syncing, so I use link.sync(1/24) to get a clock-message-like signal, that works fine.

In addition, I would like to get the current beat count. I looked into the code and saw the functions to retrieve the attributes beat, time, phase, and so on. This enabled me to check, where on a given beat I am, e.g., 7.0 is precicely on the beat, 7.5 an 8th note after that, and so on.

But how do I determine, where in a given bar I am, e.g., that 7.0 is the 2 in a bar? It seems that beat and the return value of sync just counts from 0, but the relation to the beat is not fixed.

Thanks for any help,
Boris

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.