From 6153800893f60c5a7f77f442924caf11d8f4b172 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Wed, 2 Oct 2024 14:34:41 -0400 Subject: [PATCH 1/8] updating tulip desktop to use AMY audio in --- amy | 2 +- tulip/linux/main.c | 9 +++++++-- tulip/macos/main.c | 9 +++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/amy b/amy index 826e3b0d..2b7c8d8c 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 826e3b0d46ff39ab532054a6d595e8e8e604cbed +Subproject commit 2b7c8d8cfcf8f5f03cab6d941578257c587a2259 diff --git a/tulip/linux/main.c b/tulip/linux/main.c index 97343a6a..7b93175c 100644 --- a/tulip/linux/main.c +++ b/tulip/linux/main.c @@ -481,7 +481,8 @@ STATIC void sys_set_excecutable(char *argv0) { #endif -extern int16_t amy_device_id; +extern int16_t amy_playback_device_id; +extern int16_t amy_capture_device_id; extern void setup_lvgl(); @@ -851,7 +852,10 @@ int main(int argc, char **argv) { switch(opt) { case 'd': - amy_device_id = atoi(optarg); + amy_playback_device_id = atoi(optarg); + break; + case 'c': + amy_capture_device_id = atoi(optarg); break; case 'l': amy_print_devices(); @@ -860,6 +864,7 @@ int main(int argc, char **argv) { case 'h': fprintf(stderr,"usage: tulip\n"); fprintf(stderr,"\t[-d sound device id, use -l to list, default, autodetect]\n"); + fprintf(stderr,"\t[-c capture sound device id, use -l to list, default, autodetect]\n"); fprintf(stderr,"\t[-l list all sound devices and exit]\n"); fprintf(stderr,"\t[-h show this help and exit]\n"); exit(0); diff --git a/tulip/macos/main.c b/tulip/macos/main.c index f8e01360..8e6032e6 100644 --- a/tulip/macos/main.c +++ b/tulip/macos/main.c @@ -540,7 +540,8 @@ char * get_tulip_home_path() { } -extern int16_t amy_device_id; +extern int16_t amy_playback_device_id; +extern int16_t amy_capture_device_id; /* @@ -914,7 +915,10 @@ int main(int argc, char **argv) { switch(opt) { case 'd': - amy_device_id = atoi(optarg); + amy_playback_device_id = atoi(optarg); + break; + case 'c': + amy_capture_device_id = atoi(optarg); break; case 'l': amy_print_devices(); @@ -923,6 +927,7 @@ int main(int argc, char **argv) { case 'h': fprintf(stderr,"usage: tulip\n"); fprintf(stderr,"\t[-d sound device id, use -l to list, default, autodetect]\n"); + fprintf(stderr,"\t[-c capture sound device id, use -l to list, default, autodetect]\n"); fprintf(stderr,"\t[-l list all sound devices and exit]\n"); fprintf(stderr,"\t[-h show this help and exit]\n"); exit(0); From 2a5e42689f3edae1062aea68f9be44dff2d804e1 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Thu, 3 Oct 2024 10:03:48 -0400 Subject: [PATCH 2/8] fixes for amychip/audioin/memorypcm on tulip --- amy | 2 +- tulip/esp32s3/boards/manifest.py | 1 + tulip/esp32s3/esp32_common.cmake | 2 +- tulip/linux/variants/manifest.py | 1 + tulip/macos/variants/manifest.py | 1 + tulip/shared/alles.c | 51 ++-- tulip/shared/memorypcm.c | 214 ------------- tulip/shared/modtulip.c | 33 -- tulip/shared/py/wave.py | 509 ------------------------------- tulip/shared/tulip.mk | 2 +- 10 files changed, 31 insertions(+), 785 deletions(-) delete mode 100644 tulip/shared/memorypcm.c delete mode 100644 tulip/shared/py/wave.py diff --git a/amy b/amy index 2b7c8d8c..066f3ce4 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 2b7c8d8cfcf8f5f03cab6d941578257c587a2259 +Subproject commit 066f3ce439ea9cc54441bb76c17bd9bf7496b5ff diff --git a/tulip/esp32s3/boards/manifest.py b/tulip/esp32s3/boards/manifest.py index 9c1d3a32..34b37fcd 100644 --- a/tulip/esp32s3/boards/manifest.py +++ b/tulip/esp32s3/boards/manifest.py @@ -22,4 +22,5 @@ freeze("$(PORT_DIR)/../shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") #freeze("$(MPY_DIR)/lib/micropython-lib/micropython/utarfile", "utarfile.py") diff --git a/tulip/esp32s3/esp32_common.cmake b/tulip/esp32s3/esp32_common.cmake index 67fe189e..aba8cbc1 100644 --- a/tulip/esp32s3/esp32_common.cmake +++ b/tulip/esp32s3/esp32_common.cmake @@ -143,7 +143,6 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${TULIP_SHARED_DIR}/lvgl_u8g2.c ${TULIP_SHARED_DIR}/u8fontdata.c ${TULIP_SHARED_DIR}/u8g2_fonts.c - ${TULIP_SHARED_DIR}/memorypcm.c ${AMY_DIR}/src/dsps_biquad_f32_ae32.S ${AMY_DIR}/src/algorithms.c ${AMY_DIR}/src/custom.c @@ -154,6 +153,7 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${AMY_DIR}/src/envelope.c ${AMY_DIR}/src/filters.c ${AMY_DIR}/src/oscillators.c + ${AMY_DIR}/src/transfer.c ${AMY_DIR}/src/partials.c ${AMY_DIR}/src/pcm.c ${AMY_DIR}/src/log2_exp2.c diff --git a/tulip/linux/variants/manifest.py b/tulip/linux/variants/manifest.py index 57e26da3..acccc016 100644 --- a/tulip/linux/variants/manifest.py +++ b/tulip/linux/variants/manifest.py @@ -2,4 +2,5 @@ freeze("$(MPY_DIR)/../tulip/shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") diff --git a/tulip/macos/variants/manifest.py b/tulip/macos/variants/manifest.py index 57e26da3..acccc016 100644 --- a/tulip/macos/variants/manifest.py +++ b/tulip/macos/variants/manifest.py @@ -2,4 +2,5 @@ freeze("$(MPY_DIR)/../tulip/shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") diff --git a/tulip/shared/alles.c b/tulip/shared/alles.c index 5aacd7a9..8ce9d34c 100644 --- a/tulip/shared/alles.c +++ b/tulip/shared/alles.c @@ -90,9 +90,9 @@ void esp_fill_audio_buffer_task() { // We turn off writing to i2s on r10 when doing on chip debugging because of pins #ifndef TULIP_R10_DEBUG size_t written = 0; - i2s_channel_write(tx_handle, block, AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS, &written, portMAX_DELAY); - if(written != AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS) { - fprintf(stderr,"i2s underrun: %d vs %d\n", written, AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS); + i2s_channel_write(tx_handle, block, AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS, &written, portMAX_DELAY); + if(written != AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS) { + fprintf(stderr,"i2s underrun: %d vs %d\n", written, AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS); } #endif @@ -184,8 +184,6 @@ amy_err_t setup_i2s(void) { #endif -extern struct custom_oscillator memorypcm; -extern void memorypcm_init(); #ifdef ESP_PLATFORM #include "driver/i2c.h" @@ -226,7 +224,6 @@ void run_alles() { esp_amy_init(); - amy_set_custom(&memorypcm); amy_reset_oscs(); // Schedule a "turning on" sound bleep(); @@ -243,8 +240,6 @@ void * alles_start(void *vargs) { alles_local_ip = malloc(256); alles_local_ip[0] = 0; unix_amy_init(); - amy_set_custom(&memorypcm); - memorypcm_init(); amy_reset_oscs(); // Schedule a "turning on" sound // We don't do this by default on tulip desktop as all threads start at once and it makes the bleep sound bad @@ -307,26 +302,30 @@ void alles_parse_message(char *message, uint16_t length) { uint8_t ipv4 = 0; uint16_t start = 0; uint16_t c = 0; - //char local_message[MAX_RECEIVE_LEN]; - //strncpy(local_message, message, length); + uint8_t sync_response = 0; + // Parse the AMY stuff out of the message first struct event e = amy_parse_message(message); - uint8_t sync_response = 0; - //fprintf(stderr, "message is %s len is %d\n", message, length); - // Then pull out any alles-specific modes in this message - while(c < length+1) { - uint8_t b = message[c]; - if(b == '_' && c==0) sync_response = 1; - if( ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) || b == 0) { // new mode or end - if(mode=='g') client = atoi(message + start); - 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); - mode = b; - start = c + 1; - } - c++; + if(e.status == TRANSFER_DATA) { + // transfer data already dealt with. we skip this followon check. + length = 0; + } else { + //fprintf(stderr, "message is %s len is %d\n", message, length); + // Then pull out any alles-specific modes in this message + while(c < length+1) { + uint8_t b = message[c]; + if(b == '_' && c==0) sync_response = 1; + if( ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) || b == 0) { // new mode or end + if(mode=='g') client = atoi(message + start); + 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); + mode = b; + start = c + 1; + } + c++; + } } if(sync_response) { // If this is a sync response, let's update our local map of who is booted diff --git a/tulip/shared/memorypcm.c b/tulip/shared/memorypcm.c deleted file mode 100644 index 053b5487..00000000 --- a/tulip/shared/memorypcm.c +++ /dev/null @@ -1,214 +0,0 @@ -// custom_memory_pcm.c -// AMY custom oscillator that reads from tulip RAM -#include "amy.h" -#include "py/runtime.h" -#include "polyfills.h" - -// Tulip-side functions -// A map of parameters for a in memory PCM sample. You can set SR per sample, and looping -typedef struct { - int16_t * sample_ram; - uint32_t length; - uint32_t loopstart; - uint32_t loopend; - uint8_t midinote; - uint32_t samplerate; - float log2sr; -} memorypcm_map_t; - -// list of pointers,alloced as needed -#define MAX_MEMORYPCM_PATCHES 32 - -memorypcm_map_t *memorypcm_map[MAX_MEMORYPCM_PATCHES]; - - -uint8_t osc_patch_exists(uint16_t osc) { - if(AMY_IS_UNSET(synth[osc].patch)) return 0; - if(memorypcm_map[synth[osc].patch] != NULL) return 1; - return 0; -} - -// load mono samples (let python parse wave files) into patch # -// set loopstart, loopend, midinote, samplerate (and log2sr) -int8_t memorypcm_load(mp_obj_t bytes, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend) { - // find the next free patch # - int8_t patch = -1; - for(uint8_t i=0;isamplerate = samplerate; - memorypcm_map[patch]->log2sr = log2f((float)samplerate / ZERO_LOGFREQ_IN_HZ); - memorypcm_map[patch]->midinote = midinote; - memorypcm_map[patch]->loopstart = loopstart; - // Grab the samples and len from bytes - mp_buffer_info_t bufinfo; - mp_get_buffer(bytes, &bufinfo, MP_BUFFER_READ); - memorypcm_map[patch]->length = bufinfo.len / 2; - // Alloc the buffer and copy to Tulip RAM. The python alloc'd one will go away in gc - //fprintf(stderr, "samplerate %d midinote %d, loopstart %d loopend %d length %lu\n", samplerate, midinote, loopstart, loopend, bufinfo.len/2); - memorypcm_map[patch]->sample_ram = malloc_caps(bufinfo.len, MALLOC_CAP_SPIRAM); - if(memorypcm_map[patch]->sample_ram == NULL) { - free_caps(memorypcm_map[patch]); - return -1; // no ram for sample - } - - if(loopend == 0) { // loop whole sample - memorypcm_map[patch]->loopend = memorypcm_map[patch]->length-1; - } else { - memorypcm_map[patch]->loopend = loopend; - } - memcpy(memorypcm_map[patch]->sample_ram, bufinfo.buf, bufinfo.len); - return patch; // patch number -} - -void memorypcm_unload_patch(uint8_t patch) { - if(memorypcm_map[patch] == NULL) return; - free_caps(memorypcm_map[patch]->sample_ram); - free_caps(memorypcm_map[patch]); - memorypcm_map[patch] = NULL; -} - -//free all patches -void memorypcm_unload() { - for(uint8_t i=0;imidinote. - synth[osc].logfreq_coefs[COEF_CONST] = memorypcm_map[synth[osc].patch]->log2sr - logfreq_for_midi_note(memorypcm_map[synth[osc].patch]->midinote); - } - synth[osc].phase = 0; // s16.15 index into the table; as if a PHASOR into a 16 bit sample table. - // Special case: We use the msynth feedback flag to indicate note-off for looping PCM. As a result, it's explicitly NOT set in amy:hold_and_modify for PCM voices. Set it here. - msynth[osc].feedback = synth[osc].feedback; - } -} - - -void memorypcm_note_off(uint16_t osc) { - if(osc_patch_exists(osc)) { - if(msynth[osc].feedback == 0) { - // Non-looping note: Set phase to the end to cause immediate stop. - synth[osc].phase = F2P(memorypcm_map[synth[osc].patch]->length / (float)(1 << PCM_INDEX_BITS)); - } else { - // Looping is requested, disable future looping, sample will play through to end. - // (sending a second note-off will stop it immediately). - msynth[osc].feedback = 0; - } - } -} - -void memorypcm_mod_trigger(uint16_t osc) { - memorypcm_note_on(osc, 0); -} - -SAMPLE memorypcm_render(SAMPLE* buf, uint16_t osc) { - if(osc_patch_exists(osc)) { - // Patches can be > 32768 samples long. - // We need s16.15 fixed-point indexing. - memorypcm_map_t* patch = memorypcm_map[synth[osc].patch]; - float logfreq = msynth[osc].logfreq; - // If osc[midi_note] is unset, apply patch's default here. - if (AMY_IS_UNSET(synth[osc].midi_note)) logfreq += logfreq_for_midi_note(patch->midinote); - float playback_freq = freq_of_logfreq(logfreq); // PCM_SAMPLE_RATE modified by - - SAMPLE max_value = 0; - SAMPLE amp = F2S(msynth[osc].amp); - PHASOR step = F2P((playback_freq / (float)AMY_SAMPLE_RATE) / (float)(1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = patch->sample_ram; - uint32_t base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - //fprintf(stderr, "render_pcm: time=%.3f patch=%d base_index=%d length=%d loopstart=%d loopend=%d fb=%f is_unset_note_off %d\n", total_samples / (float)AMY_SAMPLE_RATE, synth[osc].patch, base_index, patch->length, patch->loopstart, patch->loopend, msynth[osc].feedback, AMY_IS_UNSET(synth[osc].note_off_clock)); - for(uint16_t i=0; i < AMY_BLOCK_SIZE; i++) { - SAMPLE frac = S_FRAC_OF_P(synth[osc].phase, PCM_INDEX_BITS); - LUTSAMPLE b = table[base_index]; - LUTSAMPLE c = b; - if (base_index < patch->length) c = table[base_index + 1]; - SAMPLE sample = L2S(b) + MUL0_SS(L2S(c - b), frac); - synth[osc].phase = P_WRAPPED_SUM(synth[osc].phase, step); - base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - if(base_index >= patch->length) { // end - synth[osc].status = STATUS_OFF;// is this right? - sample = 0; - } else { - if(msynth[osc].feedback > 0) { // still looping. The feedback flag is cleared by pcm_note_off. - if(base_index >= patch->loopend) { // loopend - // back to loopstart - int32_t loop_len = patch->loopend - patch->loopstart; - synth[osc].phase -= F2P(loop_len / (float)(1 << PCM_INDEX_BITS)); - base_index -= loop_len; - } - } - } - SAMPLE value = buf[i] + MUL4_SS(amp, sample); - buf[i] = value; - if (value < 0) value = -value; - if (value > max_value) max_value = value; - } - //printf("render_pcm: osc %d patch %d len %d base_ix %d phase %f step %f tablestep %f amp %f\n", - // osc, synth[osc].patch, patch->length, base_index, P2F(synth[osc].phase), P2F(step), (1 << PCM_INDEX_BITS) * P2F(step), S2F(msynth[osc].amp)); - return max_value; - } - return 0; // no patch here -} - -SAMPLE memorypcm_compute_mod(uint16_t osc) { - if(osc_patch_exists(osc)) { - float mod_sr = (float)AMY_SAMPLE_RATE / (float)AMY_BLOCK_SIZE; - memorypcm_map_t* patch = memorypcm_map[synth[osc].patch]; - PHASOR step = F2P(((float)patch->samplerate / mod_sr) / (1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = patch->sample_ram; - uint32_t base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - SAMPLE sample; - if(base_index >= patch->length) { // end - synth[osc].status = STATUS_OFF;// is this right? - sample = 0; - } else { - sample = L2S(table[base_index]); - synth[osc].phase = P_WRAPPED_SUM(synth[osc].phase, step); - } - return MUL4_SS(F2S(msynth[osc].amp), sample); - } - return 0; // no patch -} - -struct custom_oscillator memorypcm = { - memorypcm_init, - memorypcm_note_on, - memorypcm_note_off, - memorypcm_mod_trigger, - memorypcm_render, - memorypcm_compute_mod -}; - - diff --git a/tulip/shared/modtulip.c b/tulip/shared/modtulip.c index 2a364ed5..b9e13814 100644 --- a/tulip/shared/modtulip.c +++ b/tulip/shared/modtulip.c @@ -221,37 +221,6 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_bg_blit_obj, 6, 7, tulip_bg_bli -extern int8_t memorypcm_load(mp_obj_t bytes, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend); -extern void memorypcm_unload_patch(uint8_t patch); -extern void memorypcm_unload(); - -STATIC mp_obj_t tulip_call_load_sample(size_t n_args, const mp_obj_t *args) { - uint32_t samplerate = 44100; - uint8_t midinote = 60; - uint32_t loopstart = 0; - uint32_t loopend = 0; - if(n_args > 1) samplerate = mp_obj_get_int(args[1]); - if(n_args > 2) midinote = mp_obj_get_int(args[2]); - if(n_args > 3) loopstart = mp_obj_get_int(args[3]); - if(n_args > 4) loopend = mp_obj_get_int(args[4]); - int8_t patch = memorypcm_load(args[0], samplerate, midinote, loopstart, loopend); - return mp_obj_new_int(patch); -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_call_load_sample_obj, 1, 5, tulip_call_load_sample); - -STATIC mp_obj_t tulip_unload_patch(size_t n_args, const mp_obj_t *args) { - if(n_args > 0) { - memorypcm_unload_patch(mp_obj_get_int(args[0])); - return mp_const_none; - } - memorypcm_unload(); - return mp_const_none; -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_unload_patch_obj, 0, 1, tulip_unload_patch); - - // tulip.bg_png(bytes, x,y) // tulip.bg_png(filename, x,y) STATIC mp_obj_t tulip_bg_png(size_t n_args, const mp_obj_t *args) { @@ -1393,8 +1362,6 @@ STATIC const mp_rom_map_elem_t tulip_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_bg_circle), MP_ROM_PTR(&tulip_bg_circle_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_bezier), MP_ROM_PTR(&tulip_bg_bezier_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_line), MP_ROM_PTR(&tulip_bg_line_obj) }, - { MP_ROM_QSTR(MP_QSTR_call_load_sample), MP_ROM_PTR(&tulip_call_load_sample_obj) }, - { MP_ROM_QSTR(MP_QSTR_unload_patch), MP_ROM_PTR(&tulip_unload_patch_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_roundrect), MP_ROM_PTR(&tulip_bg_roundrect_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_triangle), MP_ROM_PTR(&tulip_bg_triangle_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_fill), MP_ROM_PTR(&tulip_bg_fill_obj) }, diff --git a/tulip/shared/py/wave.py b/tulip/shared/py/wave.py deleted file mode 100644 index 6f0ff844..00000000 --- a/tulip/shared/py/wave.py +++ /dev/null @@ -1,509 +0,0 @@ -"""Stuff to parse WAVE files. - -Usage. - -Reading WAVE files: - f = wave.open(file, 'r') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods read(), seek(), and close(). -When the setpos() and rewind() methods are not used, the seek() -method is not necessary. - -This returns an instance of a class with the following public methods: - getnchannels() -- returns number of audio channels (1 for - mono, 2 for stereo) - getsampwidth() -- returns sample width in bytes - getframerate() -- returns sampling frequency - getnframes() -- returns number of audio frames - getcomptype() -- returns compression type ('NONE' for linear samples) - getcompname() -- returns human-readable version of - compression type ('not compressed' linear samples) - getparams() -- returns a namedtuple consisting of all of the - above in the above order - getmarkers() -- returns None (for compatibility with the - aifc module) - getmark(id) -- raises an error since the mark does not - exist (for compatibility with the aifc module) - readframes(n) -- returns at most n frames of audio - rewind() -- rewind to the beginning of the audio stream - setpos(pos) -- seek to the specified position - tell() -- return the current position - close() -- close the instance (make it unusable) -The position returned by tell() and the position given to setpos() -are compatible and have nothing to do with the actual position in the -file. -The close() method is called automatically when the class instance -is destroyed. - -Writing WAVE files: - f = wave.open(file, 'w') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods write(), tell(), seek(), and -close(). - -This returns an instance of a class with the following public methods: - setnchannels(n) -- set the number of channels - setsampwidth(n) -- set the sample width - setframerate(n) -- set the frame rate - setnframes(n) -- set the number of frames - setcomptype(type, name) - -- set the compression type and the - human-readable compression type - setparams(tuple) - -- set all parameters at once - tell() -- return current position in output file - writeframesraw(data) - -- write audio frames without pathing up the - file header - writeframes(data) - -- write audio frames and patch up the file header - close() -- patch up the file header and close the - output file -You should set the parameters before the first writeframesraw or -writeframes. The total number of frames does not need to be set, -but when it is set to the correct value, the header does not have to -be patched up. -It is best to first set all parameters, perhaps possibly the -compression type, and then write audio frames using writeframesraw. -When all frames have been written, either call writeframes('') or -close() to patch up the sizes in the header. -The close() method is called automatically when the class instance -is destroyed. -""" - -import builtins - -__all__ = ["open", "openfp", "Error"] - -class Error(Exception): - pass - -WAVE_FORMAT_PCM = 0x0001 - -_array_fmts = None, 'b', 'h', None, 'i' - -#import audioop -import struct -import sys -from chunk import Chunk -from collections import namedtuple - -_wave_params = namedtuple('_wave_params', - 'nchannels sampwidth framerate nframes comptype compname') - -class Wave_read: - """Variables used in this class: - - These variables are available to the user though appropriate - methods of this class: - _file -- the open file with methods read(), close(), and seek() - set through the __init__() method - _nchannels -- the number of audio channels - available through the getnchannels() method - _nframes -- the number of audio frames - available through the getnframes() method - _sampwidth -- the number of bytes per audio sample - available through the getsampwidth() method - _framerate -- the sampling frequency - available through the getframerate() method - _comptype -- the AIFF-C compression type ('NONE' if AIFF) - available through the getcomptype() method - _compname -- the human-readable AIFF-C compression type - available through the getcomptype() method - _soundpos -- the position in the audio stream - available through the tell() method, set through the - setpos() method - - These variables are used internally only: - _fmt_chunk_read -- 1 iff the FMT chunk has been read - _data_seek_needed -- 1 iff positioned correctly in audio - file for readframes() - _data_chunk -- instantiation of a chunk class for the DATA chunk - _framesize -- size of one frame in the file - """ - - def initfp(self, file): - self._convert = None - self._soundpos = 0 - self._file = Chunk(file, bigendian = 0) - if self._file.getname() != b'RIFF': - raise Error('file does not start with RIFF id') - if self._file.read(4) != b'WAVE': - raise Error('not a WAVE file') - self._fmt_chunk_read = 0 - self._data_chunk = None - while 1: - self._data_seek_needed = 1 - try: - chunk = Chunk(self._file, bigendian = 0) - except EOFError: - break - chunkname = chunk.getname() - if chunkname == b'fmt ': - self._read_fmt_chunk(chunk) - self._fmt_chunk_read = 1 - elif chunkname == b'data': - if not self._fmt_chunk_read: - raise Error('data chunk before fmt chunk') - self._data_chunk = chunk - self._nframes = chunk.chunksize // self._framesize - self._data_seek_needed = 0 - break - elif chunkname == b'smpl': - self._read_smpl_chunk(chunk) - chunk.skip() - if not self._fmt_chunk_read or not self._data_chunk: - raise Error('fmt chunk and/or data chunk missing') - - def __init__(self, f): - self._i_opened_the_file = None - if isinstance(f, str): - f = builtins.open(f, 'rb') - self._i_opened_the_file = f - # else, assume it is an open file object already - try: - self.initfp(f) - except: - if self._i_opened_the_file: - f.close() - raise - - def __del__(self): - self.close() - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - # - # User visible methods. - # - def getfp(self): - return self._file - - def rewind(self): - self._data_seek_needed = 1 - self._soundpos = 0 - - def close(self): - if self._i_opened_the_file: - self._i_opened_the_file.close() - self._i_opened_the_file = None - self._file = None - - def tell(self): - return self._soundpos - - def getnchannels(self): - return self._nchannels - - def getnframes(self): - return self._nframes - - def getsampwidth(self): - return self._sampwidth - - def getframerate(self): - return self._framerate - - def getcomptype(self): - return self._comptype - - def getcompname(self): - return self._compname - - def getparams(self): - return _wave_params(self.getnchannels(), self.getsampwidth(), - self.getframerate(), self.getnframes(), - self.getcomptype(), self.getcompname()) - - def getmarkers(self): - return None - - def getmark(self, id): - raise Error('no marks') - - def setpos(self, pos): - if pos < 0 or pos > self._nframes: - raise Error('position not in range') - self._soundpos = pos - self._data_seek_needed = 1 - - def readframes(self, nframes): - if self._data_seek_needed: - self._data_chunk.seek(0, 0) - pos = self._soundpos * self._framesize - if pos: - self._data_chunk.seek(pos, 0) - self._data_seek_needed = 0 - if nframes == 0: - return b'' - data = self._data_chunk.read(nframes * self._framesize) - if self._sampwidth != 1 and sys.byteorder == 'big': - data = audioop.byteswap(data, self._sampwidth) - if self._convert and data: - data = self._convert(data) - self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth) - return data - - # - # Internal methods. - # - def _read_smpl_chunk(self, chunk): - _, _,_, self._midinote, _, _, _, self._loops, _ = struct.unpack('0): # read first loop - _, _, self._loopstart, self._loopend = struct.unpack(' 4: - raise Error('bad sample width') - self._sampwidth = sampwidth - - def getsampwidth(self): - if not self._sampwidth: - raise Error('sample width not set') - return self._sampwidth - - def setframerate(self, framerate): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - if framerate <= 0: - raise Error('bad frame rate') - self._framerate = int(round(framerate)) - - def getframerate(self): - if not self._framerate: - raise Error('frame rate not set') - return self._framerate - - def setnframes(self, nframes): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - self._nframes = nframes - - def getnframes(self): - return self._nframeswritten - - def setcomptype(self, comptype, compname): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - if comptype not in ('NONE',): - raise Error('unsupported compression type') - self._comptype = comptype - self._compname = compname - - def getcomptype(self): - return self._comptype - - def getcompname(self): - return self._compname - - def setparams(self, params): - nchannels, sampwidth, framerate, nframes, comptype, compname = params - if self._datawritten: - raise Error('cannot change parameters after starting to write') - self.setnchannels(nchannels) - self.setsampwidth(sampwidth) - self.setframerate(framerate) - self.setnframes(nframes) - self.setcomptype(comptype, compname) - - def getparams(self): - if not self._nchannels or not self._sampwidth or not self._framerate: - raise Error('not all parameters set') - return _wave_params(self._nchannels, self._sampwidth, self._framerate, - self._nframes, self._comptype, self._compname) - - def setmark(self, id, pos, name): - raise Error('setmark() not supported') - - def getmark(self, id): - raise Error('no marks') - - def getmarkers(self): - return None - - def tell(self): - return self._nframeswritten - - def writeframesraw(self, data): - if not isinstance(data, (bytes, bytearray)): - data = memoryview(data).cast('B') - self._ensure_header_written(len(data)) - nframes = len(data) // (self._sampwidth * self._nchannels) - if self._convert: - data = self._convert(data) - if self._sampwidth != 1 and sys.byteorder == 'big': - data = audioop.byteswap(data, self._sampwidth) - self._file.write(data) - self._datawritten += len(data) - self._nframeswritten = self._nframeswritten + nframes - - def writeframes(self, data): - self.writeframesraw(data) - if self._datalength != self._datawritten: - self._patchheader() - - def close(self): - if self._file: - try: - self._ensure_header_written(0) - if self._datalength != self._datawritten: - self._patchheader() - self._file.flush() - finally: - self._file = None - if self._i_opened_the_file: - self._i_opened_the_file.close() - self._i_opened_the_file = None - - # - # Internal methods. - # - - def _ensure_header_written(self, datasize): - if not self._headerwritten: - if not self._nchannels: - raise Error('# channels not specified') - if not self._sampwidth: - raise Error('sample width not specified') - if not self._framerate: - raise Error('sampling rate not specified') - self._write_header(datasize) - - def _write_header(self, initlength): - assert not self._headerwritten - self._file.write(b'RIFF') - if not self._nframes: - self._nframes = initlength // (self._nchannels * self._sampwidth) - self._datalength = self._nframes * self._nchannels * self._sampwidth - try: - self._form_length_pos = self._file.tell() - except (AttributeError, OSError): - self._form_length_pos = None - self._file.write(struct.pack(' Date: Wed, 2 Oct 2024 14:34:41 -0400 Subject: [PATCH 3/8] updating tulip desktop to use AMY audio in --- amy | 2 +- tulip/linux/main.c | 11 ++++++++--- tulip/macos/main.c | 9 +++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/amy b/amy index 826e3b0d..2b7c8d8c 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 826e3b0d46ff39ab532054a6d595e8e8e604cbed +Subproject commit 2b7c8d8cfcf8f5f03cab6d941578257c587a2259 diff --git a/tulip/linux/main.c b/tulip/linux/main.c index b8abd091..05184269 100644 --- a/tulip/linux/main.c +++ b/tulip/linux/main.c @@ -480,10 +480,11 @@ static void sys_set_excecutable(char *argv0) { #endif +extern int16_t amy_playback_device_id; +extern int16_t amy_capture_device_id; +extern void setup_lvgl(); -extern int16_t amy_device_id; - /* MP_NOINLINE int main_(int argc, char **argv); @@ -875,7 +876,10 @@ int main(int argc, char **argv) { switch(opt) { case 'd': - amy_device_id = atoi(optarg); + amy_playback_device_id = atoi(optarg); + break; + case 'c': + amy_capture_device_id = atoi(optarg); break; case 'l': amy_print_devices(); @@ -884,6 +888,7 @@ int main(int argc, char **argv) { case 'h': fprintf(stderr,"usage: tulip\n"); fprintf(stderr,"\t[-d sound device id, use -l to list, default, autodetect]\n"); + fprintf(stderr,"\t[-c capture sound device id, use -l to list, default, autodetect]\n"); fprintf(stderr,"\t[-l list all sound devices and exit]\n"); fprintf(stderr,"\t[-h show this help and exit]\n"); exit(0); diff --git a/tulip/macos/main.c b/tulip/macos/main.c index 343f05f9..40e00ad7 100644 --- a/tulip/macos/main.c +++ b/tulip/macos/main.c @@ -514,7 +514,8 @@ char * get_tulip_home_path() { } -extern int16_t amy_device_id; +extern int16_t amy_playback_device_id; +extern int16_t amy_capture_device_id; /* MP_NOINLINE int main_(int argc, char **argv); @@ -907,7 +908,10 @@ int main(int argc, char **argv) { switch(opt) { case 'd': - amy_device_id = atoi(optarg); + amy_playback_device_id = atoi(optarg); + break; + case 'c': + amy_capture_device_id = atoi(optarg); break; case 'l': amy_print_devices(); @@ -916,6 +920,7 @@ int main(int argc, char **argv) { case 'h': fprintf(stderr,"usage: tulip\n"); fprintf(stderr,"\t[-d sound device id, use -l to list, default, autodetect]\n"); + fprintf(stderr,"\t[-c capture sound device id, use -l to list, default, autodetect]\n"); fprintf(stderr,"\t[-l list all sound devices and exit]\n"); fprintf(stderr,"\t[-h show this help and exit]\n"); exit(0); From fecd7d23d48158443da363a54e3778a6399016d0 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Thu, 3 Oct 2024 10:03:48 -0400 Subject: [PATCH 4/8] fixes for amychip/audioin/memorypcm on tulip --- amy | 2 +- tulip/esp32s3/boards/manifest.py | 1 + tulip/esp32s3/esp32_common.cmake | 2 +- tulip/linux/variants/manifest.py | 1 + tulip/macos/variants/manifest.py | 1 + tulip/shared/alles.c | 51 ++-- tulip/shared/memorypcm.c | 214 ------------- tulip/shared/modtulip.c | 33 -- tulip/shared/py/wave.py | 509 ------------------------------- tulip/shared/tulip.mk | 2 +- 10 files changed, 31 insertions(+), 785 deletions(-) delete mode 100644 tulip/shared/memorypcm.c delete mode 100644 tulip/shared/py/wave.py diff --git a/amy b/amy index 2b7c8d8c..066f3ce4 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 2b7c8d8cfcf8f5f03cab6d941578257c587a2259 +Subproject commit 066f3ce439ea9cc54441bb76c17bd9bf7496b5ff diff --git a/tulip/esp32s3/boards/manifest.py b/tulip/esp32s3/boards/manifest.py index 9c1d3a32..34b37fcd 100644 --- a/tulip/esp32s3/boards/manifest.py +++ b/tulip/esp32s3/boards/manifest.py @@ -22,4 +22,5 @@ freeze("$(PORT_DIR)/../shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") #freeze("$(MPY_DIR)/lib/micropython-lib/micropython/utarfile", "utarfile.py") diff --git a/tulip/esp32s3/esp32_common.cmake b/tulip/esp32s3/esp32_common.cmake index 83bd8754..3bdce0b3 100644 --- a/tulip/esp32s3/esp32_common.cmake +++ b/tulip/esp32s3/esp32_common.cmake @@ -183,7 +183,6 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${TULIP_SHARED_DIR}/lvgl_u8g2.c ${TULIP_SHARED_DIR}/u8fontdata.c ${TULIP_SHARED_DIR}/u8g2_fonts.c - ${TULIP_SHARED_DIR}/memorypcm.c ${AMY_DIR}/src/dsps_biquad_f32_ae32.S ${AMY_DIR}/src/algorithms.c ${AMY_DIR}/src/custom.c @@ -194,6 +193,7 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${AMY_DIR}/src/envelope.c ${AMY_DIR}/src/filters.c ${AMY_DIR}/src/oscillators.c + ${AMY_DIR}/src/transfer.c ${AMY_DIR}/src/partials.c ${AMY_DIR}/src/pcm.c ${AMY_DIR}/src/log2_exp2.c diff --git a/tulip/linux/variants/manifest.py b/tulip/linux/variants/manifest.py index 57e26da3..acccc016 100644 --- a/tulip/linux/variants/manifest.py +++ b/tulip/linux/variants/manifest.py @@ -2,4 +2,5 @@ freeze("$(MPY_DIR)/../tulip/shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") diff --git a/tulip/macos/variants/manifest.py b/tulip/macos/variants/manifest.py index 57e26da3..acccc016 100644 --- a/tulip/macos/variants/manifest.py +++ b/tulip/macos/variants/manifest.py @@ -2,4 +2,5 @@ freeze("$(MPY_DIR)/../tulip/shared/py") freeze("$(MPY_DIR)/../amy", "amy.py") freeze("$(MPY_DIR)/../amy", "juno.py") +freeze("$(MPY_DIR)/../amy", "amy_wave.py") diff --git a/tulip/shared/alles.c b/tulip/shared/alles.c index 5aacd7a9..8ce9d34c 100644 --- a/tulip/shared/alles.c +++ b/tulip/shared/alles.c @@ -90,9 +90,9 @@ void esp_fill_audio_buffer_task() { // We turn off writing to i2s on r10 when doing on chip debugging because of pins #ifndef TULIP_R10_DEBUG size_t written = 0; - i2s_channel_write(tx_handle, block, AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS, &written, portMAX_DELAY); - if(written != AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS) { - fprintf(stderr,"i2s underrun: %d vs %d\n", written, AMY_BLOCK_SIZE * BYTES_PER_SAMPLE * AMY_NCHANS); + i2s_channel_write(tx_handle, block, AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS, &written, portMAX_DELAY); + if(written != AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS) { + fprintf(stderr,"i2s underrun: %d vs %d\n", written, AMY_BLOCK_SIZE * AMY_BYTES_PER_SAMPLE * AMY_NCHANS); } #endif @@ -184,8 +184,6 @@ amy_err_t setup_i2s(void) { #endif -extern struct custom_oscillator memorypcm; -extern void memorypcm_init(); #ifdef ESP_PLATFORM #include "driver/i2c.h" @@ -226,7 +224,6 @@ void run_alles() { esp_amy_init(); - amy_set_custom(&memorypcm); amy_reset_oscs(); // Schedule a "turning on" sound bleep(); @@ -243,8 +240,6 @@ void * alles_start(void *vargs) { alles_local_ip = malloc(256); alles_local_ip[0] = 0; unix_amy_init(); - amy_set_custom(&memorypcm); - memorypcm_init(); amy_reset_oscs(); // Schedule a "turning on" sound // We don't do this by default on tulip desktop as all threads start at once and it makes the bleep sound bad @@ -307,26 +302,30 @@ void alles_parse_message(char *message, uint16_t length) { uint8_t ipv4 = 0; uint16_t start = 0; uint16_t c = 0; - //char local_message[MAX_RECEIVE_LEN]; - //strncpy(local_message, message, length); + uint8_t sync_response = 0; + // Parse the AMY stuff out of the message first struct event e = amy_parse_message(message); - uint8_t sync_response = 0; - //fprintf(stderr, "message is %s len is %d\n", message, length); - // Then pull out any alles-specific modes in this message - while(c < length+1) { - uint8_t b = message[c]; - if(b == '_' && c==0) sync_response = 1; - if( ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) || b == 0) { // new mode or end - if(mode=='g') client = atoi(message + start); - 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); - mode = b; - start = c + 1; - } - c++; + if(e.status == TRANSFER_DATA) { + // transfer data already dealt with. we skip this followon check. + length = 0; + } else { + //fprintf(stderr, "message is %s len is %d\n", message, length); + // Then pull out any alles-specific modes in this message + while(c < length+1) { + uint8_t b = message[c]; + if(b == '_' && c==0) sync_response = 1; + if( ((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')) || b == 0) { // new mode or end + if(mode=='g') client = atoi(message + start); + 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); + mode = b; + start = c + 1; + } + c++; + } } if(sync_response) { // If this is a sync response, let's update our local map of who is booted diff --git a/tulip/shared/memorypcm.c b/tulip/shared/memorypcm.c deleted file mode 100644 index 053b5487..00000000 --- a/tulip/shared/memorypcm.c +++ /dev/null @@ -1,214 +0,0 @@ -// custom_memory_pcm.c -// AMY custom oscillator that reads from tulip RAM -#include "amy.h" -#include "py/runtime.h" -#include "polyfills.h" - -// Tulip-side functions -// A map of parameters for a in memory PCM sample. You can set SR per sample, and looping -typedef struct { - int16_t * sample_ram; - uint32_t length; - uint32_t loopstart; - uint32_t loopend; - uint8_t midinote; - uint32_t samplerate; - float log2sr; -} memorypcm_map_t; - -// list of pointers,alloced as needed -#define MAX_MEMORYPCM_PATCHES 32 - -memorypcm_map_t *memorypcm_map[MAX_MEMORYPCM_PATCHES]; - - -uint8_t osc_patch_exists(uint16_t osc) { - if(AMY_IS_UNSET(synth[osc].patch)) return 0; - if(memorypcm_map[synth[osc].patch] != NULL) return 1; - return 0; -} - -// load mono samples (let python parse wave files) into patch # -// set loopstart, loopend, midinote, samplerate (and log2sr) -int8_t memorypcm_load(mp_obj_t bytes, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend) { - // find the next free patch # - int8_t patch = -1; - for(uint8_t i=0;isamplerate = samplerate; - memorypcm_map[patch]->log2sr = log2f((float)samplerate / ZERO_LOGFREQ_IN_HZ); - memorypcm_map[patch]->midinote = midinote; - memorypcm_map[patch]->loopstart = loopstart; - // Grab the samples and len from bytes - mp_buffer_info_t bufinfo; - mp_get_buffer(bytes, &bufinfo, MP_BUFFER_READ); - memorypcm_map[patch]->length = bufinfo.len / 2; - // Alloc the buffer and copy to Tulip RAM. The python alloc'd one will go away in gc - //fprintf(stderr, "samplerate %d midinote %d, loopstart %d loopend %d length %lu\n", samplerate, midinote, loopstart, loopend, bufinfo.len/2); - memorypcm_map[patch]->sample_ram = malloc_caps(bufinfo.len, MALLOC_CAP_SPIRAM); - if(memorypcm_map[patch]->sample_ram == NULL) { - free_caps(memorypcm_map[patch]); - return -1; // no ram for sample - } - - if(loopend == 0) { // loop whole sample - memorypcm_map[patch]->loopend = memorypcm_map[patch]->length-1; - } else { - memorypcm_map[patch]->loopend = loopend; - } - memcpy(memorypcm_map[patch]->sample_ram, bufinfo.buf, bufinfo.len); - return patch; // patch number -} - -void memorypcm_unload_patch(uint8_t patch) { - if(memorypcm_map[patch] == NULL) return; - free_caps(memorypcm_map[patch]->sample_ram); - free_caps(memorypcm_map[patch]); - memorypcm_map[patch] = NULL; -} - -//free all patches -void memorypcm_unload() { - for(uint8_t i=0;imidinote. - synth[osc].logfreq_coefs[COEF_CONST] = memorypcm_map[synth[osc].patch]->log2sr - logfreq_for_midi_note(memorypcm_map[synth[osc].patch]->midinote); - } - synth[osc].phase = 0; // s16.15 index into the table; as if a PHASOR into a 16 bit sample table. - // Special case: We use the msynth feedback flag to indicate note-off for looping PCM. As a result, it's explicitly NOT set in amy:hold_and_modify for PCM voices. Set it here. - msynth[osc].feedback = synth[osc].feedback; - } -} - - -void memorypcm_note_off(uint16_t osc) { - if(osc_patch_exists(osc)) { - if(msynth[osc].feedback == 0) { - // Non-looping note: Set phase to the end to cause immediate stop. - synth[osc].phase = F2P(memorypcm_map[synth[osc].patch]->length / (float)(1 << PCM_INDEX_BITS)); - } else { - // Looping is requested, disable future looping, sample will play through to end. - // (sending a second note-off will stop it immediately). - msynth[osc].feedback = 0; - } - } -} - -void memorypcm_mod_trigger(uint16_t osc) { - memorypcm_note_on(osc, 0); -} - -SAMPLE memorypcm_render(SAMPLE* buf, uint16_t osc) { - if(osc_patch_exists(osc)) { - // Patches can be > 32768 samples long. - // We need s16.15 fixed-point indexing. - memorypcm_map_t* patch = memorypcm_map[synth[osc].patch]; - float logfreq = msynth[osc].logfreq; - // If osc[midi_note] is unset, apply patch's default here. - if (AMY_IS_UNSET(synth[osc].midi_note)) logfreq += logfreq_for_midi_note(patch->midinote); - float playback_freq = freq_of_logfreq(logfreq); // PCM_SAMPLE_RATE modified by - - SAMPLE max_value = 0; - SAMPLE amp = F2S(msynth[osc].amp); - PHASOR step = F2P((playback_freq / (float)AMY_SAMPLE_RATE) / (float)(1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = patch->sample_ram; - uint32_t base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - //fprintf(stderr, "render_pcm: time=%.3f patch=%d base_index=%d length=%d loopstart=%d loopend=%d fb=%f is_unset_note_off %d\n", total_samples / (float)AMY_SAMPLE_RATE, synth[osc].patch, base_index, patch->length, patch->loopstart, patch->loopend, msynth[osc].feedback, AMY_IS_UNSET(synth[osc].note_off_clock)); - for(uint16_t i=0; i < AMY_BLOCK_SIZE; i++) { - SAMPLE frac = S_FRAC_OF_P(synth[osc].phase, PCM_INDEX_BITS); - LUTSAMPLE b = table[base_index]; - LUTSAMPLE c = b; - if (base_index < patch->length) c = table[base_index + 1]; - SAMPLE sample = L2S(b) + MUL0_SS(L2S(c - b), frac); - synth[osc].phase = P_WRAPPED_SUM(synth[osc].phase, step); - base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - if(base_index >= patch->length) { // end - synth[osc].status = STATUS_OFF;// is this right? - sample = 0; - } else { - if(msynth[osc].feedback > 0) { // still looping. The feedback flag is cleared by pcm_note_off. - if(base_index >= patch->loopend) { // loopend - // back to loopstart - int32_t loop_len = patch->loopend - patch->loopstart; - synth[osc].phase -= F2P(loop_len / (float)(1 << PCM_INDEX_BITS)); - base_index -= loop_len; - } - } - } - SAMPLE value = buf[i] + MUL4_SS(amp, sample); - buf[i] = value; - if (value < 0) value = -value; - if (value > max_value) max_value = value; - } - //printf("render_pcm: osc %d patch %d len %d base_ix %d phase %f step %f tablestep %f amp %f\n", - // osc, synth[osc].patch, patch->length, base_index, P2F(synth[osc].phase), P2F(step), (1 << PCM_INDEX_BITS) * P2F(step), S2F(msynth[osc].amp)); - return max_value; - } - return 0; // no patch here -} - -SAMPLE memorypcm_compute_mod(uint16_t osc) { - if(osc_patch_exists(osc)) { - float mod_sr = (float)AMY_SAMPLE_RATE / (float)AMY_BLOCK_SIZE; - memorypcm_map_t* patch = memorypcm_map[synth[osc].patch]; - PHASOR step = F2P(((float)patch->samplerate / mod_sr) / (1 << PCM_INDEX_BITS)); - const LUTSAMPLE* table = patch->sample_ram; - uint32_t base_index = INT_OF_P(synth[osc].phase, PCM_INDEX_BITS); - SAMPLE sample; - if(base_index >= patch->length) { // end - synth[osc].status = STATUS_OFF;// is this right? - sample = 0; - } else { - sample = L2S(table[base_index]); - synth[osc].phase = P_WRAPPED_SUM(synth[osc].phase, step); - } - return MUL4_SS(F2S(msynth[osc].amp), sample); - } - return 0; // no patch -} - -struct custom_oscillator memorypcm = { - memorypcm_init, - memorypcm_note_on, - memorypcm_note_off, - memorypcm_mod_trigger, - memorypcm_render, - memorypcm_compute_mod -}; - - diff --git a/tulip/shared/modtulip.c b/tulip/shared/modtulip.c index 960f523f..eb6826ed 100644 --- a/tulip/shared/modtulip.c +++ b/tulip/shared/modtulip.c @@ -221,37 +221,6 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_bg_blit_obj, 6, 7, tulip_bg_bli -extern int8_t memorypcm_load(mp_obj_t bytes, uint32_t samplerate, uint8_t midinote, uint32_t loopstart, uint32_t loopend); -extern void memorypcm_unload_patch(uint8_t patch); -extern void memorypcm_unload(); - -STATIC mp_obj_t tulip_call_load_sample(size_t n_args, const mp_obj_t *args) { - uint32_t samplerate = 44100; - uint8_t midinote = 60; - uint32_t loopstart = 0; - uint32_t loopend = 0; - if(n_args > 1) samplerate = mp_obj_get_int(args[1]); - if(n_args > 2) midinote = mp_obj_get_int(args[2]); - if(n_args > 3) loopstart = mp_obj_get_int(args[3]); - if(n_args > 4) loopend = mp_obj_get_int(args[4]); - int8_t patch = memorypcm_load(args[0], samplerate, midinote, loopstart, loopend); - return mp_obj_new_int(patch); -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_call_load_sample_obj, 1, 5, tulip_call_load_sample); - -STATIC mp_obj_t tulip_unload_patch(size_t n_args, const mp_obj_t *args) { - if(n_args > 0) { - memorypcm_unload_patch(mp_obj_get_int(args[0])); - return mp_const_none; - } - memorypcm_unload(); - return mp_const_none; -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_unload_patch_obj, 0, 1, tulip_unload_patch); - - // tulip.bg_png(bytes, x,y) // tulip.bg_png(filename, x,y) STATIC mp_obj_t tulip_bg_png(size_t n_args, const mp_obj_t *args) { @@ -1392,8 +1361,6 @@ STATIC const mp_rom_map_elem_t tulip_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_bg_circle), MP_ROM_PTR(&tulip_bg_circle_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_bezier), MP_ROM_PTR(&tulip_bg_bezier_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_line), MP_ROM_PTR(&tulip_bg_line_obj) }, - { MP_ROM_QSTR(MP_QSTR_call_load_sample), MP_ROM_PTR(&tulip_call_load_sample_obj) }, - { MP_ROM_QSTR(MP_QSTR_unload_patch), MP_ROM_PTR(&tulip_unload_patch_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_roundrect), MP_ROM_PTR(&tulip_bg_roundrect_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_triangle), MP_ROM_PTR(&tulip_bg_triangle_obj) }, { MP_ROM_QSTR(MP_QSTR_bg_fill), MP_ROM_PTR(&tulip_bg_fill_obj) }, diff --git a/tulip/shared/py/wave.py b/tulip/shared/py/wave.py deleted file mode 100644 index 6f0ff844..00000000 --- a/tulip/shared/py/wave.py +++ /dev/null @@ -1,509 +0,0 @@ -"""Stuff to parse WAVE files. - -Usage. - -Reading WAVE files: - f = wave.open(file, 'r') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods read(), seek(), and close(). -When the setpos() and rewind() methods are not used, the seek() -method is not necessary. - -This returns an instance of a class with the following public methods: - getnchannels() -- returns number of audio channels (1 for - mono, 2 for stereo) - getsampwidth() -- returns sample width in bytes - getframerate() -- returns sampling frequency - getnframes() -- returns number of audio frames - getcomptype() -- returns compression type ('NONE' for linear samples) - getcompname() -- returns human-readable version of - compression type ('not compressed' linear samples) - getparams() -- returns a namedtuple consisting of all of the - above in the above order - getmarkers() -- returns None (for compatibility with the - aifc module) - getmark(id) -- raises an error since the mark does not - exist (for compatibility with the aifc module) - readframes(n) -- returns at most n frames of audio - rewind() -- rewind to the beginning of the audio stream - setpos(pos) -- seek to the specified position - tell() -- return the current position - close() -- close the instance (make it unusable) -The position returned by tell() and the position given to setpos() -are compatible and have nothing to do with the actual position in the -file. -The close() method is called automatically when the class instance -is destroyed. - -Writing WAVE files: - f = wave.open(file, 'w') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods write(), tell(), seek(), and -close(). - -This returns an instance of a class with the following public methods: - setnchannels(n) -- set the number of channels - setsampwidth(n) -- set the sample width - setframerate(n) -- set the frame rate - setnframes(n) -- set the number of frames - setcomptype(type, name) - -- set the compression type and the - human-readable compression type - setparams(tuple) - -- set all parameters at once - tell() -- return current position in output file - writeframesraw(data) - -- write audio frames without pathing up the - file header - writeframes(data) - -- write audio frames and patch up the file header - close() -- patch up the file header and close the - output file -You should set the parameters before the first writeframesraw or -writeframes. The total number of frames does not need to be set, -but when it is set to the correct value, the header does not have to -be patched up. -It is best to first set all parameters, perhaps possibly the -compression type, and then write audio frames using writeframesraw. -When all frames have been written, either call writeframes('') or -close() to patch up the sizes in the header. -The close() method is called automatically when the class instance -is destroyed. -""" - -import builtins - -__all__ = ["open", "openfp", "Error"] - -class Error(Exception): - pass - -WAVE_FORMAT_PCM = 0x0001 - -_array_fmts = None, 'b', 'h', None, 'i' - -#import audioop -import struct -import sys -from chunk import Chunk -from collections import namedtuple - -_wave_params = namedtuple('_wave_params', - 'nchannels sampwidth framerate nframes comptype compname') - -class Wave_read: - """Variables used in this class: - - These variables are available to the user though appropriate - methods of this class: - _file -- the open file with methods read(), close(), and seek() - set through the __init__() method - _nchannels -- the number of audio channels - available through the getnchannels() method - _nframes -- the number of audio frames - available through the getnframes() method - _sampwidth -- the number of bytes per audio sample - available through the getsampwidth() method - _framerate -- the sampling frequency - available through the getframerate() method - _comptype -- the AIFF-C compression type ('NONE' if AIFF) - available through the getcomptype() method - _compname -- the human-readable AIFF-C compression type - available through the getcomptype() method - _soundpos -- the position in the audio stream - available through the tell() method, set through the - setpos() method - - These variables are used internally only: - _fmt_chunk_read -- 1 iff the FMT chunk has been read - _data_seek_needed -- 1 iff positioned correctly in audio - file for readframes() - _data_chunk -- instantiation of a chunk class for the DATA chunk - _framesize -- size of one frame in the file - """ - - def initfp(self, file): - self._convert = None - self._soundpos = 0 - self._file = Chunk(file, bigendian = 0) - if self._file.getname() != b'RIFF': - raise Error('file does not start with RIFF id') - if self._file.read(4) != b'WAVE': - raise Error('not a WAVE file') - self._fmt_chunk_read = 0 - self._data_chunk = None - while 1: - self._data_seek_needed = 1 - try: - chunk = Chunk(self._file, bigendian = 0) - except EOFError: - break - chunkname = chunk.getname() - if chunkname == b'fmt ': - self._read_fmt_chunk(chunk) - self._fmt_chunk_read = 1 - elif chunkname == b'data': - if not self._fmt_chunk_read: - raise Error('data chunk before fmt chunk') - self._data_chunk = chunk - self._nframes = chunk.chunksize // self._framesize - self._data_seek_needed = 0 - break - elif chunkname == b'smpl': - self._read_smpl_chunk(chunk) - chunk.skip() - if not self._fmt_chunk_read or not self._data_chunk: - raise Error('fmt chunk and/or data chunk missing') - - def __init__(self, f): - self._i_opened_the_file = None - if isinstance(f, str): - f = builtins.open(f, 'rb') - self._i_opened_the_file = f - # else, assume it is an open file object already - try: - self.initfp(f) - except: - if self._i_opened_the_file: - f.close() - raise - - def __del__(self): - self.close() - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - # - # User visible methods. - # - def getfp(self): - return self._file - - def rewind(self): - self._data_seek_needed = 1 - self._soundpos = 0 - - def close(self): - if self._i_opened_the_file: - self._i_opened_the_file.close() - self._i_opened_the_file = None - self._file = None - - def tell(self): - return self._soundpos - - def getnchannels(self): - return self._nchannels - - def getnframes(self): - return self._nframes - - def getsampwidth(self): - return self._sampwidth - - def getframerate(self): - return self._framerate - - def getcomptype(self): - return self._comptype - - def getcompname(self): - return self._compname - - def getparams(self): - return _wave_params(self.getnchannels(), self.getsampwidth(), - self.getframerate(), self.getnframes(), - self.getcomptype(), self.getcompname()) - - def getmarkers(self): - return None - - def getmark(self, id): - raise Error('no marks') - - def setpos(self, pos): - if pos < 0 or pos > self._nframes: - raise Error('position not in range') - self._soundpos = pos - self._data_seek_needed = 1 - - def readframes(self, nframes): - if self._data_seek_needed: - self._data_chunk.seek(0, 0) - pos = self._soundpos * self._framesize - if pos: - self._data_chunk.seek(pos, 0) - self._data_seek_needed = 0 - if nframes == 0: - return b'' - data = self._data_chunk.read(nframes * self._framesize) - if self._sampwidth != 1 and sys.byteorder == 'big': - data = audioop.byteswap(data, self._sampwidth) - if self._convert and data: - data = self._convert(data) - self._soundpos = self._soundpos + len(data) // (self._nchannels * self._sampwidth) - return data - - # - # Internal methods. - # - def _read_smpl_chunk(self, chunk): - _, _,_, self._midinote, _, _, _, self._loops, _ = struct.unpack('0): # read first loop - _, _, self._loopstart, self._loopend = struct.unpack(' 4: - raise Error('bad sample width') - self._sampwidth = sampwidth - - def getsampwidth(self): - if not self._sampwidth: - raise Error('sample width not set') - return self._sampwidth - - def setframerate(self, framerate): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - if framerate <= 0: - raise Error('bad frame rate') - self._framerate = int(round(framerate)) - - def getframerate(self): - if not self._framerate: - raise Error('frame rate not set') - return self._framerate - - def setnframes(self, nframes): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - self._nframes = nframes - - def getnframes(self): - return self._nframeswritten - - def setcomptype(self, comptype, compname): - if self._datawritten: - raise Error('cannot change parameters after starting to write') - if comptype not in ('NONE',): - raise Error('unsupported compression type') - self._comptype = comptype - self._compname = compname - - def getcomptype(self): - return self._comptype - - def getcompname(self): - return self._compname - - def setparams(self, params): - nchannels, sampwidth, framerate, nframes, comptype, compname = params - if self._datawritten: - raise Error('cannot change parameters after starting to write') - self.setnchannels(nchannels) - self.setsampwidth(sampwidth) - self.setframerate(framerate) - self.setnframes(nframes) - self.setcomptype(comptype, compname) - - def getparams(self): - if not self._nchannels or not self._sampwidth or not self._framerate: - raise Error('not all parameters set') - return _wave_params(self._nchannels, self._sampwidth, self._framerate, - self._nframes, self._comptype, self._compname) - - def setmark(self, id, pos, name): - raise Error('setmark() not supported') - - def getmark(self, id): - raise Error('no marks') - - def getmarkers(self): - return None - - def tell(self): - return self._nframeswritten - - def writeframesraw(self, data): - if not isinstance(data, (bytes, bytearray)): - data = memoryview(data).cast('B') - self._ensure_header_written(len(data)) - nframes = len(data) // (self._sampwidth * self._nchannels) - if self._convert: - data = self._convert(data) - if self._sampwidth != 1 and sys.byteorder == 'big': - data = audioop.byteswap(data, self._sampwidth) - self._file.write(data) - self._datawritten += len(data) - self._nframeswritten = self._nframeswritten + nframes - - def writeframes(self, data): - self.writeframesraw(data) - if self._datalength != self._datawritten: - self._patchheader() - - def close(self): - if self._file: - try: - self._ensure_header_written(0) - if self._datalength != self._datawritten: - self._patchheader() - self._file.flush() - finally: - self._file = None - if self._i_opened_the_file: - self._i_opened_the_file.close() - self._i_opened_the_file = None - - # - # Internal methods. - # - - def _ensure_header_written(self, datasize): - if not self._headerwritten: - if not self._nchannels: - raise Error('# channels not specified') - if not self._sampwidth: - raise Error('sample width not specified') - if not self._framerate: - raise Error('sampling rate not specified') - self._write_header(datasize) - - def _write_header(self, initlength): - assert not self._headerwritten - self._file.write(b'RIFF') - if not self._nframes: - self._nframes = initlength // (self._nchannels * self._sampwidth) - self._datalength = self._nframes * self._nchannels * self._sampwidth - try: - self._form_length_pos = self._file.tell() - except (AttributeError, OSError): - self._form_length_pos = None - self._file.write(struct.pack(' Date: Sun, 13 Oct 2024 14:49:28 -0400 Subject: [PATCH 5/8] rebase --- amy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amy b/amy index 066f3ce4..826e3b0d 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 066f3ce439ea9cc54441bb76c17bd9bf7496b5ff +Subproject commit 826e3b0d46ff39ab532054a6d595e8e8e604cbed From b0e3dda4070e27c17f1bdee5159e579ccad5962c Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Sun, 13 Oct 2024 15:22:55 -0400 Subject: [PATCH 6/8] alles fix --- amy | 2 +- tulip/shared/alles.c | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/amy b/amy index 826e3b0d..96a6e0e3 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 826e3b0d46ff39ab532054a6d595e8e8e604cbed +Subproject commit 96a6e0e38201d92ac7921f71f99de7fbc043e406 diff --git a/tulip/shared/alles.c b/tulip/shared/alles.c index 8ce9d34c..e78a193f 100644 --- a/tulip/shared/alles.c +++ b/tulip/shared/alles.c @@ -40,8 +40,8 @@ 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 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; amy_err_t sync_init() { @@ -422,8 +422,12 @@ void handle_sync(int32_t time, int8_t index) { //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; - computed_delta = time - sysclock; - computed_delta_set = 1; + + // 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); } From de82d5464c9dc19695ab35a27291a8be37424e38 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Tue, 15 Oct 2024 17:04:34 -0400 Subject: [PATCH 7/8] better docs for new in-AMY load sample --- amy | 2 +- docs/music.md | 12 +++++++----- docs/tulip_api.md | 16 +++++++--------- tulip/shared/py/tulip.py | 21 --------------------- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/amy b/amy index 96a6e0e3..74d61d85 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 96a6e0e38201d92ac7921f71f99de7fbc043e406 +Subproject commit 74d61d850861b9d94faa4ad2ffc3064d9973715d diff --git a/docs/music.md b/docs/music.md index fe4ce6f0..96f09a2a 100644 --- a/docs/music.md +++ b/docs/music.md @@ -305,24 +305,26 @@ s.note_on(40, 1.0) s.note_off(40) # looped instruments require a note_off to stop ``` -You can load your own samples into Tulip. Take any .wav file and [load it onto Tulip.](getting_started.md#transfer-files-between-tulip-and-your-computer) Now, load it in as a `CUSTOM` PCM patch: +You can load your own samples into Tulip. Take any .wav file and [load it onto Tulip.](getting_started.md#transfer-files-between-tulip-and-your-computer) Now, load it in as a PCM patch: ```python -patch = tulip.load_sample('sample.wav') -s = midi.OscSynth(wave=amy.CUSTOM, patch=patch) +amy.load_sample('sample.wav', patch=50) +s = midi.OscSynth(wave=amy.PCM, patch=50) s.note_on(60, 1.0) ``` You can also load PCM patches with looped segments if you have their loopstart and loopend parameters (these are often stored in the WAVE metadata. If the .wav file has this metadata, we'll parse it. The example file `/sys/ex/vlng3.wav` has it. You can also provide the metadata directly.) To indicate looping, use `feedback=1`. ```python -patch = tulip.load_sample("/sys/ex/vlng3.wav") # loads wave looping metadata -s = midi.OscSynth(wave=amy.CUSTOM, patch=patch, feedback=1, num_voices=1) +amy.load_sample("/sys/ex/vlng3.wav", patch=50) # loads wave looping metadata +s = midi.OscSynth(wave=amy.CUSTOM, patch=50, feedback=1, num_voices=1) s.note_on(60, 1.0) # loops s.note_on(55, 1.0) # loops s.note_off(55) # stops ``` +You can unload samples from RAM with `amy.unload_sample(patch_number)`. + ## Modify Juno-6 patches programatically We showed above how to `run('juno6')` to see a Juno-6 editor. But if you want your code to change the patches, you can do it yourself with: diff --git a/docs/tulip_api.md b/docs/tulip_api.md index e560485f..83707aec 100644 --- a/docs/tulip_api.md +++ b/docs/tulip_api.md @@ -439,24 +439,22 @@ amy.send(voices='0', load_patch=101, note=50, vel=1, client=2) # just a certain alles.local() # turns off mesh mode and goes back to local mode ``` -To load your own WAVE files as samples, use `tulip.load_sample`: +To load your own WAVE files as samples, use `amy.load_sample`: ```python # To save space / RAM, you may want to downsample your WAVE files to 11025 or 22050Hz. We detect SR automatically. -patch = tulip.load_sample("flutea4.wav") # samples are converted to mono if they are stereo +amy.load_sample("flutea4.wav", patch=50) # samples are converted to mono if they are stereo. patch # can be anything # You can optionally tell us the loop start and end point (in samples), and base MIDI note of the sample. # We can detect this in WAVE file metadata if it exists! (Many sample packs include this.) -patch = tulip.load_sample("flutea4.wav", midinote=81, loopstart=1020, loopend=1500) +amy.load_sample("flutea4.wav", midinote=81, loopstart=1020, loopend=1500, patch=50) -# The patch number can now be used in the custom Tulip memory PCM sample player. -# It has all the features of the AMY's PCM wave type. -amy.send(osc=20, wave=amy.CUSTOM, patch=patch, vel=1, note=50) +# The patch number can now be used in AMY's PCM sample player. +amy.send(osc=20, wave=amy.PCM, patch=50, vel=1, note=50) -# You can load up to 32 custom PCM patches. Be careful of memory use. load_sample will return -1 if there's no more room. # You can unload already allocated patches: -tulip.unload_patch(patch) # frees the RAM and the patch slot -tulip.unload_patch() # frees all allocated PCM patches +amy.unload_sample(patch) # frees the RAM and the patch slot +amy.reset() # frees all allocated PCM patches ``` To send signals over CV on Tulip CC (hardware only): diff --git a/tulip/shared/py/tulip.py b/tulip/shared/py/tulip.py index d0eda4a5..632eb806 100644 --- a/tulip/shared/py/tulip.py +++ b/tulip/shared/py/tulip.py @@ -660,27 +660,6 @@ def wifi(ssid, passwd, wait_timeout=10): return ip() -def load_sample(wavfile, midinote=0, loopstart=0, loopend=0): - import wave - w = wave.open(wavfile, 'r') - f = w.readframes(w.getnframes()) - if(w.getnchannels()>1): - # de-interleave and just choose the first channel - f = bytes([f[j] for i in range(0,len(f),4) for j in (i,i+1)]) - if(loopstart==0): - if(hasattr(w,'_loopstart')): - loopstart = w._loopstart - if(loopend==0): - if(hasattr(w,'_loopend')): - loopend = w._loopend - if(midinote==0): - if(hasattr(w,'_midinote')): - midinote = w._midinote - else: - midinote=60 - return call_load_sample(f, w.getframerate(), midinote, loopstart, loopend) - - def tar_create(directory): import utarfile t = utarfile.TarFile(directory+".tar", 'w') From a4e19c7c48b9e0e8cf179e34434093ef44d377a7 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Tue, 15 Oct 2024 17:08:25 -0400 Subject: [PATCH 8/8] wrong event type --- docs/tulip_api.md | 8 ++++++++ tulip/shared/alles.c | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/tulip_api.md b/docs/tulip_api.md index 83707aec..919725d4 100644 --- a/docs/tulip_api.md +++ b/docs/tulip_api.md @@ -457,6 +457,14 @@ amy.unload_sample(patch) # frees the RAM and the patch slot amy.reset() # frees all allocated PCM patches ``` +On Tulip Desktop, or with an [AMYboard / AMYchip](https://github.com/shorepine/amychip) connected to a hardware Tulip over I2C, you can use audio input as well. This is brand new and we're still working out a good API for it. For now, you can set any oscillator to be fed by the L or R channel of an audio input. + +```python +amy.send(osc=0, wave=amy.AUDIO_IN0, vel=1) +amy.echo(1, 250, 500, 0.8) # echo effect on the audio input +``` + + To send signals over CV on Tulip CC (hardware only): ```python diff --git a/tulip/shared/alles.c b/tulip/shared/alles.c index e78a193f..0b6719be 100644 --- a/tulip/shared/alles.c +++ b/tulip/shared/alles.c @@ -306,7 +306,7 @@ void alles_parse_message(char *message, uint16_t length) { // Parse the AMY stuff out of the message first struct event e = amy_parse_message(message); - if(e.status == TRANSFER_DATA) { + if(e.status == EVENT_TRANSFER_DATA) { // transfer data already dealt with. we skip this followon check. length = 0; } else {