Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype of Vortex -> Sardine #216

Merged
merged 30 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
83137fe
moving stuff from the vortex project
Bubobubobubobubo Apr 26, 2023
258702a
get import paths right
Bubobubobubobubo Apr 26, 2023
4fce20f
fixing architecture
Bubobubobubobubo Apr 26, 2023
a27b474
adding stream?
Bubobubobubobubo Apr 26, 2023
ef5a441
noodling around, connecting things
Bubobubobubobubo Apr 26, 2023
0abac1a
rewriting link clock
Bubobubobubobubo Apr 26, 2023
959c0c3
exploring
Bubobubobubobubo Apr 26, 2023
c9f3112
superficial support of vortex patterns
Bubobubobubobubo Apr 26, 2023
c082c76
slightly better tempo and osc handling
Bubobubobubobubo Apr 27, 2023
8a72d0c
whoops
Bubobubobubobubo Apr 27, 2023
5bfb803
completely clueless
Bubobubobubobubo Apr 27, 2023
b3267f3
import hush function
Bubobubobubobubo Apr 28, 2023
0623055
Modest progress on synchronisation
Bubobubobubobubo Apr 30, 2023
d793e2c
Remove silly timestamp
Bubobubobubobubo Apr 30, 2023
f487318
Correct round() call for SuperDirt messages
Bubobubobubobubo Apr 30, 2023
c4e346e
Let Sardine decide of the timestamp
Bubobubobubobubo Apr 30, 2023
56b9f13
Turn Vortex into an asyncrunner
Bubobubobubobubo May 1, 2023
da8246c
Vortex refactoring: new ideas after talking with @thegamecracks
Bubobubobubobubo May 1, 2023
98496c5
fixing some issues with internalclock
Bubobubobubobubo May 1, 2023
04e8304
rounding a number saves the day
Bubobubobubobubo May 1, 2023
b4ea032
remove testing patterns
Bubobubobubobubo May 1, 2023
d4a3325
adding some sugar
Bubobubobubobubo May 1, 2023
a08c31a
Temporary fix for overly fast patterns
Bubobubobubobubo May 1, 2023
ef6cd6b
Typo
Bubobubobubobubo May 1, 2023
e7c7cd7
fixing bug with querying rate
Bubobubobubobubo May 2, 2023
50cf577
Correct rate
Bubobubobubobubo May 2, 2023
27e762c
Adding some very basic documentation for TidalVortex
Bubobubobubobubo May 2, 2023
6e161b7
cosmetic
Bubobubobubobubo May 2, 2023
7984c73
correction about hush
Bubobubobubobubo May 2, 2023
8a3c1fb
Merge branch 'main' into vortex
Bubobubobubobubo May 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/sardine_doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
- [Random](pattern_languages/ziffers/random.md)
- [Lists](pattern_languages/ziffers/lists.md)
- [Euclidian](pattern_languages/ziffers/euclidian.md)
- [Vortex](pattern_languages/vortex.md)
- [Players](pattern_languages/vortex/players.md)
- [Clock](pattern_languages/vortex/clock.md)
- [Diving Deeper](diving_deeper.md)
- [FishBowl](diving_deeper/fishbowl.md)
- [Time-alignment](diving_deeper/nudging.md)
Expand Down
6 changes: 6 additions & 0 deletions docs/sardine_doc/src/pattern_languages/vortex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# TidalCycles (Vortex)

[Vortex](https://github.com/tidalcycles/vortex) is an experimental Python port of [TidalCycles](https://github.com/tidalcycles/Tidal) that was initially started by Alex McLean, Sylvain Le Beux, Silvani Damián and Raphaël Maurice Forment (
[conference paper](https://zenodo.org/record/6456380), [other conference paper](https://arxiv.org/pdf/2209.04289.pdf)). It has been developed as an independant program for quite a while but the development stalled in favour of the new Tidal 2.0 and [Tidal Strudel](https://strudel.tidalcycles.org/).

Vortex is now available for Sardine, and with it, the deep and robust TidalCycles patterning language. Vortex is transparently added to your session as long as your `superdirt_handler` is turned on. You have nothing to do or install.
14 changes: 14 additions & 0 deletions docs/sardine_doc/src/pattern_languages/vortex/clock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Clock

There is no difference between the **Sardine** Clock and the **Tidal** Clock. There are a few functions you can use if you are more familiar with the Tidal model:
- `clock.cps`: set the number of cycles per second for your patterns.
- `(135/60/4)` to set the tempo at 135 beats per minute.
- `clock.get_cps()`: get the `cps` from the current `clock.tempo`.

The `cps` attribute is both a setter and a getter, meaning that you can write:
```python
clock.cps = 0.5 # set the value
clock.cps # get the value
```

There is no difference between running a pattern using the Internal Clock or using the Link Clock.
49 changes: 49 additions & 0 deletions docs/sardine_doc/src/pattern_languages/vortex/players.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Players

## Syntactic Sugar

In accordance with the [TidalCycles](https://tidalcycles.org) model, there are a few players you can use to make your life easier: `d1`, `d2`, `d3`, `d4`, `d5`, `d6`, `d7`, `d8` and `d9`. The players are just thin wrappers and syntactic sugar for the `tidal` function:
```python
d1 = TidalD(name="d1", orbit_number=0)
d2 = TidalD(name="d2", orbit_number=1)
d3 = TidalD(name="d3", orbit_number=2)
d4 = TidalD(name="d4", orbit_number=3)
d5 = TidalD(name="d5", orbit_number=4)
d6 = TidalD(name="d6", orbit_number=5)
d7 = TidalD(name="d7", orbit_number=6)
d8 = TidalD(name="d8", orbit_number=7)
d9 = TidalD(name="d9", orbit_number=8)
```

Each player will be associated with an orbit number. This allows you to add effects to your players without having to think about the orbit you are currently targeting. Please note that the players are also slowed down a bit `.slow(4)` as patterns tend to be quite fast by default.

## The Tidal function

A Tidal pattern can be created using the `tidal` function. The `tidal` function takes two arguments:
- `name`: a name to give to the pattern.
- `pattern`: a pattern or any combination of patterns.

You can use it that way:

```python
tidal('my_pat', s("bd [hh hh:2] sn(2,3) <hh crow>")
.slow("<4!4 2 0.125>")
.striate("<2 8>")
.jux(rev))
```

## Stopping Tidal Patterns

You can stop all the Tidal patterns using the `hush()` function. You can also stop everything by running the base `silence()` or `panic()` functions. You can also stop individual Tidal patterns.

To do so, use the `hush()` function with a *string* or the player that holds the pattern you are willing to stop:

```python
# Using hush on a Tidal Player
d1 * s('bd sn')
hush(d1)

# Using hush with the Tidal function
tidal('dada', s('bd sn'))
hush('dada')
```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ classifiers = [

# Requirements: This is done differently by poetry!
dependencies = [
"parsimonious>=0.10.0",
"pygtail>=0.14.0",
"Flask-SocketIO>=5.3.2",
"Flask-Cors>=3.0",
Expand Down
49 changes: 49 additions & 0 deletions sardine/clock/internal_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,55 @@ def __init__(
self.tempo = tempo
self.beats_per_bar = bpb
self._internal_origin = 0.0
self._tidal_nudge: float = 0.0
self._framerate = 1 / 20
self._start: float = time.time()

#### VORTEX #############################################################

def get_cps(self) -> int | float:
"""Get the BPM in cycles per second (Tidal approach to time)"""
return self.tempo / self._beats_per_bar / 60.0

@property
def beats_per_cycle(self) -> int | float:
return self.beats_per_bar

@property
def cps(self) -> int | float:
"""Return the current cps"""
return self.get_cps()

@cps.setter
def cps(self, value: int | float) -> None:
self.tempo = value * self._beats_per_bar * 60.0

def _notify_tidal_streams(self):
"""
Notify Tidal Streams of the current passage of time.
"""

cycle_factor = self.beat_duration / self.beats_per_bar
time = self.shifted_time + self._tidal_nudge

cycle_from, cycle_to = (
time / cycle_factor,
(time / cycle_factor) + self._framerate,
)

time_on, time_off = (cycle_from * cycle_factor, cycle_to * cycle_factor)

try:
for sub in self.env._vortex_subscribers:
sub.notify_tick(
cycle=(cycle_from, cycle_to),
info=(time_on, time_off),
cycles_per_second=self.cps,
beats_per_cycle=self.beats_per_cycle,
now=time,
)
except Exception as e:
print(e)

#### GETTERS ############################################################

Expand Down
58 changes: 54 additions & 4 deletions sardine/clock/link_clock.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional, Union

import link
import math

from ..base import BaseClock, BaseThreadedLoopMixin

Expand All @@ -24,9 +25,57 @@ def __init__(
self._beats_per_bar: int = bpb
self._internal_origin: float = 0.0
self._internal_time: float = 0.0
self._last_capture: Optional[link.SessionState] = None
self._phase: float = 0.0
self._playing: bool = False
self._tempo: float = float(tempo)
self._tidal_nudge: int = 0
self._link_time: int = 0
self._beats_per_cycle: int = 4
self._framerate: float = 1 / 20

## VORTEX ################################################

def get_cps(self) -> int | float:
"""Get the BPM in cycles per second (Tidal approach to time)"""
return self.tempo / self._beats_per_bar / 60.0

@property
def cps(self) -> int | float:
"""Return the current cps"""
return self.get_cps()

@cps.setter
def cps(self, value: int | float) -> None:
self.tempo = value * self._beats_per_bar * 60.0

def _notify_tidal_streams(self):
"""
Notify Tidal Streams of the current passage of time.
"""

cycle_factor = self.beat_duration / self.beats_per_bar
# cycle_factor = self.beat_duration
time = self.shifted_time + self._tidal_nudge

cycle_from, cycle_to = (
(time / cycle_factor),
((time / cycle_factor) + self._framerate),
)

time_on, time_off = ((cycle_from * cycle_factor), (cycle_to * cycle_factor))

try:
for sub in self.env._vortex_subscribers:
sub.notify_tick(
cycle=(cycle_from, cycle_to),
info=(time_on, time_off),
cycles_per_second=self.cps,
beats_per_cycle=self._beats_per_cycle,
now=time,
)
except Exception as e:
print(e)

## GETTERS ################################################

Expand Down Expand Up @@ -88,13 +137,14 @@ def tempo(self, new_tempo: float) -> None:

def _capture_link_info(self):
s: link.SessionState = self._link.captureSessionState()
link_time: int = self._link.clock().micros()
beat: float = s.beatAtTime(link_time, self.beats_per_bar)
phase: float = s.phaseAtTime(link_time, self.beats_per_bar)
self._last_capture = s
self._link_time: int = self._link.clock().micros()
beat: float = s.beatAtTime(self._link_time, self.beats_per_bar)
phase: float = s.phaseAtTime(self._link_time, self.beats_per_bar)
playing: bool = s.isPlaying()
tempo: float = s.tempo()

self._internal_time = link_time / 1_000_000
self._internal_time = self._link_time / 1_000_000
self._beat = int(beat)
self._beat_duration = 60 / tempo
# Sardine phase is typically defined from 0.0 to the beat duration.
Expand Down
1 change: 1 addition & 0 deletions sardine/fish_bowl.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(
self._handlers: dict[BaseHandler, None] = {}
self._alive = asyncio.Event()
self._resumed = asyncio.Event()
self._vortex_subscribers: list = []

self._event_hooks: dict[
Optional[str], dict[HookProtocol, None]
Expand Down
14 changes: 8 additions & 6 deletions sardine/handlers/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ def defaults(self):
def ziffers_parser(self):
return self._ziffers_parser

def call_timed_with_nudge(self, deadline, method, **kwargs):
"""Applying nudge to call_timed method"""
return self.call_timed(deadline + self.nudge, method, **kwargs)

@ziffers_parser.setter
def ziffers_parser(self, parser):
self._ziffers_parser = parser
Expand All @@ -104,6 +100,10 @@ def nudge(self):
def nudge(self, nudge):
self._nudge = nudge

def call_timed_with_nudge(self, deadline, method, **kwargs):
"""Applying nudge to call_timed method"""
return self.call_timed(deadline + self.nudge, method, **kwargs)

def __repr__(self) -> str:
return f"<{type(self).__name__} port={self._port_name!r} nudge={self._nudge}>"

Expand Down Expand Up @@ -564,10 +564,12 @@ def send(
deadline = self.env.clock.shifted_time
for message in self.pattern_reduce(pattern, iterator, divisor, rate):
if message["program_change"] is not None:
self.send_program(program=message["program_change"], channel=message["channel"])
self.send_program(
program=message["program_change"], channel=message["channel"]
)
if message["note"] is None:
continue
for k in ("note", "velocity", "channel"):
message[k] = int(message[k])
message.pop('program_change')
message.pop("program_change")
self.call_timed_with_nudge(deadline, self.send_midi_note, **message)
11 changes: 7 additions & 4 deletions sardine/handlers/superdirt.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,14 @@ def __send(self, address: str, message: list) -> None:
)
osc_send(bun, self._name)

def __send_timed_message(self, address: str, message: list):
def _send_timed_message(
self, address: str, message: list, timestamp: Optional[int | float] = None
) -> None:
"""Build and send OSC bundles"""
timestamp = time.time() + self._ahead_amount if timestamp is None else timestamp
msg = oscbuildparse.OSCMessage(address, None, message)
bun = oscbuildparse.OSCBundle(
oscbuildparse.unixtime2timetag(time.time() + self._ahead_amount),
oscbuildparse.unixtime2timetag(timestamp),
[msg],
)
osc_send(bun, self._name)
Expand All @@ -96,7 +99,7 @@ def _send(self, address, message):
self.__send(address=address, message=message)

def _dirt_play(self, message: list):
self.__send_timed_message(address="/dirt/play", message=message)
self._send_timed_message(address="/dirt/play", message=message)

def _dirt_panic(self):
self._dirt_play(message=["sound", "superpanic"])
Expand Down Expand Up @@ -157,7 +160,7 @@ def send(

pattern["sound"] = sound
pattern["orbit"] = orbit
pattern["cps"] = round(self.env.clock.phase, 4)
pattern["cps"] = round(self.env.clock.phase, 1)
pattern["cycle"] = (
self.env.clock.bar * self.env.clock.beats_per_bar
) + self.env.clock.beat
Expand Down
Loading