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*96>UɽĶ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ư=GIFDZBEJCGCNCNBPBTBQCz\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'