Skip to content

Commit

Permalink
Merge pull request #414 from shorepine/alles
Browse files Browse the repository at this point in the history
Ensuring Tulip works with Alles latest
  • Loading branch information
bwhitman authored Nov 4, 2024
2 parents f15c34a + 9a1d8d5 commit 274f835
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 144 deletions.
7 changes: 3 additions & 4 deletions docs/tulip_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ tulip.defer(hello, 123, 1500) # will be called 1500ms later

Tulip comes with the AMY synthesizer, a very full featured 120-oscillator synth that supports FM, PCM, subtractive and additive synthesis, partial synthesis, filters, and much more. See the [AMY documentation](https://github.com/shorepine/amy/blob/main/README.md) for more information, Tulip's version of AMY comes with stereo sound, chorus and reverb. It includes a "small" version of the PCM patch set (29 patches) alongside all the Juno-6 and DX7 patches. It also has support for loading WAVE files in Tulip as samples.

Once connected to Wi-Fi, Tulip can also control or respond to an [Alles mesh.](https://github.com/shorepine/alles/blob/main/README.md) Alles is a wrapper around AMY that lets you control the synthesizer over Wi-Fi to remote speakers, or other computers or Tulips. Connect any number of Alles speakers to the wifi to have instant surround sound! See the Alles [getting started tutorial](https://github.com/shorepine/alles/blob/main/getting-started.md) for more information and for more music examples.
Once connected to Wi-Fi, Tulip can also control an [Alles mesh.](https://github.com/shorepine/alles/blob/main/README.md) Alles is a wrapper around AMY that lets you control the synthesizer over Wi-Fi to remote speakers, or other computers or Tulips. Connect any number of Alles speakers to the wifi to have instant surround sound! See the Alles [getting started tutorial](https://github.com/shorepine/alles/blob/main/getting-started.md) for more information and for more music examples.

Tulip can also route AMY signals to CV outputs connected over Tulip CC's I2C port. You will need one or two [Mabee DACs](https://www.makerfabs.com/mabee-dac-gp8413.html) or similar GP8413 setup. This lets you send accurate LFOs over CV to modular or other older analog synthesizers.

Expand All @@ -428,15 +428,14 @@ amy.send(voices='0', pan=0) # set to the right channel
amy.send(voices='0', pan=1) # set to the left channel

# start mesh mode (control multiple speakers over wifi)
alles.mesh() # after turning on wifi
alles.mesh(local_node=False) # call it this way if you don't want Tulip to also be an Alles node
# once mesh mode is set, you can't go back to local mode until you restart Tulip.
alles.mesh() # after turning on wifi. tulip itself will stop playing AMY messages.
alles.mesh(local_ip='192.168.50.4') # useful for setting a network on Tulip Desktop

alles.map() # returns booted Alles synths on the mesh

amy.send(voices='0', load_patch=101, note=50, vel=1) # all Alles speakers in a mesh will respond
amy.send(voices='0', load_patch=101, note=50, vel=1, client=2) # just a certain client
alles.local() # turns off mesh mode and goes back to local mode
```

To load your own WAVE files as samples, use `amy.load_sample`:
Expand Down
14 changes: 1 addition & 13 deletions tulip/esp32s3/multicast.c
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,6 @@ void mcast_listen_task(void *pvParameters) {
.tv_usec = 0,
};

//ipv4_quartet = esp_ip4_addr4(&wifi_manager_ip4);

//esp_netif_ip_info_t ip_info = { 0 };
//esp_netif_get_ip_info(WIFI_IF_STA, &ip_info);
//fprintf(stderr, "setting IPV4 quartet to %d %d\n", ip4_addr3(&ip_info.ip), ip4_addr4(&ip_info.ip));
//ipv4_quartet = ip4_addr4(&ip_info.ip);

int16_t full_message_length;
printf("Network listening running on core %d quartet %d\n",xPortGetCoreID(), ipv4_quartet);
while (1) {
Expand Down Expand Up @@ -196,7 +189,6 @@ void mcast_listen_task(void *pvParameters) {
else if (s > 0) {
if (FD_ISSET(sock, &rfds)) {
// Incoming UDP packet received
// Turn on the CPU monitor to see how long parsing takes
struct sockaddr_in6 raddr; // Large enough for both IPv4 or IPv6
socklen_t socklen = sizeof(raddr);
full_message_length = recvfrom(sock, udp_message, sizeof(udp_message)-1, 0,
Expand Down Expand Up @@ -224,11 +216,7 @@ void mcast_listen_task(void *pvParameters) {
}
}
}
// Do a ping every so often
int64_t sysclock = esp_timer_get_time() / 1000;
if(sysclock > (last_ping_time+PING_TIME_MS)) {
ping(sysclock);
}
// Tulip does not ping other nodes.
}

ESP_LOGE(TAG, "Shutting down socket and restarting...");
Expand Down
131 changes: 36 additions & 95 deletions tulip/shared/alles.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,14 @@ char * alles_local_ip;
//extern uint8_t battery_mask;
extern uint8_t ipv4_quartet;
extern char githash[8];
int16_t client_id;
int32_t clocks[255];
int32_t ping_times[255];
uint8_t alive = 1;

//extern int32_t computed_delta ; // can be negative no prob, but usually host is larger # than client
//extern uint8_t computed_delta_set ; // have we set a delta yet?
extern int32_t last_ping_time;

uint8_t mesh_flag = 0;

amy_err_t sync_init() {
client_id = -1; // for now
for(uint8_t i=0;i<255;i++) { clocks[i] = 0; ping_times[i] = 0; }
return AMY_OK;
}
Expand Down Expand Up @@ -266,34 +263,39 @@ void esp_parse_task() {
#include "esp_wifi.h"
#endif

void alles_init_multicast(uint8_t local_node) {
mesh_local_playback = local_node;
#ifdef ESP_PLATFORM
fprintf(stderr, "creating socket\n");
create_multicast_ipv4_socket();
fprintf(stderr, "power save off\n");
esp_wifi_set_ps(WIFI_PS_NONE);
fprintf(stderr, "creating mcast task\n");
xTaskCreatePinnedToCore(&mcast_listen_task, ALLES_RECEIVE_TASK_NAME, ALLES_RECEIVE_TASK_STACK_SIZE, NULL, ALLES_RECEIVE_TASK_PRIORITY, &alles_receive_handle, ALLES_RECEIVE_TASK_COREID);
fprintf(stderr, "creating parse task\n");
xTaskCreatePinnedToCore(&esp_parse_task, ALLES_PARSE_TASK_NAME, ALLES_PARSE_TASK_STACK_SIZE, NULL, ALLES_PARSE_TASK_PRIORITY, &alles_parse_handle, ALLES_PARSE_TASK_COREID);
#else
if(alles_local_ip[0]==0) {
get_first_ip_address(alles_local_ip);
}
fprintf(stderr, "creating socket to ip %s\n", alles_local_ip);
create_multicast_ipv4_socket();
fprintf(stderr, "creating mcast task\n");
#ifndef ESP_PLATFORM
pthread_t thread_id;
pthread_create(&thread_id, NULL, mcast_listen_task, NULL);
#endif

void alles_init_multicast() {
if(!mesh_flag) {
#ifdef ESP_PLATFORM
fprintf(stderr, "creating socket\n");
create_multicast_ipv4_socket();
fprintf(stderr, "power save off\n");
esp_wifi_set_ps(WIFI_PS_NONE);
fprintf(stderr, "creating mcast task\n");
xTaskCreatePinnedToCore(&mcast_listen_task, ALLES_RECEIVE_TASK_NAME, ALLES_RECEIVE_TASK_STACK_SIZE, NULL, ALLES_RECEIVE_TASK_PRIORITY, &alles_receive_handle, ALLES_RECEIVE_TASK_COREID);
fprintf(stderr, "creating parse task\n");
xTaskCreatePinnedToCore(&esp_parse_task, ALLES_PARSE_TASK_NAME, ALLES_PARSE_TASK_STACK_SIZE, NULL, ALLES_PARSE_TASK_PRIORITY, &alles_parse_handle, ALLES_PARSE_TASK_COREID);
#else
if(alles_local_ip[0]==0) {
get_first_ip_address(alles_local_ip);
}
fprintf(stderr, "creating mcast task\n");
pthread_create(&thread_id, NULL, mcast_listen_task, NULL);
#endif
mesh_flag = 1;
}
}








void alles_parse_message(char *message, uint16_t length) {
uint8_t mode = 0;
int16_t client = -1;
Expand All @@ -320,7 +322,7 @@ void alles_parse_message(char *message, uint16_t length) {
if(mode=='U') sync = atol(message + start);
if(mode=='W') external_map[e.osc] = atoi(message+start);
if(sync_response) if(mode=='r') ipv4=atoi(message + start);
if(sync_response) if(mode=='i') sync_index = atoi(message + start);
if(mode=='i') sync_index = atoi(message + start);
mode = b;
start = c + 1;
}
Expand All @@ -332,6 +334,8 @@ void alles_parse_message(char *message, uint16_t length) {
//fprintf(stderr, "sync response message was %s\n", message);
update_map(client, ipv4, sync);
length = 0; // don't need to do the rest
} else {
// Note, we DO NOT do anything with computed_delta in Tulip. Tulip cannot receive alles mesh messages for playback.
}
// Only do this if we got some data
if(length >0) {
Expand All @@ -340,105 +344,42 @@ void alles_parse_message(char *message, uint16_t length) {
if(sync >= 0 && sync_index >= 0) {
handle_sync(sync, sync_index);
} else {
// Assume it's for me
uint8_t for_me = 1;
// But wait, they specified, so don't assume
if(client >= 0) {
for_me = 0;
if(client <= 255) {
// If they gave an individual client ID check that it exists
if(alive>0) { // alive may get to 0 in a bad situation
if(client >= alive) {
client = client % alive;
}
}
}
// It's actually precisely for me
if(client == client_id) for_me = 1;
if(client > 255) {
// It's a group message, see if i'm in the group
if(client_id % (client-255) == 0) for_me = 1;
}
// Don't parse events other than sync messages if i'm in mesh mode.
if(!mesh_flag) {
amy_add_event(e);
}
//fprintf(stderr, "LOG: AMY message for time %lld received at time %lld (latency %lld ms) for_me %d\n", e.time, amy_sysclock(), amy_sysclock()-e.time, for_me);
if(for_me && mesh_local_playback) amy_add_event(e);
}
}
}

void update_map(uint8_t client, uint8_t ipv4, int32_t time) {
// I'm called when I get a sync response or a regular ping packet
// I update a map of booted devices.

//fprintf(stderr,"[%d %d] Got a sync response client %d ipv4 %d time %ld\n", ipv4_quartet, client_id, client , ipv4, time);
//fprintf(stderr,"Got a sync response client %d ipv4 %d time %"PRIu32"\n", client , ipv4, time);
clocks[ipv4] = time;
int32_t my_sysclock = amy_sysclock();
ping_times[ipv4] = my_sysclock;

// Now I basically see what index I would be in the list of booted synths (clocks[i] > 0)
// And I set my client_id to that index
uint8_t last_alive = alive;
uint8_t my_new_client_id = 255;
alive = 0;
for(uint8_t i=0;i<255;i++) {
if(clocks[i] > 0) {
if(my_sysclock < (ping_times[i] + (PING_TIME_MS * 2))) { // alive
//printf("[%d %d] Checking my time %lld against ipv4 %d's of %lld, client_id now %d ping_time[%d] = %lld\n",
// ipv4_quartet, client_id, my_sysclock, i, clocks[i], my_new_client_id, i, ping_times[i]);
alive++;
} else {
//printf("[ipv4 %d client %d] clock %d is dead, ping time was %lld time now is %lld.\n", ipv4_quartet, client_id, i, ping_times[i], my_sysclock);
clocks[i] = 0;
ping_times[i] = 0;
}
// If this is not me....
if(i != ipv4_quartet) {
// predicted time is what we think the alive node should be at by now
int32_t predicted_time = (my_sysclock - ping_times[i]) + clocks[i];
if(my_sysclock >= predicted_time) my_new_client_id--;
} else {
my_new_client_id--;
}
} else {
// if clocks[] is 0, no need to check
my_new_client_id--;
}
}
if(client_id != my_new_client_id || last_alive != alive) {
fprintf(stderr,"[%d] my client_id is now %d. %d alive\n", ipv4_quartet, my_new_client_id, alive);
client_id = my_new_client_id;
if(last_alive != alive) {
fprintf(stderr,"[alles] %d alive\n", alive);
}
}

void handle_sync(int32_t time, int8_t index) {
// I am called when I get an s message, which comes along with host time and index
int32_t sysclock = amy_sysclock();
char message[100];
// Before I send, i want to update the map locally
//fprintf(stderr, "handle_sync %d %d\n", client_id, ipv4_quartet);
update_map(client_id, ipv4_quartet, sysclock);
// Send back sync message with my time and received sync index and my client id & battery status (if any)
sprintf(message, "_U%" PRIi32 "i%dg%dr%dy%dZ", sysclock, index, client_id, ipv4_quartet, 0);
//mcast_send(message, strlen(message));
// Update computed delta (i could average these out, but I don't think that'll help too much)
//int64_t old_cd = computed_delta;

// TODO, fix this up for the new style
//computed_delta = time - sysclock;
//computed_delta_set = 1;


//if(old_cd != computed_delta) printf("Changed computed_delta from %lld to %lld on sync\n", old_cd, computed_delta);
// Tulip doesn't respond to alles sync messages ever, as it does not boot as an alles node (it can control them only)
}

void ping(int32_t sysclock) {
char message[100];
//printf("[%d %d] pinging with %lld\n", ipv4_quartet, client_id, sysclock);
sprintf(message, "_U%" PRIi32 "i-1g%dr%dy%dZ", sysclock, client_id, ipv4_quartet, 0);
//fprintf(stderr, "ping %d %d\n", client_id, ipv4_quartet);

update_map(client_id, ipv4_quartet, sysclock);
//mcast_send(message, strlen(message));
last_ping_time = sysclock;
}

3 changes: 2 additions & 1 deletion tulip/shared/alles.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ void ping(int32_t sysclock);
extern void mcast_send(char*, uint16_t len);
extern void create_multicast_ipv4_socket();

void alles_init_multicast(uint8_t local_node);
void alles_init_multicast();
void alles_stop_multicast();

void esp_show_debug(uint8_t type);
void alles_send_message(char * message, uint16_t len);
Expand Down
19 changes: 9 additions & 10 deletions tulip/shared/desktop/multicast.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ void create_multicast_ipv4_socket(void) {
err = socket_add_ipv4_multicast_group();
if(err) exit(EXIT_FAILURE);

printf("Multicast IF is %s. Client tag (not ID) is %d. Listening on %s:%d\n", alles_local_ip, ipv4_quartet, MULTICAST_IPV4_ADDR, UDP_PORT);
fprintf(stderr,"Multicast IF is %s. Listening on %s:%d\n", alles_local_ip, MULTICAST_IPV4_ADDR, UDP_PORT);
}


Expand All @@ -159,17 +159,20 @@ void mcast_send(char * message, uint16_t len) {
if (err < 0) {
fprintf(stderr, "IPV4 sendto failed. errno: %d", errno);
}

}



// called from pthread
void *mcast_listen_task(void *vargp) {
struct timeval tv = {
.tv_sec = 1,
.tv_usec = 0,
.tv_sec = 0,
.tv_usec = 10*1000,
};

create_multicast_ipv4_socket();

int16_t full_message_length;
while (1) {
// set destination multicast addresses for sending from these sockets
Expand Down Expand Up @@ -207,7 +210,7 @@ void *mcast_listen_task(void *vargp) {
}
udp_message[full_message_length] = 0;
uint16_t start = 0;
// Break the packet up into messages (delimited by \n.)
// Break the packet up into messages (delimited by Z.)
for(uint16_t i=0;i<full_message_length;i++) {
if(udp_message[i] == 'Z') {
udp_message[i] = 0;
Expand All @@ -220,12 +223,8 @@ void *mcast_listen_task(void *vargp) {
}
}
}
// Do a ping every so often
int64_t sysclock = amy_sysclock();
if(sysclock > (last_ping_time+PING_TIME_MS)) {
ping(sysclock);
}
usleep(THREAD_USLEEP);
// Tulip doesn't ping, and we don't need real time UDP listening on Tulip. This is just for receiving pings for the map.
usleep(1000*10);
}

fprintf(stderr, "Shutting down socket and restarting...\n");
Expand Down
6 changes: 3 additions & 3 deletions tulip/shared/modtulip.c
Original file line number Diff line number Diff line change
Expand Up @@ -909,15 +909,15 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_alles_send_obj, 1, 2, tulip_all
extern char * alles_local_ip;
STATIC mp_obj_t tulip_multicast_start(size_t n_args, const mp_obj_t *args) {
const char * local_ip = mp_obj_str_get_str(args[0]);
uint8_t local_node = mp_obj_get_int(args[1]);
if(strlen(local_ip)>2) {
strcpy(alles_local_ip, local_ip);
}
alles_init_multicast(local_node);
alles_init_multicast();
return mp_const_none;

}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_multicast_start_obj, 2, 2, tulip_multicast_start);
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_multicast_start_obj, 1, 1, tulip_multicast_start);



extern uint8_t alive;
Expand Down
1 change: 0 additions & 1 deletion tulip/shared/py/_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,5 @@
gc.collect()
# Override amy's send to work with tulip
amy.override_send = lambda x: tulip.alles_send(x, alles.mesh_flag)
alles.local() # start in local mode
midi.setup()

Loading

0 comments on commit 274f835

Please sign in to comment.