Commit d4166489 authored by Arnim Läuger's avatar Arnim Läuger Committed by Marcel Stör
Browse files

Add pcm module. (#1255)

* Add pcm module.
* Add network streaming example.
* document hw_timer dependency with pwm
* Add vu peak callback.
parent af39a0bc
...@@ -24,6 +24,7 @@ SPECIAL_MKTARGETS=$(APP_MKTARGETS) ...@@ -24,6 +24,7 @@ SPECIAL_MKTARGETS=$(APP_MKTARGETS)
SUBDIRS= \ SUBDIRS= \
user \ user \
driver \ driver \
pcm \
platform \ platform \
libc \ libc \
lua \ lua \
...@@ -69,6 +70,7 @@ LD_FILE = $(LDDIR)/nodemcu.ld ...@@ -69,6 +70,7 @@ LD_FILE = $(LDDIR)/nodemcu.ld
COMPONENTS_eagle.app.v6 = \ COMPONENTS_eagle.app.v6 = \
user/libuser.a \ user/libuser.a \
driver/libdriver.a \ driver/libdriver.a \
pcm/pcm.a \
platform/libplatform.a \ platform/libplatform.a \
task/libtask.a \ task/libtask.a \
libc/liblibc.a \ libc/liblibc.a \
......
#include "platform.h"
#include "driver/sigma_delta.h" #include "driver/sigma_delta.h"
...@@ -18,7 +19,7 @@ void sigma_delta_stop( void ) ...@@ -18,7 +19,7 @@ void sigma_delta_stop( void )
GPIO_SIGMA_DELTA_PRESCALE_SET(0x00) ); 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; uint32_t prescale_mask, target_mask;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "ets_sys.h" #include "ets_sys.h"
#include "osapi.h" #include "osapi.h"
#include "os_type.h" #include "os_type.h"
#include "user_interface.h"
/* use LOW / MEDIUM / HIGH since it isn't clear from the docs which is higher */ /* use LOW / MEDIUM / HIGH since it isn't clear from the docs which is higher */
......
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
#define LUA_USE_MODULES_NET #define LUA_USE_MODULES_NET
#define LUA_USE_MODULES_NODE #define LUA_USE_MODULES_NODE
#define LUA_USE_MODULES_OW #define LUA_USE_MODULES_OW
//#define LUA_USE_MODULES_PCM
//#define LUA_USE_MODULES_PERF //#define LUA_USE_MODULES_PERF
//#define LUA_USE_MODULES_PWM //#define LUA_USE_MODULES_PWM
//#define LUA_USE_MODULES_RC //#define LUA_USE_MODULES_RC
......
...@@ -46,6 +46,7 @@ INCLUDES += -I ../mqtt ...@@ -46,6 +46,7 @@ INCLUDES += -I ../mqtt
INCLUDES += -I ../u8glib INCLUDES += -I ../u8glib
INCLUDES += -I ../ucglib INCLUDES += -I ../ucglib
INCLUDES += -I ../lua INCLUDES += -I ../lua
INCLUDES += -I ../pcm
INCLUDES += -I ../platform INCLUDES += -I ../platform
INCLUDES += -I ../spiffs INCLUDES += -I ../spiffs
INCLUDES += -I ../smart INCLUDES += -I ../smart
......
// 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);
#############################################################
# 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
/*
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
};
#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 */
/*
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 );
}
}
}
#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 ) ...@@ -612,7 +612,7 @@ void platform_sigma_delta_set_prescale( uint8_t prescale )
sigma_delta_set_prescale_target( prescale, -1 ); 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 ); sigma_delta_set_prescale_target( -1, target );
} }
......
# 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&nbsp;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&nbsp;bit stream at sample rates between 1&nbsp;k and 16&nbsp;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`
亐亐亐亐亐亐亐亐亐亐囼黩腙噼疪g[h^j_面刭酉细ISKXN_I屙噤儋M9?CBK@x褛湓菔躢(<5?=@Q葳垩浊讌9.=5C9类杂惺效#3+:1C/涚巫宋屏4*.52>1r嫖偕依蠷!1/588P垩质依觲4)92>9蜜椅闻蠜3)8/A.握仕乒,,+5/?.{缣兹狭蘃"0/395Y咝咨欣襪3*73<=挛膳萍菐6+;2B3ω撬拿庐,2.94C1勣蒲萌寄C*367>8c廴颐嘶蚫#70:9?G滔吻式藞 8.=5D7柏仕敲浓)5/;4D3庍切蒙纠=,377A7k萸衙嘶薥$72;:=M型衅私蛝 :/=5D:钢趟攘洽&7.<5E2樲刃那阑70287A6p泳然麓罶*96<?>U棠山亩舥&=4@:FA何琶潞脰)=4A9H9炚氯懒奖676?:G9}亓私鸥翺/::=C?]遗炭品莕(>6A>EH了嚷幕艔)>4B:I:υ徘量精386@:I8呚绿颗箍H19<<D<e漳叹欧莊*=7A?DK剖闪乒茋(>5B;I>θ冀苟笩2>8C=K<娤好菇档E8<A@H@m谓鸥辣繾1A=DDGT怕暮来羱.D;F@KD笔晾靖綔2A:F?M=斝灸航覆A:<CAK?v呀乒脸繷3@?DFE[柿呕脸聎.C<EBKH啡亮几細'n酱吵g&;4>=?澤簿昂0A7H8b棵缎羞(;9<D7烄衍行蚔$=/A5Q提呜抛*.61A.逖倚且h8-97=躲首囊&7,@,f壅我轮3/4<2濆稍翘肏8*>/Q哇收琳%-2/>+呬鸵似蝋6*:4@横芍卵&5+?,l芤涡略w4-493ュ芍仆緾 8*>.W胸嗽琳".00=-嗁蒙晾肶 9-<5E鹤挛凰,81A/r姿扇刻q"81:<:к眯廊礎'</B2^弦俏较&557A3戄仆哪肬#<0@6L磷男轿/83C1z谏巳克k":2=;?男渴=*;0C2e有忍轿%658?5椶畔闷罰$=/A5Q胖畔嘉-184B2|欣每沽c%>3@=C饺该<0>5G5j吻缕溉{)<8=B<涀渴祭稭,A5F8X猛可干8<:F8喼燎两耡)A6C=I从克骨:2>7H6q亚呐喝w)=8@B>≈克济碕.A6G8]忍辽故.:<;E9嵶寥揽繺*A6E<N寡克干4>8G7s燃蓟簿o-A:CCC⑼孤淳狦3D9J;d妹剂疵?>@H>愊幻购竃1F:I@U故耗趁:<B>K<~慰两防l/E<FEI┪耗独〧6E;L<j饶玖的1A?BI@栄荒辜礥1F;K>Z际耗撑<B?K<傃计放G;L<x霞欧膼3D<J=l途酶脷5D=I>i檑嘤艿2:7@:X拶傺约436;9J屹匝蚊:-55:@熔幸适C'62<8晦逃峭M$7.=2苏囊Y!7,>.b烧抡e8+>+曚芍轮r8*?*囧芍伦7*>+y闶置諑7+=,l崴峙詺!4+;._萃悠学%2-90S匦由尾+./73J延亚来3,247@牢破澜>*53<;芬剖颗J(82?7盘渴V&:2@3Y奈就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突亩脫3B<H=f示梅罎6A>G?]评潞喀<>?EBU侣阑籍A<@CEN黄烤憾I:B@GH踩究够R6C?HC涣防[5E=J@犖荒得e2E=K=曅盖〧<J;嬓古疜:J:佄郝礡7K8w私久a0N4x幺刳e+I1h挹臆l&C0Z折誊t#?0O梯绍~"<0E罗擒$82<膏能&546嫩)070⑩刨-:,椼抛);)嬧圃=&;'嗌崖F#=&s菟紊P!='h傧涛Z=']砸视e=)S握侵p;+K琴刨| 9.A拒内!70:缔纶%335ふ合+161涁幌/;.捹疚->-堎劳礏+@+}倜私J(A,r峙擅S&A+h由剖]%B,_翁梦g$@.U刃卵r$?0M掠烙}%=4F赫吭';6>弊驹+899ㄚ驹5;4澺恳1=0撣佬:/?.囓挛稡,A,~谀丝K)B,s智赡T'B+i暇嚼]'A0\拍寂h(C2V廊簧r*B5N惶固}+@8G聪和.>;B刮=><ぴ雇9?8浾禾:7B5懺皇珸3D3喸饺碐1E2}揖藕O/F2s新美X-G2i膛僚a,F3`惹咳k+E5X檬妓u,D8P纪纪~-B9H葱何.@<B何<>=び核:@:撌泵<9C9屗绰–7F7勌妨甁6I6{斯康R4I7s苫竞[3K8i瓶豢d2K8a谅孤m2J:Z寄概v2H<S非非2F>M笆肚DAF┨渡BCA犖肚<?D>椣镀<H;幬概狥:H9喭姑盠8J9{毯婪T5J8r示窘]3K8j评焕d3J:_饭焙l4J<Y醇苯u6J@T袄袄~8ICO袄:HEJD傲=GGG溒袄桞CJC斍笨烢ALA屓部@M?吳臣璔>N>{诺翰W;O>s酶阜`:O>j炕痘h9O?d冀唇p8N@\防蠢x9LCV甭猜:KDP甭;IFKて奥=GIF溓甭橞EJC捊ǘ欸CNC埦燦BPB偪BQCz景串\ASBs疾潮b@SDl沟钡i>REe捣r>SF_焙y?QGY丂PJT圔NLP牽怐LNL毩桯IOH懧濴GRG娐ERD兝胺猇DSC{勘诞]ATEt匠吵cASEl沟捕j?SEe贩北nARI]ūケwCRKZΤΤ~DRMV》Т嘒QPR浌У岼PSP暪У擬NTM徆ǔ歈MUL埡┏燰JVK伖IWJ{弓蔼aGXIt动gEXJn窗nEXKg安tFWLb┑{FVN\ǘ┒丟UPX7ǖ塇RRT湽ǘ嶫PRP柣ǖ朜NTN徎┐決LUM偘々橶LXM~埃]MZMy唉─cL[OtЖhL\OoΚnK[QjΝuL[RdМく{LZS`ぐ侻YU[灢ぐ圢WVX櫝け峇VXV挸く揟TYS嵉く榃R[Q嚦キ漑QZQ伌ЙP[Pz触瘷櫒、JQMVMw阿獮﹊JUQUYかē☉JWN]J滹蓍c;IGH[玎阏銧,D7K6榆弦E28=8^阋凵輩!=0B2扁嫌土411;0i涮啬賝 9.=5口型尾(3-<,y渖致誠!6/9<嗽疑眩 6*>)嬨烧眯J%425H招云詰7*>+濃视迫<*061W芴致8+>.绦示0/-9.f馍茁譵7-<4截咸捅(2,<*x闵至訹"5/8<试焉癣 6*>)嬨日男K%416I韵云詰7*>*溹廊娇=,272W幽图蝲!9.@1ㄕ湃霉322<2h诿辖衛#;1?9秆善黔,61?0z苊薪蝄':4=A盼嗣虩':0B.娷孟渴N+88:L鲜卫螏$<0B0氌磐撩?/5:7Z智峡褈#=0A3鞘墓543>4h勰薪裯%<1@9挂是拳-81@1z菽丫蝄':5=B磐嗣虩':0B/嬢奈克M+88:M鲜侠蠋$</C0涆盘撩A06<6Z浊峡衺$=1B3鞘墓543>4f鸦谴莐(=4A<词量璃0;4C4y战雀荿-=8BE寥啪艣,?5F5娬旧耗P1=<>P逝燃蓫*@5F6氂狼季D6;@:]蚜屎蕕*A6F9ㄐ履来::9B8k绽使薾+A7D?端怕茅2=6E6z卓屎蒧/?9AG氯蔷茰-@5G5娭渴慌P2==>R逝杉葘+A5G5氂狼浇D6:@;^蚜珊蘿)B6F9┬媚赖9:8C9l岳撕蘮+A8D?短怕茅3=8E7w硕腊繻1@<DI估糠緱2C:I9壩孤淳R6AABT木露聤/E:K;樚豪父H;?D@_式牡舼0F;J>ι季话??=H>m虾糯舘2E=ID财拷茎7C<I<{虾诺腵4E@GK铰梁翙3D;K:嬒荒独S7BBCU趴姆膵0F;K<櫷宦父H<@E@a思诺膤0F<J>祭话>?>G=n魏拼苙2E>HD称兰睛8B<J;|泻糯胈5D?GK韭凉聵4D;J;嬒汗R;BCDV欢话粓4H>M@暸春辈K@CIDb姆桨緗7J@OD⒚饭东CDCLCo嵌堪縪8JBNI狗埂=GAN@}纱颈綽;IDJP辜嫉紩9JAPA娙稻埠W>HGHX抗境綁7K@PB椙都炒MBFJEd欧堪縸7KBOEC腹番DEBLCp鹊腊纎8IBMI悍浮=HBNA}纱勘絙;HELP菇嫉紬:JAP@娚稻补W?HGHY亢匠緤7KAPB樒痘创LAEJGb划旦秠:LDPG灮安GHGNGp喇釜竜=NGPM巢碀CLFRF}炉宫積AMJPT撤栋稉@OFTF娐案ZELMN]捍腑箞>PFTG柨捕PGJOKg坎弓箎>PGTJ〖泊钵JKHQHr帘宫筽?OHRN当礉DMHSH茂宫竐AMKQU撤钒窊?NGTF嬃案ZDLMM^勾府箞>OFTG柪卜PGJOKg砍宫簗>PGTJ⒔炒宝IJIQIn法痭BQKTQけ瑱GPKVK}憨钵癴FROTY鲍皯FSKXK埡ó\JPQR`疮畅硣DULXM敼VMPTPi弓揣磡EULXO灧NPOWNt滑揣磓FSMWTТ瘹JRMWL~滑敞瞗GRQUY鲍睉FSLXM壔┋]JRRRa蛋唱矆DUMYM敽UNPTPk弓川硔DTMXO煼OPNVMt棘川硄ETNVTТ瘹ISLXL棘矠ISQUZΘá▽JUOZO嚦かⅷ^NUVUd瓍JYQ\R懖ЙイXRUYUl钵畕KYS[U洷īSUTZSu揣璻LYS\XぎPXS\R郸琲NWVZ_瑥MYR\R壍КぉaPVWXe瓎JZR\S挸ЙΕZSTYTm敞畖KZS]T洷┅SUTZTv川畄LYT[Yく獥OWR\S郸琲NXUY^珟LYS]R壍Лエ`QWXWdO[U_V崻牐灋\VX\XmP]W`Y槳¥XZX^Xw╮R^Y`]煣ぃU\X`XU]Z_bウΑR]WaW壆ⅷ。eV\\]i▍Q^YaX懎%^XZ^[o﹠R^X`[槴いY[Y_Zx﹖S]Y`^煩ィU\XaWT][^cΖАT^XaW壆ⅶ牑dV[\\i━▌R^WaY懎う^Y[^[n灄焬U_[b^摙湝洈\^]b\wXa^ca櫋灊煈Z`]d]爉[a`bf煛牅爥Yc]e]嚙湣洘g\`bakXc^f^彟灎湜b__b`rXc^e`暏灎潠^a^d^yYc`dc洟牉煈[b^d^爊Zb`ch牋Xc]f]嚙潰洘h\`b`lWc]e^彟潫潥b^`d`rYb^e`暏煙潠]`^d_y榪[b`dd摍棖棆^daga~煐檽歰_ddfj櫃殩殗_gbib厾棝枡kbeffo潥湒潅_gcic崰槢棔gcegeu牁潠潁^hdie挐殮檽dedicz牁湒漹_heih棟洐泿afdic牁潠沺afegk殰湕泩`gcjc啞槣棛kcffgp灇潡渻_hcid崰殯棖hedieu牂湒潁^hdie挒殮殥dechdz潡渧`hdih枩洐殠agcjd仭槣棝qafehk憫拸拑cifkf儤憯悞mgijiq枔枒晛clgmh姍摃拺kijljw檽枒杴elinj彉晹搷jjjlmnnoppqqrssstuuvvwwxyyyyyyzzz{{{{|{{||{}|}}}~~}}}}~~~~}~~~亐亐
\ No newline at end of file
-- ****************************************************************************
-- 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)
-- ****************************************************************************
-- 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: ...@@ -59,6 +59,7 @@ pages:
- 'net': 'en/modules/net.md' - 'net': 'en/modules/net.md'
- 'node': 'en/modules/node.md' - 'node': 'en/modules/node.md'
- 'ow (1-Wire)': 'en/modules/ow.md' - 'ow (1-Wire)': 'en/modules/ow.md'
- 'pcm' : 'en/modules/pcm.md'
- 'perf': 'en/modules/perf.md' - 'perf': 'en/modules/perf.md'
- 'pwm' : 'en/modules/pwm.md' - 'pwm' : 'en/modules/pwm.md'
- 'rc' : 'en/modules/rc.md' - 'rc' : 'en/modules/rc.md'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment