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:
Arnim Läuger 2016-06-05 23:21:36 +02:00 committed by Marcel Stör
parent af39a0bc25
commit d416648990
18 changed files with 961 additions and 2 deletions

View File

@ -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 \

View File

@ -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;

View File

@ -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 */

View File

@ -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

View File

@ -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

263
app/modules/pcm.c Normal file
View File

@ -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);

47
app/pcm/Makefile Normal file
View File

@ -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

120
app/pcm/drv_sigma_delta.c Normal file
View File

@ -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
};

101
app/pcm/pcm.h Normal file
View File

@ -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 */

107
app/pcm/pcm_core.c Normal file
View File

@ -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 );
}
}
}

9
app/pcm/pcm_drv.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef _PCM_DRV_H
#define _PCM_DRV_H
extern const drv_t pcm_drv_sd;
#endif /* _PCM_DRV_H */

View File

@ -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 );
}

112
docs/en/modules/pcm.md Normal file
View File

@ -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&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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -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)

View File

@ -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

View File

@ -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'