From ceb3f8443aa46d60a8689a20033e11a074d9e485 Mon Sep 17 00:00:00 2001 From: Benjamin Valentin Date: Sun, 19 Apr 2020 16:48:04 +0200 Subject: [PATCH 1/2] drivers/dac_dds: add module to play sample buffer over a DAC This adds an API function to play a buffer of Audio samples using a DAC. Double buffered operation is supported by specifying a callback that will be called when the next buffer can be queued with dac_dds_play(). --- drivers/dac_dds/Makefile | 1 + drivers/dac_dds/Makefile.dep | 2 + drivers/dac_dds/Makefile.include | 2 + drivers/dac_dds/dac_dds.c | 143 ++++++++++++++++++++ drivers/dac_dds/include/dac_dds_params.h | 70 ++++++++++ drivers/include/dac_dds.h | 164 +++++++++++++++++++++++ 6 files changed, 382 insertions(+) create mode 100644 drivers/dac_dds/Makefile create mode 100644 drivers/dac_dds/Makefile.dep create mode 100644 drivers/dac_dds/Makefile.include create mode 100644 drivers/dac_dds/dac_dds.c create mode 100644 drivers/dac_dds/include/dac_dds_params.h create mode 100644 drivers/include/dac_dds.h diff --git a/drivers/dac_dds/Makefile b/drivers/dac_dds/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/dac_dds/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/dac_dds/Makefile.dep b/drivers/dac_dds/Makefile.dep new file mode 100644 index 0000000000..6b811b71f3 --- /dev/null +++ b/drivers/dac_dds/Makefile.dep @@ -0,0 +1,2 @@ +FEATURES_REQUIRED += periph_dac +FEATURES_REQUIRED += periph_timer_periodic diff --git a/drivers/dac_dds/Makefile.include b/drivers/dac_dds/Makefile.include new file mode 100644 index 0000000000..50b100afd7 --- /dev/null +++ b/drivers/dac_dds/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_dac_dds := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_dac_dds) diff --git a/drivers/dac_dds/dac_dds.c b/drivers/dac_dds/dac_dds.c new file mode 100644 index 0000000000..0a48143187 --- /dev/null +++ b/drivers/dac_dds/dac_dds.c @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_periph_dac + * @{ + * + * @file + * @brief Common DAC function fallback implementations + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include "board.h" +#include "dac_dds.h" +#include "dac_dds_params.h" +#include "irq.h" +#include "kernel_defines.h" +#include "macros/units.h" +#include "periph/timer.h" + +static struct dac_ctx { + const uint8_t *buffers[2]; /* The two sample buffers */ + size_t buffer_len[2]; /* Size of the sample buffers */ + size_t idx; /* Current position in the current buffer */ + dac_dds_cb_t cb; /* Called when the current buffer is done */ + void *cb_arg; /* Callback argument */ + uint16_t sample_ticks; /* Timer ticks per sample */ + uint8_t cur; /* Active sample buffer */ + uint8_t playing; /* DAC is playing */ + uint8_t is_16bit; /* Sample size is 16 instead of 8 bit */ +} _ctx[DAC_DDS_NUMOF]; + +static void _timer_cb(void *arg, int chan) +{ + (void)chan; + + struct dac_ctx *ctx = arg; + + dac_dds_t dac_dds = ctx - _ctx; + dac_t dac = dac_dds_params[dac_dds].dac; + const uint8_t cur = ctx->cur; + const uint8_t *buf = ctx->buffers[cur]; + const size_t len = ctx->buffer_len[cur]; + + if (ctx->is_16bit) { + uint8_t l = buf[ctx->idx++]; + uint8_t h = buf[ctx->idx++]; + + dac_set(dac, (h << 8) | l); + } else { + dac_set(dac, buf[ctx->idx++] << 8); + } + + if (ctx->idx >= len) { + + /* invalidate old buffer */ + ctx->buffer_len[cur] = 0; + + ctx->idx = 0; + ctx->cur = !cur; + + /* stop playing if no more samples are queued */ + if (ctx->buffer_len[!cur] == 0) { + ctx->playing = 0; + timer_stop(dac_dds_params[dac_dds].timer); + /* notify user that next sample buffer can be queued */ + } else if (ctx->cb) { + ctx->cb(ctx->cb_arg); + } + } +} + +void dac_dds_init(dac_dds_t dac, uint16_t sample_rate, uint8_t flags, + dac_dds_cb_t cb, void *cb_arg) +{ + assert(dac < DAC_DDS_NUMOF); + + _ctx[dac].cb = cb; + _ctx[dac].cb_arg = cb_arg; + _ctx[dac].sample_ticks = dac_dds_params[dac].timer_hz / sample_rate; + _ctx[dac].is_16bit = flags & DAC_FLAG_16BIT; + + timer_init(dac_dds_params[dac].timer, dac_dds_params[dac].timer_hz, _timer_cb, &_ctx[dac]); +} + +void dac_dds_set_cb(dac_dds_t dac, dac_dds_cb_t cb, void *cb_arg) +{ + unsigned state = irq_disable(); + + /* allow to update cb_arg independent of cb */ + if (cb || cb_arg == NULL) { + _ctx[dac].cb = cb; + } + _ctx[dac].cb_arg = cb_arg; + + irq_restore(state); +} + +bool dac_dds_play(dac_dds_t dac, const void *buf, size_t len) +{ + struct dac_ctx *ctx = &_ctx[dac]; + + unsigned state = irq_disable(); + + uint8_t next = !ctx->cur; + ctx->buffers[next] = buf; + ctx->buffer_len[next] = len; + + bool is_playing = ctx->playing; + irq_restore(state); + + if (is_playing) { + return true; + } + + ctx->playing = 1; + ctx->cur = next; + + timer_set_periodic(dac_dds_params[dac].timer, 0, ctx->sample_ticks, + TIM_FLAG_RESET_ON_MATCH | TIM_FLAG_RESET_ON_SET); + + /* We can already queue the next buffer */ + if (ctx->cb) { + ctx->cb(ctx->cb_arg); + } + + return false; +} + +void dac_dds_stop(dac_dds_t dac) +{ + timer_stop(dac_dds_params[dac].timer); + _ctx[dac].playing = 0; +} diff --git a/drivers/dac_dds/include/dac_dds_params.h b/drivers/dac_dds/include/dac_dds_params.h new file mode 100644 index 0000000000..dc0db57fc0 --- /dev/null +++ b/drivers/dac_dds/include/dac_dds_params.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_periph_dac + * @{ + * + * @file + * @brief Default configuration for the DAC DDS driver + * + * @author Benjamin Valentin + * @} + */ + +#ifndef DAC_DDS_PARAMS_H +#define DAC_DDS_PARAMS_H + +#include "board.h" +#include "macros/units.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters for the DAC DDS driver + * @{ + */ +#ifndef DAC_DDS_PARAM_DAC +#define DAC_DDS_PARAM_DAC DAC_LINE(0) +#endif +#ifndef DAC_DDS_PARAM_TIMER +#define DAC_DDS_PARAM_TIMER (TIMER_NUMOF - 1) +#endif +#ifndef DAC_DDS_PARAM_TIMER_HZ +#define DAC_DDS_PARAM_TIMER_HZ MHZ(1) +#endif + +#ifndef DAC_DDS_PARAMS +#define DAC_DDS_PARAMS { .dac = DAC_DDS_PARAM_DAC, \ + .timer = DAC_DDS_PARAM_TIMER, \ + .timer_hz = DAC_DDS_PARAM_TIMER_HZ, \ + } +#endif +/**@}*/ + +/** + * @brief DAC DDS configuration + */ +static const dac_dds_params_t dac_dds_params[] = +{ + DAC_DDS_PARAMS +}; + +/** + * @brief DAC DDS instances + */ +#define DAC_DDS_NUMOF ARRAY_SIZE(dac_dds_params) + +#ifdef __cplusplus +} +#endif + +#endif /* DAC_DDS_PARAMS_H */ +/** @} */ diff --git a/drivers/include/dac_dds.h b/drivers/include/dac_dds.h new file mode 100644 index 0000000000..688e51c4a4 --- /dev/null +++ b/drivers/include/dac_dds.h @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup drivers_dac_dds DAC Direct Digital Synthesis + * @ingroup drivers_periph_dac + * + * @{ + * @file + * @brief Use a DAC to play a buffer of samples + * + * A buffer of (audio) samples can be played on a DAC with + * a given sample rate. + * To supply a steady stream of samples, a double buffer is + * used. + * While buffer A is played on the DAC the user can fill buffer + * B with the next batch of samples by calling @ref dac_dds_play + * again. Once buffer A has finished playing, buffer B will + * automatically be used instead. + * + * A callback can be registered that signals when the next buffer + * is ready to be filled. + * Do not do the actual buffer filling inside the callback as this + * is called from the timer context that is used to play the samples. + * Instead, use it to wake a thread that then provides the samples + * and calls @ref dac_dds_play. + * If the next sample buffer is already prepared, you can also call + * `dac_dds_play` within the callback, just don't do any I/O or + * heavy processing. + * + * @author Benjamin Valentin + */ + +#ifndef DAC_DDS_H +#define DAC_DDS_H + +#include +#include +#include + +#include "periph/dac.h" +#include "periph/timer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The callback that will be called when the end of the current sample buffer + * has been reached. + * Should be used to start filling the next sample buffer with @ref dac_dds_play. + * + * @note Will be called in interrupt context. Only use the callback to signal a + * thread. Don't directly fill the sample buffer in the callback. + */ +typedef void (*dac_dds_cb_t)(void *arg); + +/** + * @brief Configuration struct for a DAC DDS channel + * @{ + */ +typedef struct { + dac_t dac; /**< The DAC used for playing the sample */ + tim_t timer; /**< Timer used for periodic DAC events */ + uint32_t timer_hz; /**< Timer frequency, must be at least 2x sample rate */ +} dac_dds_params_t; +/** @} */ + +/** + * @brief Index of the DAC DDS channel + */ +typedef uint8_t dac_dds_t; + +/** + * @brief A sample has a resolution of 8 bit + */ +#ifndef DAC_FLAG_8BIT +#define DAC_FLAG_8BIT (0x0) +#endif + +/** + * @brief A sample has a resolution of 16 bit + */ +#ifndef DAC_FLAG_16BIT +#define DAC_FLAG_16BIT (0x1) +#endif + +/** + * @brief Initialize a DAC for playing audio samples + * A user defined callback can be provided that will be called when + * the next buffer can be queued. + * @experimental + * + * @param[in] dac The DAC to initialize + * @param[in] sample_rate The sample rate in Hz + * @param[in] flags Optional flags (@ref DAC_FLAG_16BIT) + * @param[in] cb Will be called when the next buffer can be queued + * @param[in] cb_arg Callback argument + */ +void dac_dds_init(dac_dds_t dac, uint16_t sample_rate, uint8_t flags, + dac_dds_cb_t cb, void *cb_arg); + +/** + * @brief Change the 'buffer done' callback. + * A user defined callback can be provided that will be called when + * the next buffer can be queued. + * This function can be used to change the callback on the fly. + * + * Passing in a @p cb of `NULL` can be used to only update the arg + * without updating the @p cb. Conversely, to clear the callback, both + * @p cb and @p cb_arg need to be passed in as NULL. + * @experimental + * + * @param[in] dac The DAC to configure + * @param[in] cb Called when the played buffer is done + * @param[in] cb_arg Callback argument + */ +void dac_dds_set_cb(dac_dds_t dac, dac_dds_cb_t cb, void *cb_arg); + +/** + * @brief Play a buffer of (audio) samples on a DAC + * + * If this function is called while another buffer is already + * being played, the new `buf` will be played when the current + * buffer has finished playing. + * + * The DAC implementations allows one buffer to be queued + * (double buffering). + * + * Whenever a new buffer can be queued, the @ref dac_dds_cb_t + * callback function will be executed. + * + * @experimental + * + * @param[in] dac The DAC to play the sample on + * @param[in] buf A buffer with (audio) samples + * @param[in] len Number of bytes to be played + * + * @return `true` if the buffer was queued while another + * buffer is currently playing. + * `false` if the new buffer is played immediately. + * That means playing was just started or an underrun + * occurred. + */ +bool dac_dds_play(dac_dds_t dac, const void *buf, size_t len); + +/** + * @brief Stop playback of the current sample buffer + * + * @param[in] dac The DAC to stop + */ +void dac_dds_stop(dac_dds_t dac); + +#ifdef __cplusplus +} +#endif + +#endif /* DAC_DDS_H */ +/** @} */ From 5681befe751ab10a9a7a5246e28d4fbd582a78c4 Mon Sep 17 00:00:00 2001 From: Benjamin Valentin Date: Sun, 19 Apr 2020 16:26:38 +0200 Subject: [PATCH 2/2] tests/driver_dac_dds: add test for dac_dds_play() This will play a raw audio (8kHz, 8 Bit) snipped from the 1961 synthesized speech demo. (Can be disabled since the file is rather large with 17k) It will also play sine, square and sawtooth waves with frequencies that can be set on the command line. I tested this with the on-board speaker (connected to P0.26) of the mcb2388 as well as with hooking up a headphone to the ANALOG (PA02) header of the same54-xpro. --- tests/driver_dac_dds/Makefile | 15 ++ tests/driver_dac_dds/hello.raw | 1 + tests/driver_dac_dds/main.c | 323 +++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 tests/driver_dac_dds/Makefile create mode 100644 tests/driver_dac_dds/hello.raw create mode 100644 tests/driver_dac_dds/main.c diff --git a/tests/driver_dac_dds/Makefile b/tests/driver_dac_dds/Makefile new file mode 100644 index 0000000000..f1903bb91f --- /dev/null +++ b/tests/driver_dac_dds/Makefile @@ -0,0 +1,15 @@ +BOARD ?= mcb2388 + +include ../Makefile.tests_common + +BLOBS += hello.raw + +USEMODULE += dac_dds +USEMODULE += shell + +# Disable the greeting sample if the board has too little memory +# or flashing takes too long +ENABLE_GREETING ?= 1 +CFLAGS += -DENABLE_GREETING=$(ENABLE_GREETING) + +include $(RIOTBASE)/Makefile.include diff --git a/tests/driver_dac_dds/hello.raw b/tests/driver_dac_dds/hello.raw new file mode 100644 index 0000000000..1cc33fbf70 --- /dev/null +++ b/tests/driver_dac_dds/hello.raw @@ -0,0 +1 @@ +†Š‰Š‰Šˆ‰ˆ‰ˆˆ‰ˆˆˆ‰ˆ‰ˆˆˆ‰ˆ‰ˆˆ‡ˆˆ‡ˆˆ‡ˆˆ‡†‡‡†‡†‡†‡†‡†‡†‡†‡†‡†‡†…†…†…†…†………††…†‡†‡†‡†…‡††‡†‡†…†…†‡†‡††††‡†‡†††……†……†…†……„…„……„†……†……„…„…„…„…„…„„„ƒ„ƒ„ƒƒƒ„ƒ„ƒƒ„ƒ„ƒ„ƒƒ„ƒ„„ƒ„„„…„…„…„…„„…„…„ƒ„„ƒƒ„ƒ„ƒ„ƒ„„„ƒ„ƒ‚‚ƒƒƒ‚ƒ‚ƒƒƒ„ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ„ƒƒ„ƒ‚ƒ‚ƒ‚‚ƒ„ƒ„ƒ„ƒƒ„ƒƒƒ‚ƒ‚ƒƒ„ƒƒ‚ƒƒ„ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚‚‚‚ƒ‚‚ƒ‚ƒ‚ƒ„ƒƒ‚ƒ‚ƒ‚ƒƒ‚ƒƒ‚ƒƒ‚ƒ‚‚‚‚‚‚‚‚‚‚‚‚‚‚‚‚€‚‚‚‚‚‚€€€€‚€€€€‚‚‚‚‚‚‚ƒ‚ƒ‚ƒ‚‚‚ƒ‚ƒ‚ƒ‚‚ƒ‚ƒ‚ƒ‚ƒ‚ƒƒƒ„ƒƒ„ƒ„ƒ„ƒ„ƒ„ƒ„„ƒ„ƒ„„„„ƒƒ„„„ƒ„ƒ„ƒ„„ƒ„ƒ„ƒƒ„ƒ„……„„…„……„………„…„…†…††…†…†‡†‡†‡†‡‡†…†…†……‡†‡†…†…††‡†……†‡†‡‡†‡……‡ˆ††‡ˆ†„‡‰‡…‰ˆ„‰‰ƒ†Š…„‡……†~ŠŒ††‡‰‰~„”‡‹Š}„‡‡„€Ž‚ƒ‰‹ˆw}‘†y‹ƒ}Ž—vh˜—q…uŽ–fˆ¦lk©b†Š}‚…‘}y•…}v”™nx¡‚hŠ•ƒ…†z’kzš{}“t†šr‚¡tn‡vŽ‚‘}~‹‚ˆ‹†x€¦Q{ÀyQ¢¨g›„}¤’d¢fr ‹wŠ“€yŽ‚}—…i—¨po’—ƒxˆ‹€sŸ…i„­X˜£[r¨‹u‰v‹–xh}z‰‰v€ŸŒto‹žzp…™ž`‡±|M‚¶Šb|•Œf€¯ŒgqˆŸ‡[‡²n`ª«fl¢”vk€Ÿ‡lz‘‘{k£“K|¹tL ­`wªzl“¨yY£¡dv•‰ˆ|e‘©l^™§†mq˜¦oN‹©‚foœ¥sa©Pt­‡^x£¦z\‡¥€al™¡qa•¦~l|£ŠT„ªwX‘­sb•£va±qL ºlZŽŽ‡s|œ‚l”¡ta‰§}aªxb˜¡sjБނ\‹¼qLŸ²ch¡ŠoŽŠ|˜‰„˜}qŠo‚–Š…ƒŸ€`”§jh£pr•‰n~¡un¨ž]o£‹n”‰…}{•—vh—žpx‰ˆ†{’‡ywŒ’m¦udŸ™dt›“…oo¤®cTŸ¤nk¡]f³™Ts©˜x~ƒ•–qyŒˆ‚i~§…ZŒ¸_—“}“~h’¤ih«šg‚”’io²“O‹®u‡•o¢„g‰¨{l£—Y~´ƒk‰–…sƒ“…s†“„„~|z”u‰‰~y’…kŠœyo˜qvšƒvˆŠ„‚‚~ˆ‰lTJnÊÎŽ¨À¡†pil_?I{‚pН¯£œŒƒˆuVd~or‘Ÿ—Ÿ ’•€lq{{rr‹™„~œš|‰y|†og‹v{’Š…}‚‰rfaCU¡á“Ï»™…xt[Dd“‚n£Ì·£•”uIR|q_z£­««¨­¡}jy\]wˆ‹Ž–§¨•†‰ˆtfluxx{‡““Љ‰xF<_»Ñi•˨’|xhD?l‘rn°Ôµž›“lAR~l^}ªµ¨¬´´¡{p…wX_}“ °°œŒselwvv}Ž˜–Š…^;\¹Õn‰Æ§‘{ykE=h”qg­Ï²œš–Žh@V€j_€¬´¦©¯¯œwn„tZb€‘Ÿ¬ª–ŠŽ‡ofowyy€Ž™’‰ˆk:K—̈u¿³”†{zlD5\ŽvbžÊ»ž˜•n@O|l]z£±£§­®™vp…vXd‹‘ž¬¦’‡Ž…mepxwz€’•†„Z;]³ÏgŒÅ¢{v`6=oer¸Æ§˜–’‡]Ab|fh‹«¬¤§®¨sykap‡Ž“£©œŒ‹Š|mkw{|}Œ“’†ƒ]Ba¶Ên™À|oxe6@r…j€¯¾°“†“‡VGeuou†¨¹¦›§£‰soutmnƒ–•‘›¡™‹€€~umq{‚„‹ŽzLP‰Ó¨}¯©”…gb}V2Kq„€…Ÿ¼°€Œ„dRZq€|}šµ®™˜“kix{vzŠ˜›–”˜“ˆzxyytu|„…‚wUTƒÍ»©§“†nWqh=B]x“‰¥µ£Œ€}zo]\r„†‡“¥¬£“‡wnpx€‚Œ˜™”Œ‡~vxz|z}~ƒoR`™Ò³¥ž“…kSmgIEXrš¡ˆ™²®œ‡quo\cuˆ”•“¢¯§˜Œ†…„vlq}…‰‰‹”š–ŽŠ†…‚|uxz|{hVf”Ë¿ª£š—‰x`]_YUOZw˜Ÿœ¡¡¦£Žxttrqlm}‘—›ŸŸ¢¤š‹…|{zv{…‰‘’”‘І‚~~|ywxhUfŒ¿Å² –˜‹}lYLZeUUk„ °¤˜¡¤™Œyglxtqu¢¤œ›˜{|}€}‰‘ˆƒ{||u][r—Á¯œ–—Š~oWK[f_^jz–°®¡œš›—‰vkkqz}{˜¢¤˜—“‰y{~ƒ„„Š‘‹‰ˆ†‚€|xgXh„­Ç¿ª˜—“‰oVIXfhlq}“¬µ«¡™•”‚tlnv€†‡‹’›¢£–‘Œ‰‡ƒ|{}ƒ†ˆ‰Š‹ŒŒ‹‡ƒ}waXlˆ¬Âº£“Š‚{nVJS^lvuzŠœ©®¥—‹†„slovŠŽ‘•˜š˜‘‰…€€}}~€ƒ‡ˆ‡ˆ††…„~xeUbx•¹Á®›ˆƒ~ufUFFXkv}ƒ‡’¢¨¥…€|yxvrsxˆ‘”“”’Žˆ„{y{yy€€‚„†‡†…ƒ~u_Xi~œ»½ª™Œƒ~|wk]MGQcvƒ‹Ž–Ÿ£ ™ƒzvvx{{{~ƒŠ‘–—•’ދЇ„‚~|||~€‚ƒƒƒ„…„ƒ€xcZj}˜ºÂ°Ÿ‚|zundTIM\n‚”•–—›™“‰~urtx|€‚…‰“•“Œ‡„ƒ€~{|}~€‚„ƒƒ‚‚q]_r‡§Áº¨™Š~zxrj]MITexŠ“”•–˜˜„ztux}€‚‚…‰Ž“–•’‹‰‡„ƒ}~~ƒ„†………ƒzd]m€š»Á¯Ÿ‘„|zundTJP^p‚•–—™š”‹€yvx|€‚ƒ†‡Œ‘•–”‘Šˆ‡…„‚€€ƒ…‡††††…‚r`gz¯Å»©›Œ‚~{tl_ONYj~™šœ›Ÿ£¢•Šzy|€„…‡‰‘–™˜–“‹‰‡‡…ƒ‚‚ƒ†ˆ‰‰ˆ‡†ƒwddvŠ¨Ã¿­Ÿ„~|vnbRMVew‹•˜™™œ¡¡œ–Œzxz~‚„…‡Š”˜™–“‹‰‡…ƒ€€ƒ…ˆ‰ˆ‰ˆ‡…€l^k}™½Æ´£”†}ztiYHGVkƒš¥£™˜˜“Š~ohlu€Š”˜š›˜‘‰‚{z|~„„†ŠŽŠ…‚~{yjWaw“Á˶£“‚qnkWGKXn°¶ª™ˆ€ztoe^fuˆŸ¬¨Ÿ•‹†yroq{ˆ”››–Š…€{wtv{‚‡ˆsYg}¬Ï½©–~cYciYSf…°¾«˜ƒiW]knoyŠ£¶±ž‰sdhsz~†œ¥¤˜ˆwnow…Š“–”ŠsS\rŸÐì™qPThskkŠ»Â¬›}TG]q…ŠŠœ°«œŠjQTi|˜˜™žš‘‚nbht…•š—”’†y_Uj¸Òº¦jMQiuhl†°Åµ¡ŠfGLb{„…¡­¬ŸŠpXSavˆ’–—–˜•Œnehs€““``w˜ÆÇ¯ž‡dFMmwjl{’²·¥”}\EPex„ŠŒ’ ¤ž“l^alz‰‘’“’““Ž…zpknv{pfs…«ÒȱŸŠnGA^nmry‚ º²¡|^IM[o€‡Œ“š£¥Ÿ‘€n`_iw„Ž““••–“‡zmZVi|”ÃÒ¹§–ƒhTU]dgho€—´¾±¡{fY[erz†‹•¡¨¦Ÿ“…ymiov}ƒ‡ŠŒŽ„niv‡¢ÂÁ­ŽiVR\glmms€“§±¬Ÿobbhpz€„ˆ’›¢£ž˜‚yssx}€ƒƒxnt‚’¯Ìdz£”…r^SUaklnt~Œ¡´µ«Ÿ~na^eox…‰Ž•¤¥¡™‚xporw|zoo|ŠžÁн«œŒ{fTPYdilr{ˆš°·¯£”„td\`ir{ƒ‡Œ’›£§¤œ‘…zqmqvy{qmw†—·Óʵ¤•†r\OQ^ikow¥¶¸® ‘qc^dnw…‰Ž–ž¥§£›…zsqsw|{qp{ŠÁÕÆ²¡’‚mYRXdijpx„•­¼¸¬|nbahpx~ƒˆ™¢«¬¦œ…ztsuy}|oo|‹žÁØÉµ¥•…raWZcfejs‘ª¼½³¤“„ujglruy~ƒ‹– ©­©Ÿ•Š€yxwy||siqާËѽ«›|k_Z]`_^do}‘©»º¯ ‚vnlnnqrtz‚𤍦ž”‹{xvuvurhcm|©ËË·¥”…uh^YYYSR[gv‹¢´·¬Ž‚xrpokiikpz†’œ¢¡š’Šƒ~{wtrpnkban}®ÈÀ­œrg_[[ULMVcs‰Ÿ°³¬ ’‡~xusmhfhmvƒ™ŸŸ›•І‚ytpmka^jz‹¨ÇŲ¢“ƒule_\UKJS`p…𬳝£—އ~yrjeehr~‰“šœš–“‹†€ztnmj`_l|Ž«ÇIJ¡’ƒvohd`YNJQ^o‚–§°¯¦œ•Іypgdfnyƒ”—˜˜—•”’Žˆ‚{uqqnd`k{©Äij£”…yqjfbYMGMYhzŸ«¯¨¢œ—’މ€wnfejs|…‘’”•––•’އzusql`^k{Ž«ÆÃ²¢“…ztnkf]QJNZk}ž§©¦¢Ÿž›—‡|slkpv~…Š’”—™›˜’‡‚~zywn``o€”±ÇÁ±¢”†|wroi_RJMXh{Œš£¦¥£¥¥£ ™„zroquzƒ…ˆ‹”˜››™”މ…}zp`^k|­ÃÀ±£”‡|xtoeWLLTdv†“œ ¡£§ª«ª¤š…|vvwz}€…Š”™šš—“Ї„|o^\hz­Á½¯¡’†€|yundVKJSas‚“—™¤©®¬¦œ‘‡€{yzyxyxz}‚ˆ’”“’‘ŽŒŠ‰†ƒzrbX`p‚š³º°¢–ˆ|zvrk`SKLTaq~†‹’˜Ÿ¦«¬¨¡—އ‚€}ywuuw|†‹ŽŽŒŒŠ‡„€|rb]fvˆ¡µµª‘…~|ytlaVPRZgt}ƒ‡‹‘™¡©¯°«£›”‹‰‡„{xwz~‚†‰‹ŒŽ‹ˆ†ƒ€wlgiz‹ ¯¯¦œ‘ˆƒ‚{vndZUW^isz…Š™£ª¯¯«¤ž˜’ŒŠ…€|xxz~‚…‰‹Œ‘’’‘Ž‹ˆ„‚~rcal|¨¸¶ªž’‡‚€}xsk`WQS\hs{‚‡Œ‘š£ª¯°¬¥Ÿ™”‘Ž‹†|yxy~…‡Š‹‹Ž‘Œˆ„‚€|o_]iz¨¸³¨›…€{wrh\ROR\hsz€…‰™¢ª¯¯©¢›–‘Ž‹‡‚}yutw|€ƒ†‡‰‰‹ŒŽŠ†ƒ€}wh[]l~•®¶®¢•ˆ}{wslbVNMR]it{†‹“œ¥¬°­§ ™’Ž‹‡ƒzvsux{ƒ…‡ˆ‰ŠŒŒ‹ˆ†‚|xo]Wbs‡ ³³¨›Žƒ~|ytof[QKMUamu{…Œ–Ÿ§­¯«£—‘‰†|vrqtw{€ƒ…‡‡ˆŠŒŒ‹‡„~{vi[Zgx¦´¯£—‹}{xslbWNJNWepx~‚‡Ž˜¡©®­§Ÿ—‘‰†|vrppsw|€‚ƒ…‡ˆ‰‹Œ‹ˆ…‚~zvm\T]o„ž³µªƒ}{xsmcWMHJR^jtz†—¡©¯°«£œ•‘‰†€zurquy}‚…†ˆŠ‹ŒŽŽŒ‰†ƒ€|ucV[l€›µ»±¤–‰€}|wrj^RJJQ]ku|ƒ‡• ª±³°¨¡š“Š…€zvtuz~‚‡‰‹Œ‘’‘Їƒzl[Zh|•±¿¹¬…{uncVNKP\jt|ƒˆŽ–¢¬²¶³«¤—“Œ‡‚}xuw{€„ˆŠŒ’”••”Œˆ…{l\^m€›¹Â¹­†ƒ€|vnbWNLUbpz‚‡Œ’œ¨²¸·°¨Ÿ—“Œˆ‚}xvx|‚‡Œ‘’”••”’ŽŠ†ƒ€}{ztdW_sŠªÇÊ»«œŽ†‚}wnbRFFO^o}†“𥰷·²¨œ“‹…‚€}ywwz€ˆ•—˜—––•“‘މ„€}||}€€‚„††‚l^j~˜½Òȵ£’…~{undREEQcxŠ”™› ¨°±®¥˜‹€{y{}~~€ƒ‰—œœ˜•’Œ‰…~||}€‚„†‡ˆ‰Š‰‡†ƒ}yhTXnˆ°ÓϺ¨—ˆ€|vnaN@CRe{Ž–™œŸ¥¬«£™‹~wvx}€€„‰–š™–’ŽŠ‡…~{yz{~€‚„…†……„ƒ‚€~|zz|{||}|}~}nVVm‡²Õθ¥’ƒzwpfUBBQg€“šœž ¥§¤›€vrt{‚ƒ…ˆ–šš˜”ŽŠ†„ƒ€~~}~€ƒ…†…†……„ƒ‚€~}~€€€ƒ„ƒ€rXXoг×Ѻ¨–‡}tjXECQd|“ ¢£¨­ª£—ˆ}xx}‚††ˆ‹—Ÿš”‹‰‡…‚€€‚…ˆŠ‹‰‰ˆ‡ˆ‡†ƒ‚€€‚‚ƒ„ƒ„…††…„‚pX^w“¿Û͹¦”ˆ‚}thUDFWm„˜¢¡¡¥¬®«¢“†}x|ƒ…†‡‰—žŸŸ›•’‹‡‚~€‚†‡†‡ˆˆ‰Šˆ…‚€€€€€€‚ƒ„…„„‚~hTf…²Ó̵¢™•qYDBXjnpuƒŸ¹»²¥š™›’‚vnq|…‡‰Œ‘œ£ š•Ž…}zy|ƒ‚„‡Š‹ˆ…ƒƒ‚€|yz||}}~€rRUy¬Í雤žŽ}dR]eSFMe…Ÿœ™¤°¶­–…„„vln~ޕ𤦛‘Œˆ‚yv|~€‚†ŒŽ‹ˆ‰ˆ†‚}|}}{z{~}gSh È­§®¤’zmtmQ9Aa|yvŠ®À¯šš¢˜‚mgw}qn~‘™˜•¨¡‰‹ˆ€vsz€}{„ŒŒŠŠŒˆ€|~{xwvz||x{cOj·¼x¢Ä­›„x~p?3^hT^u›¯˜“°¯’„‚rd_pyz’¡œ˜žŸ™Š‚‚skw|{…‹‰‹|~zurwvxuygQb±³c¥Ê§•ŒˆvW4]e=H{Œw³·¡•”t\nu^_w‰ˆˆš¬¤”™›‰xy{rlr}†”’Žˆ}}ysqxyvp]YÅ}‚Ͷ›’‘}lJRfC;k„i~®³¢œŸžŠmv`Vo{v|ŽžŸ— §•‡‹‡vpswvt}ˆ‡„’Œ‡Š…~xxxuqut[R}Á~pɼ˜•—ƒu[`iA5hx[v£¥˜¡ª¦Œ}_cyojyŠ•§¢’—™‰~~vpu~}{†ŽŠ‹‘ˆ…ƒƒzvxxjWTŒ²kŒÐ°”ž˜†vfqc8GxaZƒ—•ª³¨–š}l~ydhy€€ƒ•¢—–¤žŽŽŽ…zwz{uz……ƒ‹‘ŽŒ‹†„…}zqZZ¡¡g¦Ï¡™§š‰{y|[?mqMiЉˆž³´¢­•Šon{|wz‹–”¦¤˜œž“‹ˆˆx}ƒ|~‡Š‡‘‘Ž‹…†„qV`§”d±È“Ÿ­™‹€~WQ}bJsƒw…ª¤ž²­’–…y‚~vu|‡……—š“¢™•–“Œƒ„„|z~‚‡‰‰ŠŽŽ‰ŒŠ†}iQ†®lƒÍ¤Œ­¢†ƒiTskB^zjqŠ˜œ—¬·žœª˜†Œ‰ysy€yx‰Œ„›””˜–ŒŒ‰}|x}~}~‚†…‡ŒŠ…†€hQ©cÉ‘­›Œƒ‚€`^{Z@ko\p†ŒŠ”°¦—­©‘”–‰~}€zs~„}ƒŒ”˜”’’‰†‰ƒ}~zz|~~„„ƒ†…iX’¢c™Äˆ”±—Œ‡„|ejvROq_Ys„–¨—œ´¥–¢›‹ˆ‰„ww€yuƒ…ˆŽŽ’•Œ„……}|~|xy}|{€~€}hX‘žc›À˜³–ŽŠ‡zgrrMZnPWmnr|Ž–Ž ®£«—–‰€€tv~wz‚ƒ†ŒŽ‹’Œ‹ˆ„…‚~{}{wy{xzzvbY•b§±y£³”‰xmyiPjeH[i`ex„‰¡—•­¤˜¢Ÿ“‰|~rs|tu}}~†‡…‹Œ‡‰‰…‚„}{|xwywts`Uއ_¥£x§ª‘‹yt}g]q_Paa]et{w‰–˜¥¢ £¤š”˜Ž„ˆƒy{{xvz}{…ƒ„ŠŠˆŠŠ‰‡‡…€€~||yxuoVj˜kv´‰ˆ²Ÿ”“’‰u||bhq[\fbbkvy}Œ‘›¡ž¢¥¢Ÿ››“ŽŽˆ€‰‡wyxzz|‚„„†ˆ‡‰ˆˆˆ†……ƒ€~}zt^\zdž˜„žš‘ˆ“‚vƒvjpmiacodgywy„ŠšŸ™£¦Ÿ¡¡Ÿš•—‰‹‡‚€y~€z~€€‚…„ƒ††„††„ƒ‚ƒyhrwx˜†“˜‘†zˆwvxxtmttksxqx}…‹Ž‹“—”˜š›š››™™˜–”’’ŽŒŠ…††ƒ‚€€€€€€€~u|‰€‰‘ˆ„˜‘‰’Šƒ‹~~‰‚}~ƒ}~ƒƒ‚ƒ†ˆ…‰‹‹ŒŽ‘’‘“’“’’”“’’’Ž‹‹Š‰ˆ‡†…†…ƒ‚ƒ‚€}~‚‚‚€†‡zƒ‰‚ƒ‡‡u„‰ƒƒy…‡‚ƒ…ˆ„†‰‡‰ˆ‰‹‹‹ŒŒŒ‹Œ‹ŒŒŒŒŒŒ‹Š‹‹ŠŠ‰ˆ‰ˆ‡ˆ‡††‡†…††…„…„ƒ„…„…„††…„……ƒƒ„ƒ„ƒ‚ƒ‚ƒ‚‚ƒ‚‚‚ƒ‚ƒ‚ƒ„ƒ„ƒ„„………„„…††††‡†‡†‡‡†‡‡†………†…†……„„…„…„ƒ„ƒ‚„ƒ„ƒ„…ƒƒ„ƒ‚‚ƒ‚‚‚€‚€€€€€€‚‚‚ƒ‚ƒƒ‚ƒƒ„ƒ„…„…„…„„…„ƒ„…„…„„„„ƒ„ƒ„„…„ƒ„ƒƒ‚€‚ƒƒ‚ƒƒ‚ƒ‚ƒ‚‚‚‚‚‚‚‚€}{‹„yƒ‚ˆ|…†~€Œ‚‚…Љ‡„ƒ…ˆ„…ˆƒ………†ƒ…†„†……†…„„……„…ƒ}ƒ…‚‚†„…Žƒ„‰…ˆ‚ƒ‡~~€w‚t{{{zvyxzz|}}~€€€ƒ„††…‡‡‰‰‰Š‰‰ˆŠ‰‡‡ˆˆ‡‡†‡†…„„ƒƒ‚‚€|s|…{}…ˆ„ˆŒ„’|}‡~|wy~ttyvvuwyuxzy~~€‚‚ˆˆ‡ŽŽ‘”‘’•“’“’‘ŒŒŠˆ‡†…ƒ‚€~}}|{ywfk†xt‰‹ŒŠŽ–ŽŒ‰…}wrxrmppomqxtw‚ƒ„‹‘”•›Ÿ¡£¡¢  š›—’‘Ž‹‰‡…ƒ‚€‚ƒƒ„„…‚za{žt†¯”•¨¦šš’|„ƒopsjabib_oqn|‡ˆŽ™ŸŸ¦­©ª¬©¤¢ž™“‘Œ†…~z~}|ƒ„…‡‡ˆˆ‡‡†…ƒ‚~vUe on²‰«ž‰“wt‚hdm_^X]g[l{s‚™¥¦¢­© ¤”“‰ƒ€yw}zz€€ƒ†ˆ‡ŠŒ‡ŠŠ…†„‚€}|ztlIi¡as¾‰‡¹¦—“•‹s~{\emQR`]Zdxvy”•‘¦ª¤ª­©  ‹Ž{€zvx}|{ƒ†‚ˆŒˆŠŒŠ‹Š…‚ƒ{}ywtpUO˜X·Ÿuº®—ž“}tcZsUH`XWao{vŒž¡±£©±©£ž•ˆ†y~}ux{}~ˆ†‡ŽŒ‘Ž‹‹‰„„„}|zxtaK…•Y˜¶x ¹˜–•‘€u€mYocI\`X_nzv…ž“𲩧±­¤Ÿ —ˆŒˆwz}rrzz{}…†…Œ‘‹‰…ƒƒ{|wvulK[žli»„¾ª—˜“ŠtyzZ^pNI`YVbtww‘›£±¥©±©ŸžžŽ…Š€v{yrtyyx†„ˆŽŒŒ‹Š‰‡‚‚{{{wtpWP“‚[¯¨z°´™˜•‘|uhYq_I[a[_qv‡ ”œ²«¨¯¬¤œŸ–‰Œ‰z|~vx|~}‡‡‡ŽŒŽ‘މ‹‹„ƒ„}~}xxxmQrŸg½Œ’½¦š˜–Švz[hrLXlZaoz€˜ž•«³£­²¦¢Ÿ‘‹„z…v€~†‰‡‹ŒŒŽ‹‹‰‡†ƒ‚~~|z|voUlŸmvº“‰²¥—’‘Žux€f`ndV\lje|†}Žœ˜¤©£¢¦ ™—–ˆ‡„|€z„‚…‰‰Š‹Œ‰ŠŒ†„‡‚‚ƒ€‚~~€|zs[z™o…²’‹«£Ž”ˆw}~hfsg^hsnmƒˆ€’œ•œ£¤¡¢“•”‹ŠŠ†ƒƒ‚€„„‚…ˆ‰ˆ‹Š‰‹ˆ†‡„…ƒ‚~|qZzr±™ ¦˜„rw}leimf_pxo|Ї–œ™›£Ÿ™š™’Œ††…ƒƒ€ƒƒ„‚ƒ†ƒ†ˆ„†‡‚‚ƒ€~~}{~~|}~~{yp_|’t€¡–‹’–’‚€‡zmutjiimkisxv€‰ˆŒ’•”””ŠŠ……†ƒ~~€~ƒ€„„ƒ‚€‚€}}~}|}|}z{}vjiˆ‹r‰ Ž†‘“‹}€…vnvuljnsppz~|‰Š‹ŽŒ‹Š‡ˆ‡„†„‚„ƒ€€ƒ‚„‚€‚ƒƒ€„{‚~‚€€|}nw‘…Œ„‹~~‚}wu{}tv~~€ƒˆ‡ˆŒŽŒŽŠŒ‹ŠŠŠˆ†‡†‡†…†‡††‡…†‡……‡†…„…†…‚…‡„„ƒ„…‚ƒƒy€ŽŠ„‹‘‰ˆ‹‚‚‚€‚~|€€~‚ƒ…††ŠŒ‰ŠŒŒŒ‹ŠŠ‰Šˆ†‡‡†‡‡…††…††…†…††…†††……ˆ†ƒ………†ƒ„‡„ƒ†„„„ƒ‚~ƒ‹ˆ‡ŠŒ‰ˆ‰††‡ƒƒ…‚‚‚ƒ„†‡ˆ‰ŠŠ‰‰‹‰‰ˆŠŠ‡‡ˆˆ‡ˆ†‡ˆ…„…†…„††„…………†„ƒ…‡„‚……ƒ„…„ƒ„„„…„„ƒ„‚|‚†„‹‹‹Š‚…‰‚‚…„ƒ€~ƒƒ‚„‡††…‰‰Š‰ˆŠ‰ˆ‰ˆ†……††„†‡…†…††…††…„†……„…†ƒƒƒ……ƒ„‡…„……‡†ƒƒˆ†‚ƒЉ‡ŒŒŠŠˆ‡‡„„…‚‚‚ƒ‚ƒ…ƒ…†…†‰ˆ‡ˆ‰ˆˆˆˆŠˆ†‰‰†††‡………†ˆƒ…†„‡„‚‡†„…„„…„‚„„„ƒ„†‚ƒ…ƒƒ„ƒ‚ƒ€~‚††…†ŠŠ††……„€€~€€‚„……‡„…ˆ‡…‡‡‡‡„‡‡„„…„……ƒ„ƒ„ƒƒ„„ƒ„ƒ„ƒ„ƒƒ„‚ƒ‚ƒƒƒ‚„„‚ƒ„‚€‚ƒ~yƒŠ…‚†ŒŠ…„……‚€€{|~~€„„‚††„†ˆ‡†‡†………†„‚„…ƒ‚†„…„‚„…„†…ƒ‡†………‡„„…„„……„………†…„…„~hf–¬““£¦Ÿ‰}„kcfoytsŒ›‹•š“…z†xt‡‹…†ŒŽ‰Œ‹†„‚……€~‡‰…ƒ…І„…‚€|zƒ}‚ˆ‚~ƒ‚‚~z‚…|~‡„~ybBa§Ð´¡¶£‰g?Q{aOˆÀ´š™”sIIt~r„¥³¬—„‡‚jaq‚•–›ž‘€yxwuwŽ“‘‹‰€vsx}…Š‹‡~{wvy}‚………ƒzweEJ‹ÓÈ—¦¦‰X0E|œa‡Ò¾Ÿ†~waGQ|¼ª‹ª­’iNYv}r‹¼¾ ŽŠƒq^`~™—’ ¥—€ppxxt€˜œ“ŒŠ…{pmw‚‚ŠŽŠ€|yzww{ƒ„pLS’ÒÈŽ£¢‡V2K…¡^ˆÎº›„~|eKS€¿¤‡«®‘kQ_‚t½Á¢‰wce™’£¨šƒsv}xs—™‹Š…~rnv~‰Ž‹…}{xuu~w[NyÃÑ™™©‘n95n—ah¼Ç¨‡ƒwOCc˜—|¢¸¨Žmf}w`o˜ªŸ—™’rcwzz‹Ÿ“ˆ‰Š{lq}‚…“ƒ}sqzƒ‚„‡ˆƒ|}zpQNvÂˤ¯–ƒR2vG^¢Ë¯›˜ž‘a=d€il†®¾¨“Ÿ¢‚dj{|“ª¤’”š~vx‚ƒ}‚‘”ŽŠŒŒ†|y€~~„‡‰…„…†}~}€||ZTÒ´‡¸²žkl‡T4Vv‰‚¡ÆªŽ’”„t^iˆsŠ ¡ž˜›¡’€…ˆ}|ƒŽ‹†Ž”‘Œ‡‰‹ƒ|~€€~ƒ…„‚„„ƒ‚€~}}}€~}eRl¯«€¥¹¯ˆˆ‹ydX`o_Nh…ƒƒ‰š¬ž™›“‡~†ˆ~|…ˆ…†‘މ‰‰†‚€‚ƒ€~€€€‚ƒ‚‚‚‚€€€€€~}q[q™Ž‹•ž° –”Œv{xlihlohjsx}}‚ŒŽ’”˜˜—™™™–”•“‘ŽŒŠ‰ˆ‡†„…„‚ƒ‚‚€€€€€€~~€€€€~zinŽ‹„ˆ’ ”Œ”“ˆƒ‡‚{yxzvqstuuu{~ƒ†‹Ž“”“”––”•”““‘‹Šˆ‡‡†„ƒ„ƒ€€€€~~~}~}~}~}zjjˆ‹‚‡Ž›•‹‘“ކƒyvuvtopsstux}}€„ˆ‹‹‘’’“””“’‘’ŽŒŠ‰ˆ‡†…„„ƒ‚€€~~~~~}~}~}~}}|zqcyŒƒƒ†•œŒ’Œ‚‚{xuuvqpsrssw|}€ƒˆŒŽ‘‘’’“”•”•”’‘ŽŒŠ‰‡†……„ƒ‚€€~~~~~~}}~}~~}|vesކ‚ˆ•ž”’ƒ„~yvwxrostvuu|€ƒ‡ŒŽ‘”•”–——–—––”’’ŽŒŠ‰‡††„„ƒ‚‚€€€~~~}}~}}}{sdxŽ„ƒ‰–œŽ•‘Œ‚‚„|wuuvppsstsv||‚†Š‹Ž’’’”•””“’’Ž‹‰ˆ†…„ƒƒ‚€~}~}~}}||}|{|{{|}{xidƒ‹…‹œ™‹’”‰ƒ‚xvuvuopsstsx|}€‚‡Š‹Ž‘“””•––••””‘‘Šˆ‡†…„ƒƒ€€€€~~€~~~~~}udsˆ…Š– ’•“Ž…ƒ…~zwvxrpuutuy}}€†ˆŠŒ’’”–—˜––—–•“”“‘ŽŠ‹Šˆ‡†……„ƒ‚‚€€€€€€€€€wv„„€‰‹‡‰†ˆ‘Œ€„Œˆ~†…~~ƒ„€ƒ‚‚‚„†‡††ŠŒ‹ŠŽŽ‘Ž‘ŽŽŒŒ‹ŠŠ‰ˆ‡‡††…„ƒƒƒ€€|ƒ}}†ˆ~†…„€ƒ†„„„…†„…†…†…†‡‡‡††‡‡†‡ˆ‰‡ˆˆˆ‰ˆ‰‰ˆŠŠ‰ŠŠ‰Š‰Šˆˆ‰ˆ‰ˆ‡ˆˆ‡ˆ‡†‡‡††…„ƒ…„ƒ‚€ƒ‚‚‚‚‚‚‚€€€€€€€€€€€‚‚‚‚ƒ„…ƒƒ……†††ˆ…ˆ‡‡‰ˆ†‡ˆ‡†‡‡††‡‡†‡†‡††…†„……„„„„„„„‚‰}r’—un–¢sfš¨pc›¨ob™¦ob”ŸpdŽ•ohˆŽsl‡yrz†ƒyx†wŠ–„yš‡~‘™‰„’–‹ˆˆ‡‰†„‚€€}xxvw{|xz‚w|ƒzv}vu|{vy}||~ƒ„‚„††…†‡‡…†‰Šƒ‚‹‹ƒƒ‹‰ƒ†‰†……††…†……ƒƒ‡†ƒˆ„}‡ƒ~„†‡‰‹ˆ€ˆ„|}€~{|{|}}~€€ƒƒ€ƒ‡‚‹ˆ~‚Œ…}†Š€‰‡‚‰…~…‹‚ˆˆ€‹‡}‚‹‡…Œƒ}‡Œ}ŠŒ~|‡„‡††Ž……‹‡ƒƒ„„€€‚„‚ƒ„…†…‡ˆ‡ˆŠˆ‰‹‰…ˆŒ…ƒ‹‹€‚‹ˆ‚ƒˆ…ƒ††‚‡…ƒƒ…„„ƒ„†„‚ƒ…„„…‚ƒ‡†€}„…‚„…Ї‚‚ƒ€€‚„‚„ˆ†…‹‰„‡Œ‰…‡ˆ……‡…†‰ƒ‰†~…‹{‹Œz}‘‰w‚“…xˆ€}ŠŠˆ‡‚ƒ‡ƒsh…¦Ž{Ÿ°ƒ˜’ytximuuv}‚‡ŽŽŽ“”Œ‹‘Œ‚‚†…„……‡Šˆˆ†‡‹€ƒˆ„‚……„ˆ†„…‰††‰…Šˆ~Œˆ}~…^S–Ì„sÅÆ‹‚¢’i_qof`e‡–ƒ«©‡‰Ÿ’zv€ƒy“‡’ž—Ž…}‚†‚€†ˆŠŒŠ‰Œ‹ƒ†…‚„}€…†ƒƒ„‡ƒ}vXMŒÚvÊÀ—‰ƒhNFgz`fŸ±Œ•²¢‰~|xrbiƒˆ~©£••—Žux|yvŒŠ“…‚ƒzy}~‚‡Š……†„{{xzaG]µÑkœÓ©“…„tW6H}_Æ´™ ŸvUayh^Ÿ—§­¥~‚lcvƒ…“œž“Œ‰xryzwy„ŠŒ†ˆŠˆ|z|yspmQOÙ˜rŵ™ˆ}wd?8e‡it¹¿£–—WSyxez ª¡¤¬ª˜}}‡xjwŠ‘” ¦š‹ŒŠzou{}{’Œˆ‹‰vxwwpq]Toýh§¿œŽ~ynM1K‚z^œÆ¯™”‘…dD^xef‹¨¦ ¥¨Ÿƒoz{ce|‰Š˜£Ÿ‡Šmov{z‡’Ž‰ŠŠƒyvuvqmVUvÊ®mµ¸šŒ}xgC2[ˆmk²À§–“}YHkwcn–©¢Ÿ¦¨›}svalˆ‰‘¥œŒ‹‰zpuz}{‚‹’‰‹‹zzxvspXVzΣm¹¶›Œ~zkF3^„hiª½¨—•“†bRsx_h‹™š§® ‡ƒŒ~io„‚ˆ”Ÿš’”ˆ{z}|yz‡‡‡Œ‰ƒƒ‚~yvqXVÆ’u¿· …wM4ixZi¤§œ©œ}}ˆuchv‚‚€–« ˜Ÿ –ˆƒƒwv€†…†Œ“‘ŽŠ„‚ƒ‚|€„ƒ†‡‡„ƒ{`XŒ¹„‹À³¥’ˆŠ~X^qYR[mŒ‡¢±žšž›–‚|‰‚tw‡ˆ…š–”——–Š‹‰€‚‚†…„†ˆˆ†……„‚€€~}r[cšš|ž³­ “~turkXXnh`mz…„‚’œ˜•—ž‘”І†ˆƒ„……„…ˆ‰‡‡ˆˆ‡…†…„„ƒ‚€€~€€~zhf“‰™¤”ˆ—”ˆ‚€ƒ{pstummxuqsy‚‚€‡ŒŽŽ•–”•–˜–””“‘ŽŒ‹‰ˆ†„ƒ‚ƒ‚‚€€€€‚€€€~yir‡„‰”œŽ“‘Œ…„„{yzzuuwyyw{ƒ†‹Ž‘“”••–—–••–”’‘ދЉˆ‡†……ƒ‚ƒ‚ƒ‚‚€€~riŒ‚…‰–•‰Ž†€‚€zxvxuqstutuz}~€„ˆŠ‹Ž‘’’’’“’‘’ŽŒ‹‰ˆ†…„ƒ‚‚€€€~~~~}~~}|vfpŠ„€…˜‹Š‹€zwtuvpoqsssx||}‡ˆŠ‘‘““’‘’ŽŽŒ‰ˆˆ‡…ƒ‚‚€€~~}}|}}|||}|}{{~||„†}|ˆŠ}|‰‹}{ŠŒ~}Š‹~ŠŠ…‹†‚‡Šˆ††‹ŠˆˆŠ‹Š‰Š‹‰‰ˆ‰ˆ‡ˆˆ‡‡†‡†‡††…†…†…………„……„„…„…„…„„…„…„ƒ„„„…„…„………„…†…††…„……„……†………††…†…††…†…†…†…†††…†…„…„…†…†…††…†…†…„…†…„…„…„…„„„ƒ„ƒ„„ƒ„„ƒƒ„„ƒ„„„ƒ„„ƒ„ƒ„ƒ„ƒƒ‚ƒƒ„„ƒ„„ƒƒ„„ƒ„ƒ„ƒ„ƒƒ„ƒ„ƒ„ƒ„ƒ‚ƒ„ƒ„ƒ„ƒƒ„ƒ„ƒ„ƒ„ƒ„„„„„……„…„…„………†…„……„…„„…†…„…„…††…†…†…††…†…„…†…„…„…„……†…„…„……„……„……„†…„„…„……„…„…„…„………†„……………„…„…„…„………„„‡…†ƒƒ‡…ƒƒ†……‚„‡…„„„ˆ‡‚ƒ‡ˆ„…„„Š„€‡……‰ƒƒ‡„ƒ‡‡ƒ„……†ƒ†„†€„‰„ƒ„…†ƒ‚†€~‡†ƒ€€ˆ‡}€„‚…ƒ|ƒˆ‚‚€ƒ†ƒ„|…ˆ~‚„€‚ƒ€…‚€…‡~„ˆƒƒ‡…|~ƒ†‡~}ƒ„†ƒ€€€„„€€„†ƒ{†€„‰‡ƒ„‡ƒ€…‡‚‚‚……ƒ…†‚‚†ƒ‚„ƒ…„‚€„„…„€ƒ‡…ƒ„…ƒ„…‚…„„…}€€oduŒœ­«™”•‹„tcafiongm˜›—–š›–‰€}~~‚~„‹‘–”ŽŽŒƒ{z~€‚…‡‹‹‡„††„…ƒ}}€€ƒƒƒ…‰‹‰ˆˆˆ‡ˆ†„ƒ„……‡‡††ˆ‰ŠŠ‰‰‡†…„„†‡††‡†‡‰ˆˆ‡ˆ††‡…‚ƒƒƒ|dZkˆ²ÏɵŸŽ…zraKAK^uŒ••—šž¢ž“„uhdksx}‚†–Ÿ¡ž—…€}{zyxx|‚‰Ž‹ˆ…ƒ}{zy|„†††‰‰ˆ†…ƒ‚ƒ„„ƒ„…‡‡…v^`w•ÂØÅ®šŒ‚{yrbNKVh€šª«¤™™—pb^gu‚Œ“•—œ¡¡ž•Š~wtuy}~‚‡‘”’‰„~}|{{}€„ˆ‰‰ˆ†…ƒkT\u•Ãש—‰{zr`MJViƒ›©«¦ž¡ž–‰yjgmx„Œ‘’–£¦£›‘‡||~~€‚…Š“’Œˆ…„‚€~{z{}‚…„††…‚~gVe~ŸÅ͸£“‡|yn]MITi–¤¨¡œžŸ›•ˆwkgku€†Š”œŸ˜‡}||}||}€„‰Š‰†„‚€~|{zz{|~€w`Xjƒ§Êǰ‚{xtjZJFTi€–¢¢žžŸ–‰zojnw€…‰‹Ž”™ž™’‹„~}}||‚†‰Œ‹‰‡…„ƒ€~||{{|}~}yeXg~ÃÌ·¤“†}ztj^MDM_t‰˜›ššœŸ š„xppu{€ƒ…ˆ’—›™•Žˆ„‚~{{{}…‡ˆˆ†…„„‚€~||{|{|{vdWdz–½Ê¸¥•†}{vl`OCJZmƒ‘•–—›¡¡›’‡zssuz~€„ˆ“––“ˆ„€~|zz{|~€‚ƒ„ƒƒ‚€}{yxxyxxmYXk¤ÆÄ¯ž‚|xqgXFBN`vŠ“•˜™œ¡¡š‘„xrsw{‚…ŠŽ”—•’ˆ…„‚€~||}‚„……††…„ƒƒ‚~~|}{n]_r…¢Â°¢’…}wrl_RRYbtƒˆŽ’’”œ ŸŸœ“Œ‰…ƒ…‡„„…„„…‡‰‹Œ‹Š‰‡‡††……„ƒƒ‚‚‚‚‚€~xpmwƒ‹™¨©ž˜’Š{||yxyxtpopsty„‡‰ŒŽ’•––—˜˜–••“‘ދЉˆˆ†……„…„ƒ‚‚ƒƒ„ƒƒ‚€€€{torz„š¡¡•‰ƒ||zzxwusqrsv{‚†‰‹’”•–—˜—–••““‘Œ‹‹‰ˆ‡††…„„„…ƒ‚‚‚‚‚€}wrqv‰“¢Ÿ™‘Šƒ}zzyyxxusrrsuy}…ˆŠŒŽ‘“”•––—––•“’’Ž‹‹Šˆ‡†…„…„ƒƒ„ƒ‚ƒƒ‚€~xrrw€Š“œ –‰ƒ{zzyywusqqrtw{ƒ…ˆŠŒŽ‘“”••–•”“’‘Ž‹‰‰‡†…ƒ„„ƒ‚‚€€€~}wqot~‡™ž—‰ƒ}zyyxvusqpopsvy}…‡‰‹Ž’“•”••“’‘ŽŒ‹‰‡††…„ƒƒ‚€€€}~~}}|zvolpy‚Œ–œœ•އ|wwvvtsrpnmmpsw{€ƒ…‡‰ŒŽ‘’““”“‘‘ŽŒ‹Š‰‡†……ƒ‚‚€€€~~~}~}|zunkoz…™Ÿž—ˆ€{wvwvvtsqnoprvz~„‡ˆ‰Œ‘’“”•”“’‘ŽŽ‹‰ˆ††…„„ƒƒ‚‚‚‚€€~}ve^kˆ©±¨£˜‰‰’si_^gg_[ahv…“¤£ž—‘‘Žˆƒ‚„‰ˆ†ˆ‹ŒŠŠŠ‰†ƒ‚‚‚€‚ƒ‚ƒ‚‚€}ePc˜Êª•ª¨Ÿ“djqYBEQh†„~™³­¡—ŽŽŠyimx{|€Š—š–—˜‘‡€~~}|€‡ˆˆŒŒ‹Š†‚|y}|~~€€€oPZ˜Ô…¿°šˆqrtI4U~rn“³¹Ÿ‹“–sW`vrmy”¨ž—£¦’€~‚xpt‚ŠŒ”œ“ˆ‡ˆvw{€‰‰ˆ‡‰€w{iMY¡Ñy‰Ã°š…xZ4Kƒb˜Å²š••ŠiMi|il¨¢œ£©{‡{js‡ŽŽ“ž¥˜~vz€‡‘•ŽŽŒ~{}vwiZi´Ìt¡Ç£“‚~tR2V‰sh«À¦—•€\Ntxexœ¦žŸ§¥z}„piz‹Ž—£ ’ŠŽ†wu|‚Š”‘Œ‚}zzvunY^–ЅŦ’}vZ5F~`’¾©•ŽŽ…fHfzdi¤š £•{u‚ter…‹ŠœŸ“ˆŠ‡wpw}{~„‰‰‹…{zyytr]YzÉ¥o¸´˜Š€|jB5o‡`{¹²›““wQc€gc…š›œ ¨¢‰Šjsƒ†‰“Ÿ—Ž”‘„}~|ˆ‹†‹Š‡„‚~y\SwÀ˜s»¸ …„xJ@upYp”©ž¥©Œ|†„pdk‚„z‰ ž˜™œšŒ‡ˆyx„„„‰‘‘‰ŒŽ‰„ƒ‚ƒ}|€~|i\µŒ€¶³¡Žˆ‰}`_rdTUn†zz™¢™””š“ˆ}vx~‚€ŠŽ‹‹ŽŽˆŠŠ„ƒ‚ƒƒ€€‚‚€‚ƒƒ‚ƒ€|i_†„ž«œ„‘–„xy{vd_pogdo€zw„ŒŽ˜˜’’•ŠŽŠ††ˆ†„„„†…„……„ƒƒ…„ƒƒ„ƒ‚‚|mt‹†‚†–‹‰ˆ€€{xvxwssvwwvz}~„‡‰ŠŒŽŽŽŒ‹‹‰ˆ‡‡††…„„„ƒ‚€ykz‹ƒƒ‡“–ŠŒ‡€‚€zwuwtprtuuuz}~€ƒ‡ˆŠŒŽŽ‘‘ŽŒŒ‹Š‰ˆ‡‡‡††…„ƒƒ‚€vk€Œ„ˆ•”ˆŽŽ‹…~‚xwuxuqttuuu{|}€ƒ‡ˆŠŒŽŽ‘ŽŒ‹Š‰‡‡††……„„ƒ„‚‚€|mrŠ……Ž—‹‰Ž‰€|xvwwsrtuvux|}~…ˆ‰Œ‘’“’‘‘’ŽŽ‹Š‰‰ˆ‡ˆ‡†…†…„„ƒ€qtŠ„‡™Œ‰ŽŠ‚„~zyyzuuwxxx|€„‡Š‹‘‘’“”“’“’‘ŽŒ‹Š‰‰ˆ‡ˆ†…†……„‚€qsŒŠ„ˆ˜Ž‰ŽŠƒƒ~zyyyttwxxw{ƒ‡‰ŠŒŽ‘‘‘ŽŽŒ‹Š‰ˆ‡‡††„ƒ„ƒƒ‚rn‡‰‚…‹–އŒŒ‰‚~|wuuvsqtuvvw|~…ˆ‰‹ŒŽŽŽŽŒ‹‹‰‰ˆ‡††……„ƒ‚‚€~vj}‰‚…‘’†‰Œ‰„}~wututprsttuy{|‚…‡‰‹ŽŽŒŒ‹ŠŠ‰ˆ‡†…„…„ƒƒ‚ƒ‚{mvŠƒ„Ž“ˆ‡ŒŠ†~€zwuwwrsuvwvz~ƒ†ˆ‰‹ŒŽŽŽŽŽŒ‹‹Š‰ˆ‡†…†…„„ƒ‚‚‚€~rq‡‡„Š”Œ†Œ‹ˆ~|wuvxtruvwwy}~€…‡ˆ‰‹ŒŒŽŒŒŒ‹Š‰‰ˆ‡††……„…„„ƒƒ‚xn‰‚‚…‘‡Š‹Š„~€~ywvxwrtvwww{~ƒ†‡ˆŠ‹ŒŒŒŒŒ‹‹Š‰‰ˆ‡†…†…„…„„ƒ‚ƒ‚~qwІƒ…“Šˆ‹ˆ€‚}zxyzvuxyzy|€€„†‰‰‹ŽŽŽŽŽŽŽŒŒ‹‹Š‰ˆˆ‡†‡‡†…†……„ƒƒ‚xs†‹ƒ†Š“‡ŒŒ…„|{{|zwz{|{}‚ƒ…ˆ‹Œ‘‘ŽŽ‹‹Š‰‰ˆ‡ˆ‡‡‡††…„„}s‚…†Š“’‰ŒŒ‡‚„ƒ~|{}{xy{|{|€‚ƒ„‡Š‹ŒŽŽŽŒŒ‹ŠŠ‰‰ˆˆ‡‡ˆ‡†††……ƒ{u…‹…†Š“ˆ‹Œ‹†‚„}|||zxz|{{|€‚‚„†‰Š‹ŒŽŽŽŽŒŒ‹ŠŠŠˆ‡‡†‡†…„…„ƒ„ƒƒ‚€|r}Šƒƒ†Ž‡ˆ‹‰†€‚}{z{{xy{zz{~€€‚„‡‰‰Š‹ŒŒ‹Œ‹‹Š‰‰‰ˆ†††………„ƒ„ƒƒ‚‚€~tw‡…‚„‰ˆ…‰‰†‚€~{z{|yxz{|{~€‚„‡ˆ‰‹ŒŒŒŒ‹‹ŒŒ‹ŠŠ‰‰ˆ‡††‡††…„„„ƒƒ‚zu…‰„…‰Œ†ŠŠˆƒ€‚|zz{ywyz{{|€ƒ…‡ˆ‰Š‹Š‹Š‹‹ŠŠ‹‹Š‰ˆ‡††……„„ƒ„ƒ‚ƒ‚€zr~‡‚…ŒŒ„†‰‡ƒ~zyxyxuvxyxy|}~€‚„……‡‡ˆ‡ˆ‰ˆ‡ˆ‡‡††……„ƒ‚‚€€€€€{s{†‚ƒ‰‹„…ˆ†ƒ~{zyzzwxyz{{~€‚„†‡‡ˆ‰‰Š‰Š‰Š‰Š‰‰ˆ‡ˆ‡†…„…„„ƒ„ƒ„„ƒ‚ƒ‚wz‡…ƒ…Šˆˆ‹‰†‚~|||}{z|}~}‚ƒ…‡ˆ‰ŠŠ‹ŒŒ‹Œ‹Œ‹‹‹Š‰‰ˆˆ‡†††…†…†…„…„…„‚{{ˆˆ…†ŠŠ‡ŠŠˆ„„…~€}}€€„……‡ˆŠŠ‹‹ŒŒŒ‹‹Š‹Š‹ŠŠ‰ˆˆ‡‡†……††…†…„…„…„‚{|‡‡„†ŠŽ‰‡‰ˆ‡ƒ‚‚€~|}~{{|}~€‚ƒ„…‡ˆ‰ˆŠŒ‹Š‹Š‹Š‰Š‰‰ˆ‰ˆ‡‡†…††…†…†……„„„‚{|‡†„…‰Œˆ‡‰ˆ‡‚‚ƒ€~}~}}~€‚ƒ„……‡‰‰‰Š‹Š‹Œ‹‹‹Š‹Š‰‰ˆˆˆ‡†…†‡††…†…††…†…„„ƒ„ƒ„„„ƒ‚}ˆ†††Š‹‡‡‰ˆ‡„„„ƒ‚‚‚‚ƒ‚ƒ„…†…†‡ˆ‰ˆ‰ˆ‰‰Š‰Š‰‰Š‰ˆ‰ˆˆ‡ˆ‡‡†………††…†……†…„…„…„…„……„…„…„…„…„…†…†…††…†…†……†…†…†…†………†…†…†…†…†………„……†…………†…„…„…„…„……„„…„ƒ„…„…„…„…„ƒƒ„„„ƒ„ƒ„„ƒ„ƒ„„ƒ„ƒ„…„ƒƒ„ƒƒ…„ƒ„ƒ„ƒ„ƒ„„ƒ„ƒ„ƒ„……„„…„„ƒ„ƒ„ƒ„…„…„ƒ„ƒƒ„ƒ„ƒ„„ƒ„„ƒ„ƒ„ƒ„ƒ„„„„„ƒ„…„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„„ƒ„ƒ„…„…„…„„ƒ„ƒ„„…„ƒ„ƒ„ƒ„ƒ„„„ƒ„„ƒƒ„„ƒ„ƒƒ„ƒ„ƒ„ƒƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒ‚ƒƒ‚ƒ‚ƒ‚ƒ‚ƒƒ‚‚‚ƒƒ‚ƒ‚ƒ‚‚‚ƒƒ‚ƒƒ‚ƒƒ‚ƒ‚‚‚‚‚‚‚€€€€€€€€‚‚‚€€€‚‚€€‚‚‚‚‚‚‚‚‚‚‚‚‚‚‚ƒ‚ƒ‚‚‚‚ƒƒƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„ƒ„…„…„…„…„…„…„…†…„…„…„…„…„…„……†…†…†…††…†…†‡†‡†‡†‡‡†‡†‡†‡†‡‡ˆ‡ˆ‡‡†‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆˆˆˆ‰ˆˆ‰ˆ‰‰ˆ‰ˆ‰ˆ‰ˆˆˆ‰ˆˆˆˆ‡ˆˆ‰ˆˆ‰‡ˆ‰ˆ‰‰‰ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰‰ˆ‰‰‰‰‰ˆ‰ˆ‰ˆˆ‰ˆ‡ˆ‰ˆ‰ˆ‰ˆ‡‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‰ˆ‰ˆ‰ˆ‰ˆˆˆˆ‰ˆ‰‰ˆ‡ˆ‡ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰ˆ‰ˆˆ‰ˆˆ‰ˆ‰ˆˆ‡ˆ‡ˆ‡ˆ‡ˆˆ‡ˆ‡ˆˆˆˆ‡ˆˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡ˆ‡†‡‡†‡ˆ‡ˆ‡ˆˆˆˆ‡ˆ‡ˆ‡ˆ‡†‡‡†‡ˆ‡‡‡†‡‡ˆ‡‡‡‡†‡†‡†‡††‡†‡†‡†‡‡†‡††‡†‡†‡†††…†…††…†…†…†…†…††…†…†…„……„…„…„ƒƒ„„ƒ„ƒ„„ƒ„ƒ„ƒ‚ƒ‚ƒ„ƒ„ƒ„ƒƒ‚ƒ‚ƒ‚ƒ‚ƒ‚‚ƒ‚‚ƒƒ‚‚ƒ‚ƒ‚‚‚‚€€€€ \ No newline at end of file diff --git a/tests/driver_dac_dds/main.c b/tests/driver_dac_dds/main.c new file mode 100644 index 0000000000..138ec49203 --- /dev/null +++ b/tests/driver_dac_dds/main.c @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief DAC (audio) test application + * + * Generates Sine, Square and Sawtooth waves using + * a DAC. + * Connect a speaker or headphones to the DAC output + * pins of your board, you should be able to hear the + * generated sounds. + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include +#include + +#include "mutex.h" +#include "dac_dds.h" +#include "dac_dds_params.h" +#include "shell.h" + +#include "blob/hello.raw.h" + +#ifndef DAC_DDS_CHAN +#define DAC_DDS_CHAN 0 +#endif + +#ifndef ENABLE_GREETING +#define ENABLE_GREETING 1 +#endif + +#ifndef min +#define min(a, b) ((a) > (b) ? (b) : (a)) +#endif + +#define DAC_BUF_SIZE (2048) + +static bool res_16b = 0; +static unsigned sample_rate = 8000; + +#define ISIN_PERIOD (0x7FFF) +#define ISIN_MAX (0x1000) + +/** + * @brief A sine approximation via a fourth-order cosine approx. + * source: https://www.coranac.com/2009/07/sines/ + * + * @param x angle (with 2^15 units/circle) + * @return sine value (Q12) + */ +static int32_t isin(int32_t x) +{ + int32_t c, y; + static const int32_t qN = 13, + qA = 12, + B = 19900, + C = 3516; + + c = x << (30 - qN); /* Semi-circle info into carry. */ + x -= 1 << qN; /* sine -> cosine calc */ + + x = x << (31 - qN); /* Mask with PI */ + x = x >> (31 - qN); /* Note: SIGNED shift! (to qN) */ + x = x * x >> (2 * qN - 14); /* x=x^2 To Q14 */ + + y = B - (x * C >> 14); /* B - x^2*C */ + y = (1 << qA) /* A - x^2*(B-x^2*C) */ + - (x * y >> 16); + + return c >= 0 ? y : -y; +} + +/* simple function to fill buffer with samples */ +typedef void (*sample_gen_t)(uint8_t *dst, size_t len, uint16_t period); + +static void _fill_saw_samples_8(uint8_t *buf, size_t len, uint16_t period) +{ + uint8_t x = 0; + unsigned step = 0xFF / period; + + for (uint16_t i = 0; i < len; ++i) { + x += step; + buf[i] = x; + } +} + +static void _fill_saw_samples_16(uint8_t *buf, size_t len, uint16_t period) +{ + uint16_t x = 0; + unsigned step = 0xFFFF / period; + + for (uint16_t i = 0; i < len; ++i) { + x += step; + buf[i] = x; + buf[++i] = x >> 8; + } +} + +static void _fill_sine_samples_8(uint8_t *buf, size_t len, uint16_t period) +{ + uint16_t x = 0; + unsigned step = ISIN_PERIOD / period; + + for (uint16_t i = 0; i < period; ++i) { + x += step; + buf[i] = isin(x) >> 5; + } + + for (uint16_t i = period; i < len; i += period) { + memcpy(&buf[i], &buf[0], period); + } +} + +static void _fill_sine_samples_16(uint8_t *buf, size_t len, uint16_t period) +{ + uint16_t x = 0; + unsigned step = ISIN_PERIOD / period; + + period *= 2; + + for (uint16_t i = 0; i < period; ++i) { + x += step; + + uint16_t y = isin(x); + buf[i] = y; + buf[++i] = y >> 8; + } + + for (uint16_t i = period; i < len; i += period) { + memcpy(&buf[i], &buf[0], period); + } +} + +static void _fill_square_samples(uint8_t *buf, size_t len, uint16_t period) +{ + period /= 2; + + if (res_16b) { + period *= 2; + } + + for (uint8_t *end = buf + len; buf < end; ) { + memset(buf, 0xFF, period); + buf += period; + memset(buf, 0x0, period); + buf += period; + } +} + +static void _unlock(void *arg) +{ + mutex_unlock(arg); +} + +static void play_function(uint16_t period, uint32_t samples, sample_gen_t fun) +{ + static uint8_t buf[DAC_BUF_SIZE]; + mutex_t lock = MUTEX_INIT_LOCKED; + + /* only work with whole wave periods */ + uint16_t len_aligned = DAC_BUF_SIZE - DAC_BUF_SIZE % period; + + /* 16 bit samples doubles data rate */ + if (res_16b) { + samples *= 2; + } + + /* pre-calculate buffer */ + fun(buf, len_aligned, period); + + /* we want to block until the next buffer can be queued */ + dac_dds_set_cb(DAC_DDS_CHAN, _unlock, &lock); + + while (samples) { + size_t len = min(samples, len_aligned); + samples -= len; + + dac_dds_play(DAC_DDS_CHAN, buf, len); + + /* wait for buffer flip */ + mutex_lock(&lock); + } +} + +#if IS_USED(ENABLE_GREETING) +static int cmd_greeting(int argc, char **argv) +{ + (void) argc; + (void) argv; + + if (sample_rate != 8000 || res_16b) { + puts("Warning: audio clip was recoded with 8bit/8000 Hz"); + } + + puts("Play Greeting…"); + + /* we only want to play a single sample */ + dac_dds_set_cb(DAC_DDS_CHAN, NULL, NULL); + + dac_dds_play(DAC_DDS_CHAN, hello_raw, hello_raw_len); + + return 0; +} +#endif + +static void _dac_init(void) +{ + printf("init DAC with %d bit, %u Hz\n", res_16b ? 16 : 8, sample_rate); + dac_dds_init(DAC_DDS_CHAN, sample_rate, + res_16b ? DAC_FLAG_16BIT : DAC_FLAG_8BIT, NULL, NULL); +} + +static int cmd_init(int argc, char **argv) +{ + printf("argc: %d\n", argc); + + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + if (argc > 2) { + unsigned bit = atoi(argv[2]); + + if (bit != 8 && bit != 16) { + printf("Only 8 and 16 bit samples supported.\n"); + return 1; + } + + res_16b = bit == 16; + } + + sample_rate = atoi(argv[1]); + + _dac_init(); + + return 0; +} + +static int cmd_saw(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + unsigned freq = atoi(argv[1]); + unsigned secs = atoi(argv[2]); + + play_function((sample_rate + freq/2) / freq, secs * sample_rate, + res_16b ? _fill_saw_samples_16 : _fill_saw_samples_8); + + return 0; +} + +static int cmd_sine(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + unsigned freq = atoi(argv[1]); + unsigned secs = atoi(argv[2]); + + play_function((sample_rate + freq/2) / freq, secs * sample_rate, + res_16b ? _fill_sine_samples_16 : _fill_sine_samples_8); + + return 0; +} + +static int cmd_square(int argc, char **argv) +{ + if (argc < 3) { + printf("usage: %s \n", argv[0]); + return 1; + } + + unsigned freq = atoi(argv[1]); + unsigned secs = atoi(argv[2]); + + play_function((sample_rate + freq/2) / freq, secs * sample_rate, + _fill_square_samples); + + return 0; +} + +static const shell_command_t shell_commands[] = { +#if IS_USED(ENABLE_GREETING) + { "hello", "Play Greeting", cmd_greeting }, +#endif + { "init", "Initialize DAC", cmd_init }, + { "saw", "Play sawtooth wave", cmd_saw }, + { "sine", "Play Sine wave", cmd_sine }, + { "square", "Play Square wave", cmd_square }, + { NULL, NULL, NULL } +}; + +int main(void) +{ + dac_init(DAC_DDS_PARAM_DAC); + _dac_init(); + + /* start the shell */ + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + + return 0; +}