Skip to content

Commit 1acb698

Browse files
authored
Merge pull request #566 from shorepine/pythonaudio
Support for AMY input and output buffer callbacks in Python
2 parents 4bfd2f4 + 243f246 commit 1acb698

50 files changed

Lines changed: 1010 additions & 240 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/tulip_api.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ amy.send(voices='0', load_patch=101, note=50, vel=1) # all Alles speakers in a m
493493
amy.send(voices='0', load_patch=101, note=50, vel=1, client=2) # just a certain client
494494
```
495495

496-
To load your own WAVE files as samples, use `amy.load_sample`:
496+
To load your own WAVE files as samples you can play like an instrument, use `amy.load_sample`:
497497

498498
```python
499499
# To save space / RAM, you may want to downsample your WAVE files to 11025 or 22050Hz. We detect SR automatically.
@@ -543,6 +543,54 @@ for i,note in enumerate(chord.midinotes()):
543543
amy.send(wave=amy.SINE,osc=i*9,note=note,vel=0.25)
544544
```
545545

546+
## Low level sample access and direct audio playback
547+
548+
You can access the audio input (`AUDIO_IN0/1`) sample buffer per frame (on Tulip Desktop, Web, and future devices with audio input support), and you can also set two external audio channels (`AUDIO_EXT0/1`) from Python. This lets you synthesize audio in Python, or do things like stream WAV files from disk to the audio output.
549+
550+
To do so, you need to register an AMY frame callback in Tulip. In this example, we open a WAV file and read it 256 frames per block, and set those frames to the EXT0/1 oscillators, which we initialize as AMY oscillators 0 and 1, with their pan set to left and right.
551+
552+
```python
553+
# Play a wav file through AMY, streaming from disk
554+
import amy_wave
555+
f = amy_wave.open(wav_filename,'rb')
556+
557+
def cb(x):
558+
frames = f.readframes(256)
559+
if(len(frames)!=1024): # file done. stop the AMY frame callback.
560+
frames = bytes(1024)
561+
tulip.amy_block_done_callback()
562+
# Sets the stereo channel buffer EXT0/EXT1 from the frames bytes
563+
tulip.amy_set_external_input_buffer(frames)
564+
565+
amy.reset()
566+
amy.send(osc=0,wave=amy.AUDIO_EXT0, pan=0, vel=1)
567+
amy.send(osc=1,wave=amy.AUDIO_EXT1, pan=1, vel=1)
568+
tulip.amy_block_done_callback(cb)
569+
```
570+
571+
To sample incoming audio (on devices that support it), use `tulip.amy_get_input_buffer`:
572+
573+
```python
574+
buf = bytes()
575+
tick_start = 0
576+
ms = 2000
577+
578+
def sample(x):
579+
global buf
580+
buf = buf + tulip.amy_get_input_buffer()
581+
# stop "recording" to buf after ms
582+
if(tulip.ticks_ms() > tick_start + ms):
583+
tulip.amy_block_done_callback()
584+
play()
585+
586+
tick_start = tulip.ticks_ms()
587+
print("Recording for 2s. Make sure audio input is on!")
588+
tulip.amy_block_done_callback(sample)
589+
# Then buf will have stereo frames of audio to do whatever you want with.
590+
```
591+
592+
Please note: on Tulip CC hardware, you do not have much compute time left per block to do much. Reading files, saving to memory or doing simple synthesis works, but most anything more complicated you should write your effects / synthesis code in C, as part of AMY.
593+
546594
## Music sequencer
547595

548596
Tulip is always running AMY's live sequencer, which allows you to have multiple music programs running sharing a common clock.

tulip/amyboard/amychip.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ float cv_input_hook(uint16_t channel) {
211211
#endif
212212
}
213213

214-
214+
extern mp_obj_t audio_buffer_callback;
215215
// Write to the GP8413
216216
uint8_t cv_output_hook(uint16_t osc, SAMPLE * buf, uint16_t len) {
217+
if(external_map[osc]==1 || external_map[osc]==2) {
217218
#ifdef ESP_PLATFORM
218-
if(external_map[osc]>0) {
219219
// -5v to +5v?
220220
float volts = S2F(buf[0])*5.0;
221221
int32_t val = (int32_t)(((volts + 10)/20.0) * 0x8000);
@@ -229,13 +229,16 @@ uint8_t cv_output_hook(uint16_t osc, SAMPLE * buf, uint16_t len) {
229229
uint8_t channel = external_map[osc]-1;
230230
if(channel == 1) ch = 0x04;
231231
bytes[0] = ch;
232-
//fprintf(stderr, "writing %f volts [%ld] to channel %d\n", volts, val, channel);
233232
i2c_master_write_to_device(I2C_NUM_0, addr, bytes, 3, pdMS_TO_TICKS(10));
233+
// silence this output
234234
return 1;
235-
}
236235
#endif
236+
return 0;
237+
} else if(external_map[osc]>2) { // python audio buffer callback, WIP
238+
239+
return 0;
240+
}
237241
return 0;
238-
239242
}
240243

241244
#ifdef ESP_PLATFORM
@@ -276,7 +279,9 @@ void amyboard_fill_audio_buffer_task() {
276279
if(written != AMY_BLOCK_SIZE * sizeof(i2s_sample_type) * AMY_NCHANS || read != AMY_BLOCK_SIZE * sizeof(i2s_sample_type) * AMY_NCHANS) {
277280
fprintf(stderr,"i2s underrun: [w %d,r %d] vs %d\n", written, read, AMY_BLOCK_SIZE * sizeof(i2s_sample_type) * AMY_NCHANS);
278281
}
279-
282+
if(audio_buffer_callback != NULL) {
283+
mp_sched_schedule(audio_buffer_callback, mp_obj_new_int(osc));
284+
}
280285
}
281286
}
282287
extern void esp_render_task( void * pvParameters);

tulip/amyboardweb/main.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ void setup_fs() {
129129
void mp_js_init(int pystack_size, int heap_size) {
130130

131131
setup_fs();
132-
emscripten_set_main_loop(main_loop__tulip, 60, 0);
132+
emscripten_set_main_loop(main_loop__tulip, 0, 0);
133133

134134
#if MICROPY_ENABLE_PYSTACK
135135
mp_obj_t *pystack = (mp_obj_t *)malloc(pystack_size * sizeof(mp_obj_t));

tulip/amyboardweb/proxy_c.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ enum {
5050
PROXY_KIND_MP_OBJECT = 8,
5151
PROXY_KIND_MP_JSPROXY = 9,
5252
PROXY_KIND_MP_EXISTING = 10,
53+
PROXY_KIND_MP_BYTES = 11,
5354
};
5455

5556
enum {
@@ -61,6 +62,7 @@ enum {
6162
PROXY_KIND_JS_STRING = 5,
6263
PROXY_KIND_JS_OBJECT = 6,
6364
PROXY_KIND_JS_PYPROXY = 7,
65+
PROXY_KIND_JS_BYTES = 8,
6466
};
6567

6668
MP_DEFINE_CONST_OBJ_TYPE(
@@ -170,6 +172,10 @@ mp_obj_t proxy_convert_js_to_mp_obj_cside(uint32_t *value) {
170172
mp_obj_t s = mp_obj_new_str((void *)value[2], value[1]);
171173
free((void *)value[2]);
172174
return s;
175+
} else if (value[0] == PROXY_KIND_JS_BYTES) {
176+
mp_obj_t s = mp_obj_new_bytes((void *) value[2], value[1]);
177+
free((void *)value[2]);
178+
return s;
173179
} else if (value[0] == PROXY_KIND_JS_PYPROXY) {
174180
return proxy_c_get_obj(value[1]);
175181
} else {
@@ -200,6 +206,14 @@ void proxy_convert_mp_to_js_obj_cside(mp_obj_t obj, uint32_t *out) {
200206
const char *str = mp_obj_str_get_data(obj, &len);
201207
out[1] = len;
202208
out[2] = (uintptr_t)str;
209+
} else if (mp_obj_get_type(obj) == &mp_type_bytes || mp_obj_get_type(obj) == &mp_type_bytearray) {
210+
kind = PROXY_KIND_MP_BYTES;
211+
mp_buffer_info_t bufinfo;
212+
mp_get_buffer(obj, &bufinfo, MP_BUFFER_READ);
213+
size_t len = bufinfo.len;
214+
const uint8_t *buf = bufinfo.buf;
215+
out[1] = len;
216+
out[2] = (uintptr_t)buf;
203217
} else if (obj == mp_const_undefined) {
204218
kind = PROXY_KIND_MP_JSPROXY;
205219
out[1] = 1;

tulip/amyboardweb/proxy_js.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const PROXY_KIND_MP_GENERATOR = 7;
4141
const PROXY_KIND_MP_OBJECT = 8;
4242
const PROXY_KIND_MP_JSPROXY = 9;
4343
const PROXY_KIND_MP_EXISTING = 10;
44+
const PROXY_KIND_MP_BYTES = 11;
4445

4546
const PROXY_KIND_JS_UNDEFINED = 0;
4647
const PROXY_KIND_JS_NULL = 1;
@@ -50,6 +51,8 @@ const PROXY_KIND_JS_DOUBLE = 4;
5051
const PROXY_KIND_JS_STRING = 5;
5152
const PROXY_KIND_JS_OBJECT = 6;
5253
const PROXY_KIND_JS_PYPROXY = 7;
54+
const PROXY_KIND_JS_BYTES = 8;
55+
5356

5457
class PythonError extends Error {
5558
constructor(exc_type, exc_details) {
@@ -196,7 +199,15 @@ function proxy_convert_js_to_mp_obj_jsside(js_obj, out) {
196199
Module.stringToUTF8(js_obj, buf, len + 1);
197200
Module.setValue(out + 4, len, "i32");
198201
Module.setValue(out + 8, buf, "i32");
199-
} else if (
202+
} else if (js_obj instanceof Uint8Array) {
203+
kind = PROXY_KIND_JS_BYTES;
204+
const len = js_obj.length;
205+
const buf = Module._malloc(len);
206+
for(let i = 0; i < len; i++) Module.HEAPU8[buf + i] = js_obj[i];
207+
Module.setValue(out + 4, len, "i32");
208+
Module.setValue(out + 8, buf, "i32");
209+
} else if
210+
(
200211
js_obj instanceof PyProxy ||
201212
(typeof js_obj === "function" && "_ref" in js_obj) ||
202213
js_obj instanceof PyProxyThenable
@@ -265,6 +276,11 @@ function proxy_convert_mp_to_js_obj_jsside(value) {
265276
const str_len = Module.getValue(value + 4, "i32");
266277
const str_ptr = Module.getValue(value + 8, "i32");
267278
obj = Module.UTF8ToString(str_ptr, str_len);
279+
} else if (kind === PROXY_KIND_MP_BYTES) {
280+
// bytes
281+
const buf_len = Module.getValue(value + 4, "i32");
282+
const buf_ptr = Module.getValue(value + 8, "i32");
283+
obj = new Uint8Array(Module.HEAPU8.buffer, buf_ptr, buf_len);
268284
} else if (kind === PROXY_KIND_MP_JSPROXY) {
269285
// js proxy
270286
const id = Module.getValue(value + 4, "i32");

tulip/amyboardweb/static/examples.js

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,96 @@ js.fetch(url).then(lambda r: r.text()).then(lambda x: json.loads(x)).then(lambda
7272
amy.send(osc=30, pan=0, wave=amy.AUDIO_IN0, vel=1)
7373
amy.send(osc=31, pan=1, wave=amy.AUDIO_IN1, vel=1)
7474
amy.echo(level=1, delay_ms=400, max_delay_ms=1500, feedback=0.8, filter_coef=None)
75-
`}
76-
]
75+
`},{
76+
't':'music',
77+
'd':'Directly play a wav file',
78+
'c':`
79+
# wav.py
80+
# plays a wavfile from amy external audio
81+
82+
import amy_wave
83+
f = None
84+
try:
85+
f = amy_wave.open(wav_filename,'rb')
86+
except:
87+
print("Upload a wav file and type >>> wav_filename='file.wav' first.")
88+
89+
def cb(x):
90+
frames = f.readframes(256)
91+
if(len(frames)!=1024):
92+
frames = bytes(1024)
93+
tulip.amy_block_done_callback()
94+
tulip.amy_set_external_input_buffer(frames)
95+
96+
if(f is not None):
97+
amy.reset()
98+
amy.send(osc=0,wave=amy.AUDIO_EXT0, pan=0, vel=1)
99+
amy.send(osc=1,wave=amy.AUDIO_EXT1, pan=1, vel=1)
100+
tulip.amy_block_done_callback(cb)
101+
`},{
102+
't':'music',
103+
'd':'Sample audio in and play it as an instrument',
104+
'c':`
105+
# sample.py
106+
# try sampling audio to pcm memorypcm
107+
108+
import tulip, amy, synth, music
109+
110+
buf = bytes()
111+
t = 0
112+
tick_start = 0
113+
ms = 2000
114+
syn = None
115+
116+
def play():
117+
amy.send(reset=amy.RESET_TIMEBASE+amy.RESET_ALL_OSCS)
118+
amy.load_sample_bytes(buf, stereo=True, patch=50, loopstart=0, loopend=int(amy.AMY_SAMPLE_RATE*(ms/1000.0)))
119+
syn = synth.OscSynth(wave=amy.PCM, patch=50)
120+
for i,note in enumerate(music.Chord('F:min7').midinotes()):
121+
syn.note_on(note+24, 1, time=i*1000)
122+
syn.note_off(note+24, time=5000)
123+
124+
def sample(x):
125+
global buf, syn
126+
buf = buf + tulip.amy_get_input_buffer()
127+
if(tulip.ticks_ms() > tick_start + ms):
128+
tulip.amy_block_done_callback()
129+
play()
130+
131+
tick_start = tulip.ticks_ms()
132+
print("Recording for 2s. Make sure audio input is on!")
133+
tulip.amy_block_done_callback(sample)
134+
`
135+
},{
136+
'd':'Generate audio buffers in Python',
137+
't':'music',
138+
'c':`
139+
# makes a sine wave. obviously, it's easier to do this in AMY directly!
140+
# but just showing how to do it for any audio synthesis in python
141+
from ulab import numpy as np
142+
import ulab
143+
freq = 440.0
144+
amp = 32767
145+
lut_size = 512
146+
count = 0
147+
lut = np.array(amp * np.sin(np.linspace(0,2*np.pi,lut_size,endpoint=False)), dtype=np.int16)
148+
# freq/2 because we send stereo buffer in this example
149+
step_size = ((freq/2.0) * lut_size) / float(amy.AMY_SAMPLE_RATE)
150+
index = 0
151+
152+
def cb(x):
153+
global count
154+
t = np.array(np.arange(count*512,count*512+512)*step_size)
155+
samples = ulab.user.arraymodlut(lut, t);
156+
tulip.amy_set_external_input_buffer(samples.tobytes())
157+
if(count==1000): # stop
158+
tulip.amy_block_done_callback()
159+
amy.reset()
160+
count = count + 1
161+
162+
amy.reset()
163+
amy.send(osc=0,wave=amy.AUDIO_EXT0, pan=0, vel=1)
164+
amy.send(osc=1,wave=amy.AUDIO_EXT1, pan=1, vel=1)
165+
tulip.amy_block_done_callback(cb)
166+
`
167+
}]

0 commit comments

Comments
 (0)