Add pcm module. (#1255)
* Add pcm module. * Add network streaming example. * document hw_timer dependency with pwm * Add vu peak callback.
This commit is contained in:
parent
af39a0bc25
commit
d416648990
|
@ -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 \
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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
|
|
@ -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
|
||||
};
|
|
@ -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 */
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
#ifndef _PCM_DRV_H
|
||||
#define _PCM_DRV_H
|
||||
|
||||
|
||||
extern const drv_t pcm_drv_sd;
|
||||
|
||||
|
||||
#endif /* _PCM_DRV_H */
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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 V<sub>PP</sub>. 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`
|
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
File diff suppressed because one or more lines are too long
|
@ -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)
|
|
@ -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
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue