diff --git a/app/Makefile b/app/Makefile index 348df4af..10511451 100644 --- a/app/Makefile +++ b/app/Makefile @@ -24,6 +24,7 @@ SPECIAL_MKTARGETS=$(APP_MKTARGETS) SUBDIRS= \ user \ driver \ + pcm \ platform \ libc \ lua \ @@ -69,6 +70,7 @@ LD_FILE = $(LDDIR)/nodemcu.ld COMPONENTS_eagle.app.v6 = \ user/libuser.a \ driver/libdriver.a \ + pcm/pcm.a \ platform/libplatform.a \ task/libtask.a \ libc/liblibc.a \ diff --git a/app/driver/sigma_delta.c b/app/driver/sigma_delta.c index 50c99005..da5575e5 100644 --- a/app/driver/sigma_delta.c +++ b/app/driver/sigma_delta.c @@ -1,4 +1,5 @@ +#include "platform.h" #include "driver/sigma_delta.h" @@ -18,7 +19,7 @@ void sigma_delta_stop( void ) GPIO_SIGMA_DELTA_PRESCALE_SET(0x00) ); } -void sigma_delta_set_prescale_target( sint16 prescale, sint16 target ) +void ICACHE_RAM_ATTR sigma_delta_set_prescale_target( sint16 prescale, sint16 target ) { uint32_t prescale_mask, target_mask; diff --git a/app/include/task/task.h b/app/include/task/task.h index e8b9bfb9..b090e54f 100644 --- a/app/include/task/task.h +++ b/app/include/task/task.h @@ -4,6 +4,7 @@ #include "ets_sys.h" #include "osapi.h" #include "os_type.h" +#include "user_interface.h" /* use LOW / MEDIUM / HIGH since it isn't clear from the docs which is higher */ diff --git a/app/include/user_modules.h b/app/include/user_modules.h index f8ff454c..be3ad1fa 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -42,6 +42,7 @@ #define LUA_USE_MODULES_NET #define LUA_USE_MODULES_NODE #define LUA_USE_MODULES_OW +//#define LUA_USE_MODULES_PCM //#define LUA_USE_MODULES_PERF //#define LUA_USE_MODULES_PWM //#define LUA_USE_MODULES_RC diff --git a/app/modules/Makefile b/app/modules/Makefile index b6986286..ef6d6dae 100644 --- a/app/modules/Makefile +++ b/app/modules/Makefile @@ -46,6 +46,7 @@ INCLUDES += -I ../mqtt INCLUDES += -I ../u8glib INCLUDES += -I ../ucglib INCLUDES += -I ../lua +INCLUDES += -I ../pcm INCLUDES += -I ../platform INCLUDES += -I ../spiffs INCLUDES += -I ../smart diff --git a/app/modules/pcm.c b/app/modules/pcm.c new file mode 100644 index 00000000..d74170bf --- /dev/null +++ b/app/modules/pcm.c @@ -0,0 +1,263 @@ +// Module for interfacing with PCM functionality + +#include "module.h" +#include "lauxlib.h" +#include "task/task.h" +#include "c_string.h" +#include "c_stdlib.h" + +#include "pcm.h" +#include "pcm_drv.h" + + +#define GET_PUD() pud_t *pud = (pud_t *)luaL_checkudata(L, 1, "pcm.driver"); \ + cfg_t *cfg = &(pud->cfg); + +#define UNREF_CB(_cb) luaL_unref( L, LUA_REGISTRYINDEX, _cb ); \ + _cb = LUA_NOREF; + +#define COND_REF(_cond) _cond ? luaL_ref( L, LUA_REGISTRYINDEX ) : LUA_NOREF + + +static void dispatch_callback( lua_State *L, int self_ref, int cb_ref, int returns ) +{ + if (cb_ref != LUA_NOREF) { + lua_rawgeti( L, LUA_REGISTRYINDEX, cb_ref ); + lua_rawgeti( L, LUA_REGISTRYINDEX, self_ref ); + lua_call( L, 1, returns ); + } +} + +static int pcm_drv_free( lua_State *L ) +{ + GET_PUD(); + + UNREF_CB( cfg->cb_data_ref ); + UNREF_CB( cfg->cb_drained_ref ); + UNREF_CB( cfg->cb_paused_ref ); + UNREF_CB( cfg->cb_stopped_ref ); + UNREF_CB( cfg->cb_vu_ref ); + UNREF_CB( cfg->self_ref ); + + if (cfg->bufs[0].data) { + c_free( cfg->bufs[0].data ); + cfg->bufs[0].data = NULL; + } + if (cfg->bufs[1].data) { + c_free( cfg->bufs[1].data ); + cfg->bufs[1].data = NULL; + } + + return 0; +} + +// Lua: drv:close() +static int pcm_drv_close( lua_State *L ) +{ + GET_PUD(); + + pud->drv->close( cfg ); + + return pcm_drv_free( L ); +} + +// Lua: drv:stop(self) +static int pcm_drv_stop( lua_State *L ) +{ + GET_PUD(); + + // throttle ISR and reader + cfg->isr_throttled = -1; + + pud->drv->stop( cfg ); + + // invalidate the buffers + cfg->bufs[0].empty = cfg->bufs[1].empty = TRUE; + + dispatch_callback( L, cfg->self_ref, cfg->cb_stopped_ref, 0 ); + + return 0; +} + +// Lua: drv:pause(self) +static int pcm_drv_pause( lua_State *L ) +{ + GET_PUD(); + + // throttle ISR and reader + cfg->isr_throttled = -1; + + pud->drv->stop( cfg ); + + dispatch_callback( L, cfg->self_ref, cfg->cb_paused_ref, 0 ); + + return 0; +} + +static void pcm_start_play_task( task_param_t param, uint8 prio ) +{ + lua_State *L = lua_getstate(); + pud_t *pud = (pud_t *)param; + cfg_t *cfg = &(pud->cfg); + + // stop driver before starting it in case it hasn't been stopped from Lua + pud->drv->stop( cfg ); + + if (!pud->drv->play( cfg )) { + luaL_error( L, "pcm driver start" ); + } + + // unthrottle ISR and reader + pud->cfg.isr_throttled = 0; +} + +// Lua: drv:play(self, rate) +static int pcm_drv_play( lua_State *L ) +{ + GET_PUD(); + + cfg->rate = luaL_optinteger( L, 2, PCM_RATE_8K ); + + luaL_argcheck( L, (cfg->rate >= PCM_RATE_1K) && (cfg->rate <= PCM_RATE_16K), 2, "invalid bit rate" ); + + if (cfg->self_ref == LUA_NOREF) { + lua_pushvalue( L, 1 ); // copy self userdata to the top of stack + cfg->self_ref = luaL_ref( L, LUA_REGISTRYINDEX ); + } + + // schedule actions for play in separate task since drv:play() might have been called + // in the callback fn of pcm_data_play_task() which in turn gets called when starting play... + task_post_low( cfg->start_play_task, (os_param_t)pud ); + + return 0; +} + +// Lua: drv.on(self, event, cb_fn) +static int pcm_drv_on( lua_State *L ) +{ + size_t len; + const char *event; + uint8_t is_func = FALSE; + + GET_PUD(); + + event = luaL_checklstring( L, 2, &len ); + + if ((lua_type( L, 3 ) == LUA_TFUNCTION) || + (lua_type( L, 3 ) == LUA_TLIGHTFUNCTION)) { + lua_pushvalue( L, 3 ); // copy argument (func) to the top of stack + is_func = TRUE; + } + + if ((len == 4) && (c_strcmp( event, "data" ) == 0)) { + luaL_unref( L, LUA_REGISTRYINDEX, cfg->cb_data_ref); + cfg->cb_data_ref = COND_REF( is_func ); + } else if ((len == 7) && (c_strcmp( event, "drained" ) == 0)) { + luaL_unref( L, LUA_REGISTRYINDEX, cfg->cb_drained_ref); + cfg->cb_drained_ref = COND_REF( is_func ); + } else if ((len == 6) && (c_strcmp( event, "paused" ) == 0)) { + luaL_unref( L, LUA_REGISTRYINDEX, cfg->cb_paused_ref); + cfg->cb_paused_ref = COND_REF( is_func ); + } else if ((len == 7) && (c_strcmp( event, "stopped" ) == 0)) { + luaL_unref( L, LUA_REGISTRYINDEX, cfg->cb_stopped_ref); + cfg->cb_stopped_ref = COND_REF( is_func ); + } else if ((len == 2) && (c_strcmp( event, "vu" ) == 0)) { + luaL_unref( L, LUA_REGISTRYINDEX, cfg->cb_vu_ref); + cfg->cb_vu_ref = COND_REF( is_func ); + + int freq = luaL_optinteger( L, 4, 10 ); + luaL_argcheck( L, (freq > 0) && (freq <= 200), 4, "invalid range" ); + cfg->vu_freq = (uint8_t)freq; + } else { + if (is_func) { + // need to pop pushed function arg + lua_pop( L, 1 ); + } + return luaL_error( L, "method not supported" ); + } + + return 0; +} + +// Lua: pcm.new( type, pin ) +static int pcm_new( lua_State *L ) +{ + pud_t *pud = (pud_t *) lua_newuserdata( L, sizeof( pud_t ) ); + cfg_t *cfg = &(pud->cfg); + int driver; + + cfg->rbuf_idx = cfg->fbuf_idx = 0; + cfg->isr_throttled = -1; // start ISR and reader in throttled mode + + driver = luaL_checkinteger( L, 1 ); + luaL_argcheck( L, (driver >= 0) && (driver < PCM_DRIVER_END), 1, "invalid driver" ); + + cfg->self_ref = LUA_NOREF; + cfg->cb_data_ref = cfg->cb_drained_ref = LUA_NOREF; + cfg->cb_paused_ref = cfg->cb_stopped_ref = LUA_NOREF; + cfg->cb_vu_ref = LUA_NOREF; + + cfg->bufs[0].buf_size = cfg->bufs[1].buf_size = 0; + cfg->bufs[0].data = cfg->bufs[1].data = NULL; + cfg->bufs[0].len = cfg->bufs[1].len = 0; + cfg->bufs[0].rpos = cfg->bufs[1].rpos = 0; + cfg->bufs[0].empty = cfg->bufs[1].empty = TRUE; + + cfg->data_vu_task = task_get_id( pcm_data_vu_task ); + cfg->vu_freq = 10; + cfg->data_play_task = task_get_id( pcm_data_play_task ); + cfg->start_play_task = task_get_id( pcm_start_play_task ); + + if (driver == PCM_DRIVER_SD) { + cfg->pin = luaL_checkinteger( L, 2 ); + MOD_CHECK_ID(sigma_delta, cfg->pin); + + pud->drv = &pcm_drv_sd; + + pud->drv->init( cfg ); + + /* set its metatable */ + lua_pushvalue( L, -1 ); // copy self userdata to the top of stack + luaL_getmetatable( L, "pcm.driver" ); + lua_setmetatable( L, -2 ); + + return 1; + } else { + pud->drv = NULL; + return 0; + } +} + + +static const LUA_REG_TYPE pcm_driver_map[] = { + { LSTRKEY( "play" ), LFUNCVAL( pcm_drv_play ) }, + { LSTRKEY( "pause" ), LFUNCVAL( pcm_drv_pause ) }, + { LSTRKEY( "stop" ), LFUNCVAL( pcm_drv_stop ) }, + { LSTRKEY( "close" ), LFUNCVAL( pcm_drv_close ) }, + { LSTRKEY( "on" ), LFUNCVAL( pcm_drv_on ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( pcm_drv_free ) }, + { LSTRKEY( "__index" ), LROVAL( pcm_driver_map ) }, + { LNILKEY, LNILVAL } +}; + +// Module function map +static const LUA_REG_TYPE pcm_map[] = { + { LSTRKEY( "new" ), LFUNCVAL( pcm_new ) }, + { LSTRKEY( "SD" ), LNUMVAL( PCM_DRIVER_SD ) }, + { LSTRKEY( "RATE_1K" ), LNUMVAL( PCM_RATE_1K ) }, + { LSTRKEY( "RATE_2K" ), LNUMVAL( PCM_RATE_2K ) }, + { LSTRKEY( "RATE_4K" ), LNUMVAL( PCM_RATE_4K ) }, + { LSTRKEY( "RATE_5K" ), LNUMVAL( PCM_RATE_5K ) }, + { LSTRKEY( "RATE_8K" ), LNUMVAL( PCM_RATE_8K ) }, + { LSTRKEY( "RATE_10K" ), LNUMVAL( PCM_RATE_10K ) }, + { LSTRKEY( "RATE_12K" ), LNUMVAL( PCM_RATE_12K ) }, + { LSTRKEY( "RATE_16K" ), LNUMVAL( PCM_RATE_16K ) }, + { LNILKEY, LNILVAL } +}; + +int luaopen_pcm( lua_State *L ) { + luaL_rometatable( L, "pcm.driver", (void *)pcm_driver_map ); // create metatable + return 0; +} + +NODEMCU_MODULE(PCM, "pcm", pcm_map, luaopen_pcm); diff --git a/app/pcm/Makefile b/app/pcm/Makefile new file mode 100644 index 00000000..efa7e3bd --- /dev/null +++ b/app/pcm/Makefile @@ -0,0 +1,47 @@ + +############################################################# +# Required variables for each makefile +# Discard this section from all parent makefiles +# Expected variables (with automatic defaults): +# CSRCS (all "C" files in the dir) +# SUBDIRS (all subdirs with a Makefile) +# GEN_LIBS - list of libs to be generated () +# GEN_IMAGES - list of images to be generated () +# COMPONENTS_xxx - a list of libs/objs in the form +# subdir/lib to be extracted and rolled up into +# a generated lib/image xxx.a () +# +ifndef PDIR +GEN_LIBS = pcm.a +endif + +STD_CFLAGS=-std=gnu11 -Wimplicit + +############################################################# +# Configuration i.e. compile options etc. +# Target specific stuff (defines etc.) goes in here! +# Generally values applying to a tree are captured in the +# makefile at its root level - these are then overridden +# for a subtree within the makefile rooted therein +# +#DEFINES += + +############################################################# +# Recursion Magic - Don't touch this!! +# +# Each subtree potentially has an include directory +# corresponding to the common APIs applicable to modules +# rooted at that subtree. Accordingly, the INCLUDE PATH +# of a module can only contain the include directories up +# its parent path, and not its siblings +# +# Required for each makefile to inherit from the parent +# + +INCLUDES := $(INCLUDES) -I $(PDIR)include +INCLUDES += -I ./ +INCLUDES += -I ../lua +INCLUDES += -I ../libc +INCLUDES += -I ../platform +PDIR := ../$(PDIR) +sinclude $(PDIR)Makefile diff --git a/app/pcm/drv_sigma_delta.c b/app/pcm/drv_sigma_delta.c new file mode 100644 index 00000000..77c3f164 --- /dev/null +++ b/app/pcm/drv_sigma_delta.c @@ -0,0 +1,120 @@ +/* + This file contains the sigma-delta driver implementation. +*/ + +#include "platform.h" +#include "hw_timer.h" +#include "task/task.h" +#include "c_stdlib.h" + +#include "pcm.h" + + +static const os_param_t drv_sd_hw_timer_owner = 0x70636D; // "pcm" + +static void ICACHE_RAM_ATTR drv_sd_timer_isr( os_param_t arg ) +{ + cfg_t *cfg = (cfg_t *)arg; + pcm_buf_t *buf = &(cfg->bufs[cfg->rbuf_idx]); + + if (cfg->isr_throttled) { + return; + } + + if (!buf->empty) { + uint16_t tmp; + // buffer is not empty, continue reading + + tmp = abs((int16_t)(buf->data[buf->rpos]) - 128); + if (tmp > cfg->vu_peak_tmp) { + cfg->vu_peak_tmp = tmp; + } + cfg->vu_samples_tmp++; + if (cfg->vu_samples_tmp >= cfg->vu_req_samples) { + cfg->vu_peak = cfg->vu_peak_tmp; + + task_post_low( cfg->data_vu_task, (os_param_t)cfg ); + + cfg->vu_samples_tmp = 0; + cfg->vu_peak_tmp = 0; + } + + platform_sigma_delta_set_target( buf->data[buf->rpos++] ); + if (buf->rpos >= buf->len) { + // buffer data consumed, request to re-fill it + buf->empty = TRUE; + cfg->fbuf_idx = cfg->rbuf_idx; + task_post_high( cfg->data_play_task, (os_param_t)cfg ); + // switch to next buffer + cfg->rbuf_idx ^= 1; + dbg_platform_gpio_write( PLATFORM_GPIO_LOW ); + } + } else { + // flag ISR throttled + cfg->isr_throttled = 1; + dbg_platform_gpio_write( PLATFORM_GPIO_LOW ); + cfg->fbuf_idx = cfg->rbuf_idx; + task_post_high( cfg->data_play_task, (os_param_t)cfg ); + } + +} + +static uint8_t drv_sd_stop( cfg_t *cfg ) +{ + platform_hw_timer_close( drv_sd_hw_timer_owner ); + + return TRUE; +} + +static uint8_t drv_sd_close( cfg_t *cfg ) +{ + drv_sd_stop( cfg ); + + platform_sigma_delta_close( cfg->pin ); + + dbg_platform_gpio_mode( PLATFORM_GPIO_INPUT, PLATFORM_GPIO_PULLUP ); + + return TRUE; +} + +static uint8_t drv_sd_play( cfg_t *cfg ) +{ + // VU control: derive callback frequency + cfg->vu_req_samples = (uint16_t)((1000000L / (uint32_t)cfg->vu_freq) / (uint32_t)pcm_rate_def[cfg->rate]); + cfg->vu_samples_tmp = 0; + cfg->vu_peak_tmp = 0; + + // (re)start hardware timer ISR to feed the sigma-delta + if (platform_hw_timer_init( drv_sd_hw_timer_owner, FRC1_SOURCE, TRUE )) { + platform_hw_timer_set_func( drv_sd_hw_timer_owner, drv_sd_timer_isr, (os_param_t)cfg ); + platform_hw_timer_arm_us( drv_sd_hw_timer_owner, pcm_rate_def[cfg->rate] ); + + return TRUE; + } else { + return FALSE; + } +} + +static uint8_t drv_sd_init( cfg_t *cfg ) +{ + dbg_platform_gpio_write( PLATFORM_GPIO_HIGH ); + dbg_platform_gpio_mode( PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP ); + + platform_sigma_delta_setup( cfg->pin ); + platform_sigma_delta_set_prescale( 9 ); + + return TRUE; +} + +static uint8_t drv_sd_fail( cfg_t *cfg ) +{ + return FALSE; +} + +const drv_t pcm_drv_sd = { + .init = drv_sd_init, + .close = drv_sd_close, + .play = drv_sd_play, + .record = drv_sd_fail, + .stop = drv_sd_stop +}; diff --git a/app/pcm/pcm.h b/app/pcm/pcm.h new file mode 100644 index 00000000..9d6526b5 --- /dev/null +++ b/app/pcm/pcm.h @@ -0,0 +1,101 @@ + + +#ifndef _PCM_H +#define _PCM_H + + +#include "platform.h" + + +//#define DEBUG_PIN 2 + +#ifdef DEBUG_PIN +# define dbg_platform_gpio_write(level) platform_gpio_write( DEBUG_PIN, level ) +# define dbg_platform_gpio_mode(mode, pull) platform_gpio_mode( DEBUG_PIN, mode, pull) +#else +# define dbg_platform_gpio_write(level) +# define dbg_platform_gpio_mode(mode, pull) +#endif + + +#define BASE_RATE 1000000 + +enum pcm_driver_index { + PCM_DRIVER_SD = 0, + PCM_DRIVER_END = 1 +}; + +enum pcm_rate_index { + PCM_RATE_1K = 0, + PCM_RATE_2K = 1, + PCM_RATE_4K = 2, + PCM_RATE_5K = 3, + PCM_RATE_8K = 4, + PCM_RATE_10K = 5, + PCM_RATE_12K = 6, + PCM_RATE_16K = 7, +}; + +static const uint16_t pcm_rate_def[] = {BASE_RATE / 1000, BASE_RATE / 2000, BASE_RATE / 4000, + BASE_RATE / 5000, BASE_RATE / 8000, BASE_RATE / 10000, + BASE_RATE / 12000, BASE_RATE / 16000}; + +typedef struct { + // available bytes in buffer + size_t len; + // next read position + size_t rpos; + // empty flag + uint8_t empty; + // allocated size + size_t buf_size; + uint8_t *data; +} pcm_buf_t; + +typedef struct { + // selected sample rate + int rate; + // ISR throttle control + // -1 = ISR and data task throttled + // 1 = ISR throttled + // 0 = all running + sint8_t isr_throttled; + // buffer selectors + uint8_t rbuf_idx; // read by ISR + uint8_t fbuf_idx; // fill by data task + // task handles + task_handle_t data_vu_task, data_play_task, start_play_task; + // callback fn refs + int self_ref; + int cb_data_ref, cb_drained_ref, cb_paused_ref, cb_stopped_ref, cb_vu_ref; + // data buffers + pcm_buf_t bufs[2]; + // vu measuring + uint8_t vu_freq; + uint16_t vu_req_samples, vu_samples_tmp; + uint16_t vu_peak_tmp, vu_peak; + // sigma-delta: output pin + int pin; +} cfg_t; + +typedef uint8_t (*drv_fn_t)(cfg_t *); + +typedef struct { + drv_fn_t init; + drv_fn_t close; + drv_fn_t play; + drv_fn_t record; + drv_fn_t stop; +} drv_t; + + +typedef struct { + cfg_t cfg; + const drv_t *drv; +} pud_t; + + +void pcm_data_vu_task( task_param_t param, uint8 prio ); +void pcm_data_play_task( task_param_t param, uint8 prio ); + +#endif /* _PCM_H */ diff --git a/app/pcm/pcm_core.c b/app/pcm/pcm_core.c new file mode 100644 index 00000000..0314c10d --- /dev/null +++ b/app/pcm/pcm_core.c @@ -0,0 +1,107 @@ +/* + This file contains core routines for the PCM sub-system. + + pcm_data_play_task() + Triggered by the driver ISR when further data for play mode is required. + It handles the play buffer allocation and forwards control to 'data' and 'drained' + callbacks in Lua land. + + pcm_data_rec_task() - n/a yet + Triggered by the driver ISR when data for record mode is available. + +*/ + +#include "lauxlib.h" +#include "task/task.h" +#include "c_string.h" +#include "c_stdlib.h" + +#include "pcm.h" + +static void dispatch_callback( lua_State *L, int self_ref, int cb_ref, int returns ) +{ + if (cb_ref != LUA_NOREF) { + lua_rawgeti( L, LUA_REGISTRYINDEX, cb_ref ); + lua_rawgeti( L, LUA_REGISTRYINDEX, self_ref ); + lua_call( L, 1, returns ); + } +} + +void pcm_data_vu_task( task_param_t param, uint8 prio ) +{ + cfg_t *cfg = (cfg_t *)param; + lua_State *L = lua_getstate(); + + if (cfg->cb_vu_ref != LUA_NOREF) { + lua_rawgeti( L, LUA_REGISTRYINDEX, cfg->cb_vu_ref ); + lua_rawgeti( L, LUA_REGISTRYINDEX, cfg->self_ref ); + lua_pushnumber( L, (LUA_NUMBER)(cfg->vu_peak) ); + lua_call( L, 2, 0 ); + } +} + +void pcm_data_play_task( task_param_t param, uint8 prio ) +{ + cfg_t *cfg = (cfg_t *)param; + pcm_buf_t *buf = &(cfg->bufs[cfg->fbuf_idx]); + size_t string_len; + const char *data = NULL; + uint8_t need_pop = FALSE; + lua_State *L = lua_getstate(); + + // retrieve new data from callback + if ((cfg->isr_throttled >= 0) && + (cfg->cb_data_ref != LUA_NOREF)) { + dispatch_callback( L, cfg->self_ref, cfg->cb_data_ref, 1 ); + need_pop = TRUE; + + if (lua_type( L, -1 ) == LUA_TSTRING) { + data = lua_tolstring( L, -1, &string_len ); + if (string_len > buf->buf_size) { + uint8_t *new_data = (uint8_t *) c_malloc( string_len ); + if (new_data) { + if (buf->data) c_free( buf->data ); + buf->buf_size = string_len; + buf->data = new_data; + } + } + } + } + + if (data) { + size_t to_copy = string_len > buf->buf_size ? buf->buf_size : string_len; + c_memcpy( buf->data, data, to_copy ); + + buf->rpos = 0; + buf->len = to_copy; + buf->empty = FALSE; + dbg_platform_gpio_write( PLATFORM_GPIO_HIGH ); + lua_pop( L, 1 ); + + if (cfg->isr_throttled > 0) { + uint8_t other_buf = cfg->fbuf_idx ^ 1; + + if (cfg->bufs[other_buf].empty) { + // rerun data callback to get next buffer chunk + dbg_platform_gpio_write( PLATFORM_GPIO_LOW ); + cfg->fbuf_idx = other_buf; + pcm_data_play_task( param, 0 ); + } + // unthrottle ISR + cfg->isr_throttled = 0; + } + } else { + dbg_platform_gpio_write( PLATFORM_GPIO_HIGH ); + if (need_pop) lua_pop( L, 1 ); + + if (cfg->isr_throttled > 0) { + // ISR found no further data + // this was the last invocation of the reader task, fire drained cb + + cfg->isr_throttled = -1; + + dispatch_callback( L, cfg->self_ref, cfg->cb_drained_ref, 0 ); + } + } + +} diff --git a/app/pcm/pcm_drv.h b/app/pcm/pcm_drv.h new file mode 100644 index 00000000..a5ee935c --- /dev/null +++ b/app/pcm/pcm_drv.h @@ -0,0 +1,9 @@ + +#ifndef _PCM_DRV_H +#define _PCM_DRV_H + + +extern const drv_t pcm_drv_sd; + + +#endif /* _PCM_DRV_H */ diff --git a/app/platform/platform.c b/app/platform/platform.c index 69089480..566a6f22 100755 --- a/app/platform/platform.c +++ b/app/platform/platform.c @@ -612,7 +612,7 @@ void platform_sigma_delta_set_prescale( uint8_t prescale ) sigma_delta_set_prescale_target( prescale, -1 ); } -void platform_sigma_delta_set_target( uint8_t target ) +void ICACHE_RAM_ATTR platform_sigma_delta_set_target( uint8_t target ) { sigma_delta_set_prescale_target( -1, target ); } diff --git a/docs/en/modules/pcm.md b/docs/en/modules/pcm.md new file mode 100644 index 00000000..8491a21e --- /dev/null +++ b/docs/en/modules/pcm.md @@ -0,0 +1,112 @@ +# pcm module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2016-06-05 | [Arnim Läuger](https://github.com/devsaurus) | [Arnim Läuger](https://github.com/devsaurus) | [pcm.c](../../../app/modules/pcm.c)| + +Play sounds through various back-ends. + +## Sigma-Delta hardware + +The ESP contains a sigma-delta generator that can be used to synthesize audio with the help of an external low-pass filter. All regular GPIOs (except GPIO0) are able to output the digital waveform, though there is only one single generator. + +The external filter circuit is shown in the following schematic. Note that the voltage divider resistors limit the output voltage to 1 VPP. This should match most amplifier boards, but cross-checking against your specific configuration is required. + +![low-pass filter](../../img/sigma_delta_audiofilter.png "low-pass filter for sigma-delta driver") + + +!!! note "Note:" + + This driver shares hardware resources with other modules. Thus you can't operate it in parallel to the `sigma delta`, `perf`, or `pwm` modules. They require the sigma-delta generator and the hw_timer, respectively. + + +## Audio format +Audio is expected as a mono raw unsigned 8 bit stream at sample rates between 1 k and 16 k samples per second. Regular WAV files can be converted with OSS tools like [Audacity](http://www.audacityteam.org/) or [SoX](http://sox.sourceforge.net/). Adjust the volume before the conversion. +``` +sox jump.wav -r 8000 -b 8 -c 1 jump_8k.u8 +``` + +Also see [play_file.lua](../../../lua_examples/pcm/play_file.lua) in the examples folder. + +## pcm.new() +Initializes the audio driver. + +### Sigma-Delta driver + +#### Syntax +`pcm.new(pcm.SD, pin)` + +#### Parameters +`pcm.SD` use sigma-delta hardware +- `pin` 1~10, IO index + +#### Returns +Audio driver object. + +# Audio driver sub-module +Each audio driver exhibits the same control methods for playing sounds. + +## pcm.drv:close() +Stops playback and releases the audio hardware. + +#### Syntax +`drv:close()` + +#### Parameters +none + +#### Returns +`nil` + +## pcm.drv:on() +Register callback functions for events. + +#### Syntax +`drv:on(event[, cb_fn[, freq]])` + +#### Parameters +- `event` identifier, one of: + - `data` callback function is supposed to return a string containing the next chunk of data. + - `drained` playback was stopped due to lack of data. The last 2 invocations of the `data` callback didn't provide new chunks in time (intentionally or unintentionally) and the internal buffers were fully consumed. + - `paused` playback was paused by `pcm.drv:pause()`. + - `stopped` playback was stopped by `pcm.drv:stop()`. + - `vu` new peak data, `cb_fn` is triggered `freq` times per second (1 to 200 Hz). +- `cb_fn` callback function for the specified event. Unregisters previous function if omitted. First parameter is `drv`, followed by peak data for `vu` callback. + +#### Returns +`nil` + +## pcm.drv:play() +Starts playback. + +#### Syntax +`drv:play(rate)` + +#### Parameters +`rate` sample rate. Supported are `pcm.RATE_1K`, `pcm.RATE_2K`, `pcm.RATE_4K`, `pcm.RATE_5K`, `pcm.RATE_8K`, `pcm.RATE_10K`, `pcm.RATE_12K`, `pcm.RATE_16K` and defaults to `RATE_8K` if omitted. + +#### Returns +`nil` + +## pcm.drv:pause() +Pauses playback. A call to `drv:play()` will resume from the last position. + +#### Syntax +`drv:pause()` + +#### Parameters +none + +#### Returns +`nil` + +## pcm.drv:stop() +Stops playback and releases buffered chunks. + +#### Syntax +`drv:stop()` + +#### Parameters +none + +#### Returns +`nil` diff --git a/docs/img/sigma_delta_audiofilter.png b/docs/img/sigma_delta_audiofilter.png new file mode 100644 index 00000000..089264ef Binary files /dev/null and b/docs/img/sigma_delta_audiofilter.png differ diff --git a/lua_examples/pcm/jump_8k.u8 b/lua_examples/pcm/jump_8k.u8 new file mode 100644 index 00000000..e7619854 --- /dev/null +++ b/lua_examples/pcm/jump_8k.u8 @@ -0,0 +1 @@ +Rg[h^j_ϸISKXN_IM9?CBK@xc(<5?=@Qׅ9.=5C9Ч#3+:1C/4*.52>1rR!1/588Pw4)92>9ϛ3)8/A.ƹ,,+5/?.{H"0/395Ym3*73<=Ƽǎ6+;2B3®,2.94C1ȼC*367>8c˻d#70:9?Gʽˈ 8.=5D7Ũ)5/;4D3ɾ=,377A7k˻\$72;:=M˽̀ :/=5D:Ǣ&7.<5E270287A6pӾȻ´S*96UɽĶu&=4@:FAºÖ)=4A9H9676?:G9}˽ŸO/::=C?]̿Ʒn(>6A>EHĻŏ)>4B:I:386@:I8̿ŹH19<5B;I>ȼ2>8C=K<ϺùE8=?ɲC0A7H8böߒ(;9/Q՘%-2/>+`6*:4@Ѭ1&5+?,lw4-493ͿC 8*>.WՐ".00=-Y 9-<5Eλˢ1,81A/rȿq"81:<:ȵA'3@=Cѽȸè<0>5G5jƸ{)<8=B<׿ʼM,A5F8XͿɸɑ08<:F8a)A6C=Iӿ˹ǥ:2>7H6qźw)=8@B>ֿ˼ôJ.A6G8]ɹʌ.:<;E9\*A6E8G7sȼo-A:CCC͹´G3D9J;düÄ1?>@H>ϻùY1F:I@Uʺij×:K<~οl/EZʺijœ8iܵ2:7@:XԼ436;9J:-55:@C'62<8M$7.=2Y!7,>.e8+>+r8*?*ׁ7*>+yՎ7+=,lԚ!4+;._ѧ%2-90Sβ+./73J3,247@>*53<;ʿJ(82?7̿V&:2@3ξa$<0B1Ͻn#<0C/Ѿ{#=/B/~ѾЈ#;0A0rϕ%:0@2f̠)82?5Zʫ/44;8PǴ6169:GŽ>/76=@H+85@9ͻ÷Q+<5B7нǷ]*?5E5Ծȸj)@4F5սʸv)A4F5־ʸʂ*@5F6wֿɺɎ+@6E7kɻȚ.=7D8aȽƤ2<8A;Wƿí88:@>N@5;=AFľH2>;CAƼR/?9E<Ⱥ\-A7F9Կɹg+A6F7̶p,B8H9θò|.C9J9zιôÈ0D:I;pͻĶÓ3BG?]º<>?EBUA<@CENƿI:B@GHȾR6C?HC˻[5E=J@λĵe2E=K=иǩF-͵B+@+}˽J(A,rS&A+h]%B,_g$@.Ur$?0M}%=4FտԈ';6>׾Ԓ+899ھԜ05;4ۿҦ51=0Ю:/?.ηB,A,~˿K)B,sT'B+iϾ]'A0\ļh(C2VȻr*B5N̹}+@8GϺ͇.>;Bҹΐ0=><Թ͛49?8պ̤:7B5Իʬ@3D3ԽȴG1E2}ҾźO/F2sX-G2ia,F3`ǿk+E5Xʼu,D8Pͼ~-B9HкΈ.@=Ӻ˕7:@:ʱÞ<9C9˴¨C7F7̷J6I6{˹R4I7sɻ[3K8iƿd2K8a¹m2J:Zĸv2HMʶlj5DAF̶ɑ8BCAζǚ϶ƣ@N>{ŵW;O>sø`:O>jh9O?dp8N@\x9LCV²€:KDPű‰;IFKư‘=GIFDZ™BEJCGCNCNBPBTBQCz\ASBsb@SDli>REer>SF_y?QGY@PJTBNLPDLNLHIOH®LGRG¯QERDVDSC{]ATEtcASElj?SEenARI]wCRKZ~DRMVGQPRJPSPMNTMQMULVJVK[IWJ{aGXItgEXJnnEXKgtFWLb{FVN\GUPXHRRTJPRPNNTNQLUMWLXM~]MZMycL[OthL\OonK[QjuL[Rd{LZS`MYU[NWVXQVXVTTYSWR[Q[QZQ_P[PzJQMVMwiJUQUYJWN]Jc;IGH[,D7K6E28=8^݅!=0B2411;0io 9.=5β(3-<,y\!6/9<ѣ 6*>)J%425Hԑ7*>+<*061W8+>.ʾ0/-9.fm7-<4ͱ(2,<*x["5/8<Ѣ 6*>)K%416Iԑ7*>*Ƚ=,272Wͼ|!9.@1ù322<2hϽl#;1?9ǭ,61?0zн\':4=A̟':0B.ϿN+88:LΎ$<0B0?/5:7ZϿ~#=0A3Ĺ543>4hнn%<1@9ȭ-81@1zѾ\':5=B̟':0B/οM+88:Mώ$4fѻǴk(=4A<0;4C4yսȸ^-=8BEžś,?5F5վɺP1=<>Pȼɍ*@5F6ǼD6;@:]ʺ~*A6F9::9B8kʹn+A7D?é2=6E6z׿ʺ_/?9AGǾƜ-@5G5ֿʻP2==>RɼȌ+A5G5ǽD6:@;^ɺ})B6F99:8C9l˺n+A8D?é3=8E7w˶]1@ɼ??=H>mϺŴo2E=IDƿ7Cʼ>?>G=nκƴn2E>HD8BM@ŴK@CIDbķ|7J@OD÷CDCLCoǶo8JBNI=GAN@}ɴb;IDJP9JAPAȵW>HGHX7K@PBǶMBFJEdŷ}7KBOEøDEBLCpȵo8IBMI=HBNA}ɴb;HELP:JAP@ɵW?HGHY7KAPBƶLAEJGby:LDPGGHGNGpo=NGPMCLFRF}¯eAMJPT@OFTF°ZELMN]>PFTGPGJOKg}>PGTJJKHQHrp?OHRNDMHSHïeAMKQU?NGTFZDLMM^>OFTGPGJOKg|>PGTJIJIQInnBQKTQGPKVK}fFROTYFSKXK\JPQR`DULXMVMPTPi|EULXONPOWNtqFSMWTJRMWL~fGRQUYFSLXM]JRRRaDUMYMUNPTPk{DTMXOOPNVMtqETNVTISLXLdISQUZJUOZO^NUVUdJYQ\RXRUYUl{KYS[USUTZSurLYS\XPXS\RiNWVZ_MYR\RaPVWXeJZR\SZSTYTm|KZS]TSUTZTvqLYT[YOWR\SiNXUY^LYS]R`QWXWdO[U_V\VX\Xm{P]W`YXZX^XwrR^Y`]U\X`XlU]Z_bR]WaWeV\\]iQ^YaX^XZ^[o}R^X`[Y[Y_ZxtS]Y`^U\XaWlT][^cT^XaWdV[\\iR^WaY^Y[^[nyU_[b^\^]b\wtXa^caZ`]d]m[a`bfYc]e]g\`bakXc^f^b__b`r|Xc^e`^a^d^yuYc`dc[b^d^nZb`chXc]f]h\`b`lWc]e^b^`d`r|Yb^e`]`^d_yq[b`dd^daga~o_ddfj_gbibkbeffo_gcicgcegeu}^hdiedediczv_heihafdicpafegk`gcjckcffgp_hcidhedieu}^hdiedechdzv`hdihagcjdqafehkcifkfmgijiqclgmhkijljw}elinjjjjlmnnoppqqrssstuuvvwwxyyyyyyzzz{{{{|{{||{}|}}}~~}}}}~~~~}~~~ \ No newline at end of file diff --git a/lua_examples/pcm/play_file.lua b/lua_examples/pcm/play_file.lua new file mode 100644 index 00000000..f3d47b2a --- /dev/null +++ b/lua_examples/pcm/play_file.lua @@ -0,0 +1,40 @@ +-- **************************************************************************** +-- Play file with pcm module. +-- +-- Upload jump_8k.u8 to spiffs before running this script. +-- +-- **************************************************************************** + + +function cb_drained(d) + print("drained "..node.heap()) + + file.seek("set", 0) + -- uncomment the following line for continuous playback + --d:play(pcm.RATE_8K) +end + +function cb_stopped(d) + print("playback stopped") + file.seek("set", 0) +end + +function cb_paused(d) + print("playback paused") +end + +file.open("jump_8k.u8", "r") + +drv = pcm.new(pcm.SD, 1) + +-- fetch data in chunks of LUA_BUFFERSIZE (1024) from file +drv:on("data", file.read) + +-- get called back when all samples were read from the file +drv:on("drained", cb_drained) + +drv:on("stopped", cb_stopped) +drv:on("paused", cb_paused) + +-- start playback +drv:play(pcm.RATE_8K) diff --git a/lua_examples/pcm/play_network.lua b/lua_examples/pcm/play_network.lua new file mode 100644 index 00000000..4220e921 --- /dev/null +++ b/lua_examples/pcm/play_network.lua @@ -0,0 +1,152 @@ +-- **************************************************************************** +-- Network streaming example +-- +-- stream = require("play_network") +-- stream.init(pin) +-- stream.play(pcm.RATE_8K, ip, port, "/jump_8k.u8", function () print("stream finished") end) +-- +-- Playback can be stopped with stream.stop(). +-- And resources are free'd with stream.close(). +-- +local M, module = {}, ... +_G[module] = M + +local _conn +local _drv +local _buf +local _lower_thresh = 2 +local _upper_thresh = 5 +local _play_cb + + +-- **************************************************************************** +-- PCM +local function stop_stream(cb) + _drv:stop() + if _conn then + _conn:close() + _conn = nil + end + _buf = nil + if cb then cb() + elseif _play_cb then _play_cb() + end + _play_cb = nil +end + +local function cb_drained() + print("drained "..node.heap()) + stop_stream() +end + +local function cb_data() + if #_buf > 0 then + local data = table.remove(_buf, 1) + + if #_buf <= _lower_thresh then + -- unthrottle server to get further data into the buffer + _conn:unhold() + end + return data + end +end + +local _rate +local function start_play() + print("starting playback") + + -- start playback + _drv:play(_rate) +end + + +-- **************************************************************************** +-- Networking functions +-- +local _skip_headers +local _chunk +local _buffering +local function data_received(c, data) + + if _skip_headers then + -- simple logic to filter the HTTP headers + _chunk = _chunk..data + local i, j = string.find(_chunk, '\r\n\r\n') + if i then + _skip_headers = false + data = string.sub(_chunk, j+1, -1) + _chunk = nil + end + end + + if not _skip_headers then + _buf[#_buf+1] = data + + if #_buf > _upper_thresh then + -- throttle server to avoid buffer overrun + c:hold() + if _buffering then + -- buffer got filled, start playback + start_play() + _buffering = false + end + end + end + +end + +local function cb_disconnected() + if _buffering then + -- trigger playback when disconnected but we're still buffering + start_play() + _buffering = false + end +end + +local _path +local function cb_connected(c) + c:send("GET ".._path.." HTTP/1.0\r\nHost: iot.nix.nix\r\n".."Connection: close\r\nAccept: /\r\n\r\n") + _path = nil +end + + +function M.play(rate, ip, port, path, cb) + _skip_headers = true + _chunk = "" + _buffering = true + _buf = {} + _rate = rate + _path = path + _play_cb = cb + + _conn = net.createConnection(net.TCP, false) + _conn:on("receive", data_received) + _conn:on("disconnection", cb_disconnected) + _conn:connect(port, ip, cb_connected) +end + +function M.stop(cb) + stop_stream(cb) +end + +function M.init(pin) + _drv = pcm.new(pcm.SD, pin) + + -- get called back when all samples were read from stream + _drv:on("drained", cb_drained) + _drv:on("data", cb_data) + + --_drv:on("stopped", cb_stopped) +end + +function M.vu(cb, freq) + _drv:on("vu", cb, freq) +end + +function M.close() + stop_stream() + _drv:close() + _drv = nil +end + +return M diff --git a/mkdocs.yml b/mkdocs.yml index 1f1d0d02..716f7d39 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,6 +59,7 @@ pages: - 'net': 'en/modules/net.md' - 'node': 'en/modules/node.md' - 'ow (1-Wire)': 'en/modules/ow.md' + - 'pcm' : 'en/modules/pcm.md' - 'perf': 'en/modules/perf.md' - 'pwm' : 'en/modules/pwm.md' - 'rc' : 'en/modules/rc.md'