diff --git a/components/display/display.c b/components/display/display.c index 24ea5c93..8ccfa419 100644 --- a/components/display/display.c +++ b/components/display/display.c @@ -117,7 +117,7 @@ static void displayer_task(void *args) { if (displayer.state == DISPLAYER_IDLE) display->line(2, 0, DISPLAY_CLEAR | DISPLAY_UPDATE, displayer.string); vTaskSuspend(NULL); scroll_sleep = 0; - display->clear(); + display->clear(true); display->line(1, DISPLAY_LEFT, DISPLAY_UPDATE, displayer.header); } else if (displayer.refresh) { // little trick when switching master while in IDLE and missing it diff --git a/components/display/display.h b/components/display/display.h index c8753254..31f0f5e7 100644 --- a/components/display/display.h +++ b/components/display/display.h @@ -51,8 +51,9 @@ enum displayer_time_e { DISPLAYER_ELAPSED, DISPLAYER_REMAINING }; // don't change anything there w/o changing all drivers init code extern struct display_s { int width, height; + bool dirty; bool (*init)(char *config, char *welcome); - void (*clear)(void); + void (*clear)(bool full, ...); bool (*set_font)(int num, enum display_font_e font, int space); void (*on)(bool state); void (*brightness)(uint8_t level); @@ -60,8 +61,10 @@ extern struct display_s { bool (*line)(int num, int x, int attribute, char *text); int (*stretch)(int num, char *string, int max); void (*update)(void); - void (*draw)(int x1, int y1, int x2, int y2, bool by_column, uint8_t *data); - void (*draw_cbr)(uint8_t *data, int height); // height is the # of columns in data, as oppoosed to display height (0 = display height) + void (*draw_raw)(int x1, int y1, int x2, int y2, bool by_column, bool MSb, uint8_t *data); + void (*draw_cbr)(uint8_t *data, int width, int height); // width and height is the # of rows/columns in data, as opposed to display height (0 = display width, 0 = display height) + void (*draw_line)(int x1, int y1, int x2, int y2); + void (*draw_box)( int x1, int y1, int x2, int y2, bool fill); } *display; void displayer_scroll(char *string, int speed); diff --git a/components/display/driver_SSD13x6.c b/components/display/driver_SSD13x6.c index 252225ca..baec7144 100644 --- a/components/display/driver_SSD13x6.c +++ b/components/display/driver_SSD13x6.c @@ -38,25 +38,26 @@ static const char *TAG = "display"; // handlers static bool init(char *config, char *welcome); -static void clear(void); +static void clear(bool full, ...); static bool set_font(int num, enum display_font_e font, int space); static void text(enum display_font_e font, enum display_pos_e pos, int attribute, char *text, ...); static bool line(int num, int x, int attribute, char *text); static int stretch(int num, char *string, int max); -static void draw_cbr(u8_t *data, int height); -static void draw(int x1, int y1, int x2, int y2, bool by_column, u8_t *data); +static void draw_cbr(u8_t *data, int width, int height); +static void draw_raw(int x1, int y1, int x2, int y2, bool by_column, bool MSb, u8_t *data); +static void draw_line(int x1, int y1, int x2, int y2); +static void draw_box( int x1, int y1, int x2, int y2, bool fill); static void brightness(u8_t level); static void on(bool state); static void update(void); // display structure for others to use -struct display_s SSD13x6_display = { 0, 0, +struct display_s SSD13x6_display = { 0, 0, true, init, clear, set_font, on, brightness, - text, line, stretch, update, draw, draw_cbr, NULL }; + text, line, stretch, update, draw_raw, draw_cbr, draw_line, draw_box }; // SSD13x6 specific function static struct SSD13x6_Device Display; -static SSD13x6_AddressMode AddressMode = AddressMode_Invalid; static const unsigned char BitReverseTable256[] = { @@ -150,9 +151,24 @@ static bool init(char *config, char *welcome) { /**************************************************************************************** * */ -static void clear(void) { - SSD13x6_Clear( &Display, SSD_COLOR_BLACK ); - SSD13x6_Update( &Display ); +static void clear(bool full, ...) { + bool commit = true; + + if (full) { + SSD13x6_Clear( &Display, SSD_COLOR_BLACK ); + } else { + va_list args; + va_start(args, full); + commit = va_arg(args, int); + int x1 = va_arg(args, int), y1 = va_arg(args, int), x2 = va_arg(args, int), y2 = va_arg(args, int); + if (x2 < 0) x2 = display->width - 1; + if (y2 < 0) y2 = display->height - 1; + SSD13x6_ClearWindow( &Display, x1, y1, x2, y2, SSD_COLOR_BLACK ); + va_end(args); + } + + if (commit) update(); + else SSD13x6_display.dirty = true; } /**************************************************************************************** @@ -209,12 +225,6 @@ static bool line(int num, int x, int attribute, char *text) { // counting 1..n num--; - // always horizontal mode for text display - if (AddressMode != AddressMode_Horizontal) { - AddressMode = AddressMode_Horizontal; - SSD13x6_SetDisplayAddressMode( &Display, AddressMode ); - } - SSD13x6_SetFont( &Display, lines[num].font ); if (attribute & DISPLAY_MONOSPACE) SSD13x6_FontForceMonospace( &Display, true ); @@ -237,7 +247,8 @@ static bool line(int num, int x, int attribute, char *text) { ESP_LOGD(TAG, "displaying %s line %u (x:%d, attr:%u)", text, num+1, x, attribute); // update whole display if requested - if (attribute & DISPLAY_UPDATE) SSD13x6_Update( &Display ); + if (attribute & DISPLAY_UPDATE) update(); + else SSD13x6_display.dirty = true; return width + x < Display.Width; } @@ -278,13 +289,14 @@ static int stretch(int num, char *string, int max) { static void text(enum display_font_e font, enum display_pos_e pos, int attribute, char *text, ...) { va_list args; - va_start(args, text); TextAnchor Anchor = TextAnchor_Center; if (attribute & DISPLAY_CLEAR) SSD13x6_Clear( &Display, SSD_COLOR_BLACK ); if (!text) return; + va_start(args, text); + switch(font) { case DISPLAY_FONT_LINE_1: SSD13x6_SetFont( &Display, &Font_line_1 ); @@ -327,127 +339,82 @@ static void text(enum display_font_e font, enum display_pos_e pos, int attribute ESP_LOGD(TAG, "SSDD13x6 displaying %s at %u with attribute %u", text, Anchor, attribute); - if (AddressMode != AddressMode_Horizontal) { - AddressMode = AddressMode_Horizontal; - SSD13x6_SetDisplayAddressMode( &Display, AddressMode ); - } - SSD13x6_FontDrawAnchoredString( &Display, Anchor, text, SSD_COLOR_WHITE ); - if (attribute & DISPLAY_UPDATE) SSD13x6_Update( &Display ); + if (attribute & DISPLAY_UPDATE) update(); + else SSD13x6_display.dirty = true; va_end(args); } /**************************************************************************************** - * Process graphic display data from column-oriented bytes, MSbit first + * Process graphic display data from column-oriented data (MSbit first) */ -static void draw_cbr(u8_t *data, int height) { -#ifndef FULL_REFRESH - // force addressing mode by rows - if (AddressMode != AddressMode_Horizontal) { - AddressMode = AddressMode_Horizontal; - SSD13x6_SetDisplayAddressMode( &Display, AddressMode ); +static void draw_cbr(u8_t *data, int width, int height) { + if (!height) height = Display.Height; + if (!width) width = Display.Width; + + // need to do row/col swap and bit-reverse + int rows = height / 8; + for (int r = 0; r < rows; r++) { + uint8_t *optr = Display.Framebuffer + r*Display.Width, *iptr = data + r; + for (int c = width; --c >= 0;) { + *optr++ = BitReverseTable256[*iptr];; + iptr += rows; + } } - // try to minimize I2C traffic which is very slow - int rows = (height ? height : Display.Height) / 8; - for (int r = 0; r < rows; r++) { - uint8_t first = 0, last; - uint8_t *optr = Display.Framebuffer + r*Display.Width, *iptr = data + r; - - // row/col swap, frame buffer comparison and bit-reversing - for (int c = 0; c < Display.Width; c++) { - u8_t byte = BitReverseTable256[*iptr]; - if (byte != *optr) { - if (!first) first = c + 1; - last = c ; - } - *optr++ = byte; - iptr += rows; - } - - // now update the display by "byte rows" - if (first--) { - SSD13x6_SetColumnAddress( &Display, first, last ); - SSD13x6_SetPageAddress( &Display, r, r); - SSD13x6_WriteRawData( &Display, Display.Framebuffer + r*Display.Width + first, last - first + 1); - } - } -#else - if (!height) height = Display->Height; - - SSD13x6_SetPageAddress( &Display, 0, height / 8 - 1); - - // force addressing mode by columns (if we can) - if (SSD13x6_GetCaps( &Display ) & CAPS_ADDRESS_VERTICAL) { - // just copy data in frame buffer with bit-reverse - for (int c = 0; c < Display.Width; c++) - for (int r = 0; r < height / 8; r++) - Display.Framebuffer[c*Display.Height/8 + r] = BitReverseTable256[data[c*height/8 +r]]; - - if (AddressMode != AddressMode_Vertical) { - AddressMode = AddressMode_Vertical; - SSD13x6_SetDisplayAddressMode( &Display, AddressMode ); - } - } else { - // need to do rwo/col swap and bit-reverse - int rows = (height ? height : Display.Height) / 8; - for (int r = 0; r < rows; r++) { - uint8_t *optr = Display.Framebuffer + r*Display.Width, *iptr = data + r; - for (int c = 0; c < Display.Width; c++) { - *optr++ = BitReverseTable256[*iptr];; - iptr += rows; - } - } - ESP_LOGW(TAG, "Can't set addressing mode to vertical, swapping"); - } - - SSD13x6_WriteRawData(&Display, Display.Framebuffer, Display.Width * Display.Height/8); - #endif + SSD13x6_display.dirty = true; } /**************************************************************************************** * Process graphic display data MSBit first * WARNING: this has not been tested yet */ -static void draw(int x1, int y1, int x2, int y2, bool by_column, u8_t *data) { - - if (y1 % 8 || y2 % 8) { - ESP_LOGW(TAG, "must write rows on byte boundaries (%u,%u) to (%u,%u)", x1, y1, x2, y2); - return; - } - +static void draw_raw(int x1, int y1, int x2, int y2, bool by_column, bool MSb, u8_t *data) { // default end point to display size if (x2 == -1) x2 = Display.Width - 1; if (y2 == -1) y2 = Display.Height - 1; - // set addressing mode to match data - if (by_column) { - - if (AddressMode != AddressMode_Vertical) { - AddressMode = AddressMode_Vertical; - SSD13x6_SetDisplayAddressMode( &Display, AddressMode ); - } - - // copy the window and do row/col exchange - for (int r = y1/8; r <= y2/8; r++) { - uint8_t *optr = Display.Framebuffer + r*Display.Width + x1, *iptr = data + r; - for (int c = x1; c <= x2; c++) { - *optr++ = *iptr; - iptr += (y2-y1)/8 + 1; + display->dirty = true; + + // not a boundary draw + if (y1 % 8 || y2 % 8 || x1 % 8 | x2 % 8) { + ESP_LOGW(TAG, "can't write on non cols/rows boundaries for now"); + } else { + // set addressing mode to match data + if (by_column) { + // copy the window and do row/col exchange + for (int r = y1/8; r <= y2/8; r++) { + uint8_t *optr = Display.Framebuffer + r*Display.Width + x1, *iptr = data + r; + for (int c = x1; c <= x2; c++) { + *optr++ = MSb ? BitReverseTable256[*iptr] : *iptr; + iptr += (y2-y1)/8 + 1; } - } - } else { - // just copy the window inside the frame buffer - for (int r = y1/8; r <= y2/8; r++) { - uint8_t *optr = Display.Framebuffer + r*Display.Width + x1, *iptr = data + r*(x2-x1+1); - for (int c = x1; c <= x2; c++) *optr++ = *iptr++; - } - } - - SSD13x6_SetColumnAddress( &Display, x1, x2); - SSD13x6_SetPageAddress( &Display, y1/8, y2/8); - SSD13x6_WriteRawData( &Display, data, (x2-x1 + 1) * ((y2-y1)/8 + 1)); + } + } else { + // just copy the window inside the frame buffer + for (int r = y1/8; r <= y2/8; r++) { + uint8_t *optr = Display.Framebuffer + r*Display.Width + x1, *iptr = data + r*(x2-x1+1); + for (int c = x1; c <= x2; c++) *optr++ = *iptr++; + } + } + } +} + +/**************************************************************************************** + * Draw line + */ +static void draw_line( int x1, int y1, int x2, int y2) { + SSD13x6_DrawLine( &Display, x1, y1, x2, y2, SSD_COLOR_WHITE ); + SSD13x6_display.dirty = true; +} + +/**************************************************************************************** + * Draw Box + */ +static void draw_box( int x1, int y1, int x2, int y2, bool fill) { + SSD13x6_DrawBox( &Display, x1, y1, x2, y2, SSD_COLOR_WHITE, fill ); + SSD13x6_display.dirty = true; } /**************************************************************************************** @@ -470,7 +437,8 @@ static void on(bool state) { * Update */ static void update(void) { - SSD13x6_Update( &Display ); + if (SSD13x6_display.dirty) SSD13x6_Update( &Display ); + SSD13x6_display.dirty = false; } diff --git a/components/display/tarablessd13x6/ssd13x6.c b/components/display/tarablessd13x6/ssd13x6.c index ea1a0836..0ba7139f 100644 --- a/components/display/tarablessd13x6/ssd13x6.c +++ b/components/display/tarablessd13x6/ssd13x6.c @@ -15,6 +15,8 @@ #include "ssd13x6.h" +#define SHADOW_BUFFER + // used by both but different static uint8_t SSDCmd_Set_Display_Start_Line; static uint8_t SSDCmd_Set_Display_Offset; @@ -111,8 +113,32 @@ void SSD13x6_SetDisplayAddressMode( struct SSD13x6_Device* DeviceHandle, SSD13x6 } void SSD13x6_Update( struct SSD13x6_Device* DeviceHandle ) { +#ifdef SHADOW_BUFFER + // not sure the compiler does not have to redo all calculation in for loops, so local it is + int width = DeviceHandle->Width, rows = DeviceHandle->Height / 8; + uint8_t *optr = DeviceHandle->Shadowbuffer, *iptr = DeviceHandle->Framebuffer; + + // by row, find first and last columns that have been updated + for (int r = 0; r < rows; r++) { + uint8_t first = 0, last; + for (int c = 0; c < width; c++) { + if (*iptr != *optr) { + if (!first) first = c + 1; + last = c ; + } + *optr++ = *iptr++; + } + + // now update the display by "byte rows" + if (first--) { + SSD13x6_SetColumnAddress( DeviceHandle, first, last ); + SSD13x6_SetPageAddress( DeviceHandle, r, r); + SSD13x6_WriteData( DeviceHandle, DeviceHandle->Shadowbuffer + r*width + first, last - first + 1); + } + } +#else if (DeviceHandle->Model == SH1106) { - // SH1106 requires a page-by-page update and ahs no end Page/Column + // SH1106 requires a page-by-page update and has no end Page/Column for (int i = 0; i < DeviceHandle->Height / 8 ; i++) { SSD13x6_SetPageAddress( DeviceHandle, i, 0); SSD13x6_SetColumnAddress( DeviceHandle, 0, 0); @@ -124,6 +150,7 @@ void SSD13x6_Update( struct SSD13x6_Device* DeviceHandle ) { SSD13x6_SetPageAddress( DeviceHandle, 0, DeviceHandle->Height / 8 - 1); SSD13x6_WriteData( DeviceHandle, DeviceHandle->Framebuffer, DeviceHandle->FramebufferSize ); } +#endif } void SSD13x6_WriteRawData( struct SSD13x6_Device* DeviceHandle, uint8_t* Data, size_t DataLength ) { @@ -214,6 +241,11 @@ static bool SSD13x6_Init( struct SSD13x6_Device* DeviceHandle, int Width, int He DeviceHandle->Width = Width; DeviceHandle->Height = Height; +#ifdef SHADOW_BUFFER + DeviceHandle->Shadowbuffer = heap_caps_malloc( DeviceHandle->FramebufferSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA ); + memset( DeviceHandle->Shadowbuffer, 0xFF, DeviceHandle->FramebufferSize ); +#endif + SSD13x6_HWReset( DeviceHandle ); SSD13x6_DisplayOff( DeviceHandle ); @@ -308,7 +340,11 @@ bool SSD13x6_Init_SPI( struct SSD13x6_Device* DeviceHandle, int Width, int Heigh DeviceHandle->CSPin = CSPin; DeviceHandle->FramebufferSize = ( Width * Height ) / 8; +#ifdef SHADOW_BUFFER + DeviceHandle->Framebuffer = calloc( 1, DeviceHandle->FramebufferSize ); +#else DeviceHandle->Framebuffer = heap_caps_calloc( 1, DeviceHandle->FramebufferSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA ); +#endif NullCheck( DeviceHandle->Framebuffer, return false ); return SSD13x6_Init( DeviceHandle, Width, Height ); diff --git a/components/display/tarablessd13x6/ssd13x6.h b/components/display/tarablessd13x6/ssd13x6.h index 95a814a1..697eb02a 100644 --- a/components/display/tarablessd13x6/ssd13x6.h +++ b/components/display/tarablessd13x6/ssd13x6.h @@ -58,7 +58,7 @@ struct SSD13x6_Device { enum { SSD1306, SSD1326, SH1106 } Model; uint8_t ReMap; - uint8_t* Framebuffer; + uint8_t* Framebuffer, *Shadowbuffer; int FramebufferSize; WriteCommandProc WriteCommand; diff --git a/components/display/tarablessd13x6/ssd13x6_draw.c b/components/display/tarablessd13x6/ssd13x6_draw.c index 9467abf8..9a4d9f91 100644 --- a/components/display/tarablessd13x6/ssd13x6_draw.c +++ b/components/display/tarablessd13x6/ssd13x6_draw.c @@ -16,6 +16,9 @@ #include "ssd13x6.h" #include "ssd13x6_draw.h" +#undef NullCheck +#define NullCheck(X,Y) + __attribute__( ( always_inline ) ) static inline bool IsPixelVisible( struct SSD13x6_Device* DeviceHandle, int x, int y ) { bool Result = ( ( x >= 0 ) && @@ -75,7 +78,7 @@ void IRAM_ATTR SSD13x6_DrawHLine( struct SSD13x6_Device* DeviceHandle, int x, in NullCheck( DeviceHandle, return ); NullCheck( DeviceHandle->Framebuffer, return ); - for ( ; x <= XEnd; x++ ) { + for ( ; x < XEnd; x++ ) { if ( IsPixelVisible( DeviceHandle, x, y ) == true ) { SSD13x6_DrawPixelFast( DeviceHandle, x, y, Color ); } else { @@ -90,7 +93,7 @@ void IRAM_ATTR SSD13x6_DrawVLine( struct SSD13x6_Device* DeviceHandle, int x, in NullCheck( DeviceHandle, return ); NullCheck( DeviceHandle->Framebuffer, return ); - for ( ; y <= YEnd; y++ ) { + for ( ; y < YEnd; y++ ) { if ( IsPixelVisible( DeviceHandle, x, y ) == true ) { SSD13x6_DrawPixel( DeviceHandle, x, y, Color ); } else { @@ -114,7 +117,7 @@ static inline void IRAM_ATTR DrawWideLine( struct SSD13x6_Device* DeviceHandle, Error = ( dy * 2 ) - dx; - for ( ; x <= x1; x++ ) { + for ( ; x < x1; x++ ) { if ( IsPixelVisible( DeviceHandle, x, y ) == true ) { SSD13x6_DrawPixelFast( DeviceHandle, x, y, Color ); } @@ -219,3 +222,32 @@ void SSD13x6_Clear( struct SSD13x6_Device* DeviceHandle, int Color ) { memset( DeviceHandle->Framebuffer, Color, DeviceHandle->FramebufferSize ); } + +void SSD13x6_ClearWindow( struct SSD13x6_Device* DeviceHandle, int x1, int y1, int x2, int y2, int Color ) { + NullCheck( DeviceHandle, return ); + NullCheck( DeviceHandle->Framebuffer, return ); + +/* + int xr = ((x1 - 1) / 8) + 1 ) * 8; + int xl = (x2 / 8) * 8; + + for (int y = y1; y <= y2; y++) { + for (int x = x1; x < xr; x++) SSD13x6_DrawPixelFast( DeviceHandle, x, y, Color); + if (xl > xr) memset( DeviceHandle->Framebuffer + (y / 8) * DeviceHandle->Width + xr, 0, xl - xr ); + for (int x = xl; x <= x2; x++) SSD13x6_DrawPixelFast( DeviceHandle, x, y, Color); + } + + return; +*/ + + // cheap optimization on boundaries + if (x1 == 0 && x2 == DeviceHandle->Width - 1 && y1 % 8 == 0 && (y2 + 1) % 8 == 0) { + memset( DeviceHandle->Framebuffer + (y1 / 8) * DeviceHandle->Width, 0, (y2 - y1 + 1) / 8 * DeviceHandle->Width ); + } else { + for (int y = y1; y <= y2; y++) { + for (int x = x1; x <= x2; x++) { + SSD13x6_DrawPixelFast( DeviceHandle, x, y, Color); + } + } + } +} diff --git a/components/display/tarablessd13x6/ssd13x6_draw.h b/components/display/tarablessd13x6/ssd13x6_draw.h index 18cc35cb..63196a40 100644 --- a/components/display/tarablessd13x6/ssd13x6_draw.h +++ b/components/display/tarablessd13x6/ssd13x6_draw.h @@ -39,6 +39,7 @@ extern "C" { #define SSD_COLOR_XOR 2 void SSD13x6_Clear( struct SSD13x6_Device* DeviceHandle, int Color ); +void SSD13x6_ClearWindow( struct SSD13x6_Device* DeviceHandle, int x1, int y1, int x2, int y2, int Color ); void SSD13x6_DrawPixel( struct SSD13x6_Device* DeviceHandle, int X, int Y, int Color ); void SSD13x6_DrawPixelFast( struct SSD13x6_Device* DeviceHandle, int X, int Y, int Color ); void SSD13x6_DrawHLine( struct SSD13x6_Device* DeviceHandle, int x, int y, int Width, int Color ); diff --git a/components/services/monitor.c b/components/services/monitor.c index b68bfe88..7d421414 100644 --- a/components/services/monitor.c +++ b/components/services/monitor.c @@ -146,4 +146,10 @@ void monitor_svc_init(void) { xTimerStart(monitor_timer, portMAX_DELAY); } free(p); + + ESP_LOGI(TAG, "Heap internal:%zu (min:%zu) external:%zu (min:%zu)", + heap_caps_get_free_size(MALLOC_CAP_INTERNAL), + heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL), + heap_caps_get_free_size(MALLOC_CAP_SPIRAM), + heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)); } diff --git a/components/squeezelite/display.c b/components/squeezelite/display.c index 7342f846..2a982f48 100644 --- a/components/squeezelite/display.c +++ b/components/squeezelite/display.c @@ -18,6 +18,8 @@ */ #include +#include +#include "esp_dsp.h" #include "squeezelite.h" #include "slimproto.h" #include "display.h" @@ -54,6 +56,21 @@ struct grfg_packet { u16_t width; // # of pixels of scrollable }; +struct visu_packet { + char opcode[4]; + u8_t which; + u8_t count; + union { + u32_t width; + u32_t full_bars; + }; + u32_t height; + s32_t col; + s32_t row; + u32_t border; + u32_t small_bars; +}; + struct ANIC_header { char opcode[4]; u32_t length; @@ -64,20 +81,59 @@ struct ANIC_header { extern struct outputstate output; +static struct { + TaskHandle_t task; + SemaphoreHandle_t mutex; +} displayer; + +#define LONG_WAKE (10*1000) +#define SB_HEIGHT 32 + +// lenght are number of frames, i.e. 2 channels of 16 bits +#define FFT_LEN_BIT 6 +#define FFT_LEN (1 << FFT_LEN_BIT) +#define RMS_LEN_BIT 6 +#define RMS_LEN (1 << RMS_LEN_BIT) + +// actually this is 2x the displayed BW +#define DISPLAY_BW 32000 + static struct scroller_s { // copy of grfs content - u8_t screen, direction; - u32_t pause, speed; - u16_t by, mode, full_width, window_width; - u16_t max, size; + u8_t screen; + u32_t pause, speed; + int wake; + u16_t mode; + s16_t by; // scroller management & sharing between grfg and scrolling task - TaskHandle_t task; - u8_t *scroll_frame, *back_frame; - bool active, updated; - u8_t *scroll_ptr; - int scroll_len, scroll_step; + bool active, first; + int scrolled; + struct { + u8_t *frame; + u32_t width; + u32_t max, size; + } scroll; + struct { + u8_t *frame; + u32_t width; + } back; + u8_t *frame; + u32_t width; } scroller; +#define MAX_BARS 32 +static EXT_RAM_ATTR struct { + int bar_gap, bar_width, bar_border; + struct { + int current; + int max; + } bars[MAX_BARS]; + int n, col, row, height, width, border; + enum { VISU_BLANK, VISU_VUMETER, VISU_SPECTRUM, VISU_WAVEFORM } mode; + int speed, wake; + float fft[FFT_LEN*2], samples[FFT_LEN*2], hanning[FFT_LEN]; +} visu; + #define ANIM_NONE 0x00 #define ANIM_TRANSITION 0x01 // A transition animation has finished #define ANIM_SCROLL_ONCE 0x02 @@ -91,11 +147,11 @@ static u8_t SETD_width; #define LINELEN 40 static log_level loglevel = lINFO; -static SemaphoreHandle_t display_mutex; static bool (*slimp_handler_chain)(u8_t *data, int len); static void (*slimp_loop_chain)(void); static void (*notify_chain)(in_addr_t ip, u16_t hport, u16_t cport); static int display_width, display_height; +static bool display_dirty = true; #define max(a,b) (((a) > (b)) ? (a) : (b)) @@ -107,7 +163,9 @@ static void grfe_handler( u8_t *data, int len); static void grfb_handler(u8_t *data, int len); static void grfs_handler(u8_t *data, int len); static void grfg_handler(u8_t *data, int len); -static void scroll_task(void* arg); +static void visu_handler( u8_t *data, int len); + +static void displayer_task(void* arg); /* scrolling undocumented information grfs @@ -151,17 +209,24 @@ bool sb_display_init(void) { // need to force height to 32 maximum display_width = display->width; - display_height = min(display->height, 32); + display_height = min(display->height, SB_HEIGHT); SETD_width = display->width; - + + // create visu configuration + visu.bar_gap = 1; + visu.speed = 100; + dsps_fft2r_init_fc32(visu.fft, FFT_LEN); + dsps_wind_hann_f32(visu.hanning, FFT_LEN); + // create scroll management task - display_mutex = xSemaphoreCreateMutex(); - scroller.task = xTaskCreateStatic( (TaskFunction_t) scroll_task, "scroll_thread", SCROLL_STACK_SIZE, NULL, ESP_TASK_PRIO_MIN + 1, xStack, &xTaskBuffer); + displayer.mutex = xSemaphoreCreateMutex(); + displayer.task = xTaskCreateStatic( (TaskFunction_t) displayer_task, "displayer_thread", SCROLL_STACK_SIZE, NULL, ESP_TASK_PRIO_MIN + 1, xStack, &xTaskBuffer); // size scroller - scroller.max = (display_width * display_height / 8) * 10; - scroller.scroll_frame = malloc(scroller.max); - scroller.back_frame = malloc(display_width * display_height / 8); + scroller.scroll.max = (display_width * display_height / 8) * 10; + scroller.scroll.frame = malloc(scroller.scroll.max); + scroller.back.frame = malloc(display_width * display_height / 8); + scroller.frame = malloc(display_width * display_height / 8); // chain handlers slimp_handler_chain = slimp_handler; @@ -225,6 +290,8 @@ static void server(in_addr_t ip, u16_t hport, u16_t cport) { char msg[32]; sprintf(msg, "%s:%hu", inet_ntoa(ip), hport); display->text(DISPLAY_FONT_DEFAULT, DISPLAY_CENTERED, DISPLAY_CLEAR | DISPLAY_UPDATE, msg); + SETD_width = display->width; + display_dirty = true; if (notify_chain) (*notify_chain)(ip, hport, cport); } @@ -246,6 +313,8 @@ static bool handler(u8_t *data, int len){ grfs_handler(data, len); } else if (!strncmp((char*) data, "grfg", 4)) { grfg_handler(data, len); + } else if (!strncmp((char*) data, "visu", 4)) { + visu_handler(data, len); } else { res = false; } @@ -374,12 +443,27 @@ static void vfdc_handler( u8_t *_data, int bytes_read) { * Process graphic display data */ static void grfe_handler( u8_t *data, int len) { - xSemaphoreTake(display_mutex, portMAX_DELAY); + + xSemaphoreTake(displayer.mutex, portMAX_DELAY); scroller.active = false; - display->draw_cbr(data + sizeof(struct grfe_packet), display_height); - xSemaphoreGive(display_mutex); + // if we are displaying visu on a small screen, do not do screen update + if (visu.mode && !visu.col && visu.row < SB_HEIGHT) { + xSemaphoreGive(displayer.mutex); + return; + } + + // did we have something that might have write on the bottom of a SB_HEIGHT+ display + if (display_dirty) { + display->clear(true); + display_dirty = false; + } + + display->draw_cbr(data + sizeof(struct grfe_packet), display_width, display_height); + display->update(); + + xSemaphoreGive(displayer.mutex); LOG_DEBUG("grfe frame %u", len); } @@ -415,36 +499,46 @@ static void grfs_handler(u8_t *data, int len) { htonl(pkt->pause), // in ms htonl(pkt->speed), // in ms htons(pkt->by), // # of pixel of scroll step - htons(pkt->mode), // 0=continuous, 1=once and stop, 2=once and end - htons(pkt->width), // total width of animation + htons(pkt->mode), // 0=continuous, 1=once and stop, 2=once and end + htons(pkt->width), // last column of animation that contains a "full" screen htons(pkt->offset) // offset if multiple packets are sent ); // new grfs frame, build scroller info if (!offset) { // use the display as a general lock - xSemaphoreTake(display_mutex, portMAX_DELAY); + xSemaphoreTake(displayer.mutex, portMAX_DELAY); // copy & set scroll parameters scroller.screen = pkt->screen; - scroller.direction = pkt->direction; scroller.pause = htonl(pkt->pause); scroller.speed = htonl(pkt->speed); - scroller.by = htons(pkt->by); scroller.mode = htons(pkt->mode); - scroller.full_width = htons(pkt->width); - scroller.updated = scroller.active = true; + scroller.scroll.width = htons(pkt->width); + scroller.first = true; + + // background excludes space taken by visu (if any) + scroller.back.width = display_width - ((visu.mode && visu.row < SB_HEIGHT) ? visu.width : 0); - xSemaphoreGive(display_mutex); + // set scroller steps & beginning + if (pkt->direction == 1) { + scroller.scrolled = 0; + scroller.by = htons(pkt->by); + } else { + scroller.scrolled = scroller.scroll.width; + scroller.by = -htons(pkt->by); + } + + xSemaphoreGive(displayer.mutex); } // copy scroll frame data (no semaphore needed) - if (scroller.size + size < scroller.max) { - memcpy(scroller.scroll_frame + offset, data + sizeof(struct grfs_packet), size); - scroller.size = offset + size; - LOG_INFO("scroller current size %u", scroller.size); + if (scroller.scroll.size + size < scroller.scroll.max) { + memcpy(scroller.scroll.frame + offset, data + sizeof(struct grfs_packet), size); + scroller.scroll.size = offset + size; + LOG_INFO("scroller current size %u", scroller.scroll.size); } else { - LOG_INFO("scroller too larger %u/%u", scroller.size + size, scroller.max); + LOG_INFO("scroller too larger %u/%u", scroller.scroll.size + size, scroller.scroll.max); } } @@ -456,109 +550,273 @@ static void grfg_handler(u8_t *data, int len) { LOG_DEBUG("gfrg s:%hu w:%hu (len:%u)", htons(pkt->screen), htons(pkt->width), len); - memcpy(scroller.back_frame, data + sizeof(struct grfg_packet), len - sizeof(struct grfg_packet)); - scroller.window_width = htons(pkt->width); + xSemaphoreTake(displayer.mutex, portMAX_DELAY); - xSemaphoreTake(display_mutex, portMAX_DELAY); - - // can't be in grfs as we need full size & scroll_width - if (scroller.updated) { - scroller.scroll_len = display_width * display_height / 8 - (display_width - scroller.window_width) * display_height / 8; - if (scroller.direction == 1) { - scroller.scroll_ptr = scroller.scroll_frame; - scroller.scroll_step = scroller.by * display_height / 8; - } else { - scroller.scroll_ptr = scroller.scroll_frame + scroller.size - scroller.scroll_len; - scroller.scroll_step = -scroller.by * display_height / 8; - } + // size of scrollable area (less than background) + scroller.width = htons(pkt->width); + memcpy(scroller.back.frame, data + sizeof(struct grfg_packet), len - sizeof(struct grfg_packet)); - scroller.updated = false; - } + // update display asynchronously (frames are oganized by columns) + memcpy(scroller.frame, scroller.back.frame, scroller.back.width * display_height / 8); + for (int i = 0; i < scroller.width * display_height / 8; i++) scroller.frame[i] |= scroller.scroll.frame[scroller.scrolled * display_height / 8 + i]; + display->draw_cbr(scroller.frame, scroller.back.width, display_height); + display->update(); + + // now we can active scrolling, but only if we are not on a small screen + if (!visu.mode || visu.col || visu.row >= SB_HEIGHT) scroller.active = true; + + // if we just got a content update, let the scroller manage the screen + LOG_DEBUG("resuming scrolling task"); + + xSemaphoreGive(displayer.mutex); - if (!scroller.active) { - // this is a background update and scroller has been finished, so need to update here - u8_t *frame = malloc(display_width * display_height / 8); - memcpy(frame, scroller.back_frame, display_width * display_height / 8); - for (int i = 0; i < scroller.scroll_len; i++) frame[i] |= scroller.scroll_ptr[i]; - display->draw_cbr(frame, display_height); - free(frame); - LOG_DEBUG("direct drawing"); - } - else { - // if we just got a content update, let the scroller manage the screen - LOG_DEBUG("resuming scrolling task"); - vTaskResume(scroller.task); - } - - xSemaphoreGive(display_mutex); + // resume task once we have background, not in grfs + vTaskResume(displayer.task); } /**************************************************************************************** - * Scroll task + * Update visualization bars */ -static void scroll_task(void *args) { - u8_t *frame = NULL; - int len = display_width * display_height / 8; +static void visu_update(void) { + if (pthread_mutex_trylock(&visu_export.mutex)) return; + + // not enough samples + if (visu_export.level < (visu.mode == VISU_VUMETER ? RMS_LEN : FFT_LEN) * 2 && visu_export.running) { + pthread_mutex_unlock(&visu_export.mutex); + return; + } - while (1) { - xSemaphoreTake(display_mutex, portMAX_DELAY); - - // suspend ourselves if nothing to do, grfg will wake us up - if (!scroller.active) { - xSemaphoreGive(display_mutex); - vTaskSuspend(NULL); - xSemaphoreTake(display_mutex, portMAX_DELAY); - } - - // lock screen & active status - frame = malloc(display_width * display_height / 8); - - // scroll required amount of columns (within the window) - while (scroller.direction == 1 ? (scroller.scroll_ptr <= scroller.scroll_frame + scroller.size - scroller.scroll_step - len) : - (scroller.scroll_ptr + scroller.scroll_step >= scroller.scroll_frame) ) { - - // don't do anything if we have aborted - if (!scroller.active) break; + // reset bars for all cases first + for (int i = visu.n; --i >= 0;) visu.bars[i].current = 0; + + if (visu_export.running && visu_export.running) { - // scroll required amount of columns (within the window) - memcpy(frame, scroller.back_frame, display_width * display_height / 8); - for (int i = 0; i < scroller.scroll_len; i++) frame[i] |= scroller.scroll_ptr[i]; - scroller.scroll_ptr += scroller.scroll_step; - display->draw_cbr(frame, display_height); + if (visu.mode == VISU_VUMETER) { + s16_t *iptr = visu_export.buffer; - xSemaphoreGive(display_mutex); - vTaskDelay(scroller.speed / portTICK_PERIOD_MS); - - xSemaphoreTake(display_mutex, portMAX_DELAY); - } + // calculate sum(X²), try to not overflow at the expense of some precision + for (int i = RMS_LEN; --i >= 0;) { + visu.bars[0].current += (*iptr * *iptr + (1 << (RMS_LEN_BIT - 2))) >> (RMS_LEN_BIT - 1); + iptr++; + visu.bars[1].current += (*iptr * *iptr + (1 << (RMS_LEN_BIT - 2))) >> (RMS_LEN_BIT - 1); + iptr++; + } - // done with scrolling cycle reset scroller ptr - scroller.scroll_ptr = scroller.scroll_frame + (scroller.direction == 2 ? scroller.size - scroller.scroll_len : 0); - - // scrolling done, update screen and see if we need to continue - if (scroller.active) { - memcpy(frame, scroller.back_frame, len); - for (int i = 0; i < scroller.scroll_len; i++) frame[i] |= scroller.scroll_ptr[i]; - display->draw_cbr(frame, display_height); - free(frame); - - // see if we need to pause or if we are done - if (scroller.mode) { - scroller.active = false; - xSemaphoreGive(display_mutex); - // can't call directly send_packet from slimproto as it's not re-entrant - ANIC_resp = ANIM_SCROLL_ONCE | ANIM_SCREEN_1; - LOG_INFO("scroll-once terminated"); - } else { - xSemaphoreGive(display_mutex); - vTaskDelay(scroller.pause / portTICK_PERIOD_MS); - LOG_DEBUG("scroll cycle done, pausing for %u (ms)", scroller.pause); + // convert to dB (1 bit remaining for getting X²/N, 60dB dynamic starting from 0dBFS = 3 bits back-off) + for (int i = visu.n; --i >= 0;) { + visu.bars[i].current = 32 * (0.01667f*10*log10f(0.0000001f + (visu.bars[i].current >> 1)) - 0.2543f); + if (visu.bars[i].current > 31) visu.bars[i].current = 31; + else if (visu.bars[i].current < 0) visu.bars[i].current = 0; } } else { - free(frame); - xSemaphoreGive(display_mutex); - LOG_INFO("scroll aborted"); + // on xtensa/esp32 the floating point FFT takes 1/2 cycles of the fixed point + for (int i = 0 ; i < FFT_LEN ; i++) { + // don't normalize here, but we are due INT16_MAX and FFT_LEN / 2 / 2 + visu.samples[i * 2 + 0] = (float) (visu_export.buffer[2*i] + visu_export.buffer[2*i + 1]) * visu.hanning[i]; + visu.samples[i * 2 + 1] = 0; + } + + // actual FFT that might be less cycle than all the crap below + dsps_fft2r_fc32_ae32(visu.samples, FFT_LEN); + dsps_bit_rev_fc32_ansi(visu.samples, FFT_LEN); + + // now arrange the result with the number of bar and sampling rate (don't want DC) + for (int i = 1, j = 1; i <= visu.n && j < (FFT_LEN / 2); i++) { + float power, count; + + // find the next point in FFT (this is real signal, so only half matters) + for (count = 0, power = 0; j * visu.n * visu_export.rate < i * (FFT_LEN / 2) * DISPLAY_BW && j < (FFT_LEN / 2); j++, count += 1) { + power += visu.samples[2*j] * visu.samples[2*j] + visu.samples[2*j+1] * visu.samples[2*j+1]; + } + // due to sample rate, we have reached the end of the available spectrum + if (j >= (FFT_LEN / 2)) { + // normalize accumulated data + if (count) power /= count * 2.; + } else if (count) { + // how much of what remains do we need to add + float ratio = j - (float) (i * DISPLAY_BW * (FFT_LEN / 2)) / (float) (visu.n * visu_export.rate); + power += (visu.samples[2*j] * visu.samples[2*j] + visu.samples[2*j+1] * visu.samples[2*j+1]) * ratio; + + // normalize accumulated data + power /= (count + ratio) * 2; + } else { + // no data for that band (sampling rate too high), just assume same as previous one + power = (visu.samples[2*j] * visu.samples[2*j] + visu.samples[2*j+1] * visu.samples[2*j+1]) / 2.; + } + + // convert to dB and bars, same back-off + if (power) visu.bars[i-1].current = 32 * (0.01667f*10*(log10f(power) - log10f(FFT_LEN/2*2)) - 0.2543f); + if (visu.bars[i-1].current > 31) visu.bars[i-1].current = 31; + else if (visu.bars[i-1].current < 0) visu.bars[i-1].current = 0; + } + } + } + + // we took what we want, we can release the buffer + visu_export.level = 0; + pthread_mutex_unlock(&visu_export.mutex); + + display->clear(false, false, visu.col, visu.row, visu.col + visu.width - 1, visu.row + visu.height - 1); + + for (int i = visu.n; --i >= 0;) { + int x1 = visu.col + visu.border + visu.bar_border + i*(visu.bar_width + visu.bar_gap); + int y1 = visu.row + visu.height - 1; + + if (visu.bars[i].current > visu.bars[i].max) visu.bars[i].max = visu.bars[i].current; + else if (visu.bars[i].max) visu.bars[i].max--; + + for (int j = 0; j <= visu.bars[i].current; j += 2) + display->draw_line( x1, y1 - j, x1 + visu.bar_width - 1, y1 - j); + + if (visu.bars[i].max > 2) { + display->draw_line( x1, y1 - visu.bars[i].max, x1 + visu.bar_width - 1, y1 - visu.bars[i].max); + display->draw_line( x1, y1 - visu.bars[i].max + 1, x1 + visu.bar_width - 1, y1 - visu.bars[i].max + 1); } + } +} + +/**************************************************************************************** + * Visu packet handler + */ +static void visu_handler( u8_t *data, int len) { + struct visu_packet *pkt = (struct visu_packet*) data; + int bars = 0; + + LOG_DEBUG("visu %u with %u parameters", pkt->which, pkt->count); + + /* + If width is specified, then respect all coordinates, otherwise we try to + use the bottom part of the display and if it is a small display, we overwrite + text + */ + + xSemaphoreTake(displayer.mutex, portMAX_DELAY); + visu.mode = pkt->which; + + // little trick to clean the taller screens when switching visu + if (visu.row >= SB_HEIGHT) display->clear(false, true, visu.col, visu.row, visu.col + visu.width - 1, visu.row - visu.height - 1); + + if (visu.mode) { + pkt->width = htonl(pkt->width); + pkt->height = htonl(pkt->height); + pkt->row = htonl(pkt->row); + pkt->col = htonl(pkt->col); + + if (pkt->width > 0 && pkt->count >= 4) { + // small visu, then go were we are told to + visu.width = pkt->width; + visu.height = pkt->height ? pkt->height : SB_HEIGHT; + visu.col = pkt->col < 0 ? display->width + pkt->col : pkt->col; + visu.row = pkt->row < 0 ? display->height + pkt->row : pkt->row; + visu.border = htonl(pkt->border); + bars = htonl(pkt->small_bars); + } else { + // full screen visu, try to use bottom screen if available + visu.width = display->width; + visu.height = display->height > SB_HEIGHT ? display->height - SB_HEIGHT : display->height; + visu.col = visu.border = 0; + visu.row = display->height - visu.height; + // already in CPU order + bars = pkt->full_bars; + } + + // try to adapt to what we have + visu.n = visu.mode == VISU_VUMETER ? 2 : (bars ? bars : MAX_BARS); + do { + visu.bar_width = (visu.width - visu.border - visu.bar_gap * (visu.n - 1)) / visu.n; + if (visu.bar_width > 0) break; + } while (--visu.n); + visu.bar_border = (visu.width - visu.border - (visu.bar_width + visu.bar_gap) * visu.n + visu.bar_gap) / 2; + + // give up if not enough space + if (visu.bar_width < 0) visu.mode = VISU_BLANK; + else vTaskResume(displayer.task); + visu.wake = 0; + + // reset bars maximum + for (int i = visu.n; --i >= 0;) visu.bars[i].max = 0; + + display->clear(false, true, visu.col, visu.row, visu.col + visu.width - 1, visu.row - visu.height - 1); + + LOG_INFO("Visualizer with %u bars of width %d:%d:%d:%d (%w:%u,h:%u,c:%u,r:%u)", visu.n, visu.bar_border, visu.bar_width, visu.bar_gap, visu.border, visu.width, visu.height, visu.col, visu.row); + } else { + LOG_INFO("Stopping visualizer"); + } + + xSemaphoreGive(displayer.mutex); +} + +/**************************************************************************************** + * Scroll task + * - with the addition of the visualizer, it's a bit a 2-headed beast not easy to + * maintain, so som better separation between the visu and scroll is probably needed + */ +static void displayer_task(void *args) { + int sleep; + + while (1) { + xSemaphoreTake(displayer.mutex, portMAX_DELAY); + + // suspend ourselves if nothing to do, grfg or visu will wake us up + if (!scroller.active && !visu.mode) { + xSemaphoreGive(displayer.mutex); + vTaskSuspend(NULL); + xSemaphoreTake(displayer.mutex, portMAX_DELAY); + scroller.wake = visu.wake = 0; + } + + // go for long sleep when either item is disabled + if (!visu.mode) visu.wake = LONG_WAKE; + if (!scroller.active) scroller.wake = LONG_WAKE; + + // scroll required amount of columns (within the window) + if (scroller.active && scroller.wake <= 0) { + // by default go for the long sleep, will change below if required + scroller.wake = LONG_WAKE; + + // do we have more to scroll (scroll.width is the last column from which we havea full zone) + if (scroller.by > 0 ? (scroller.scrolled <= scroller.scroll.width) : (scroller.scrolled >= 0)) { + memcpy(scroller.frame, scroller.back.frame, scroller.back.width * display_height / 8); + for (int i = 0; i < scroller.width * display_height / 8; i++) scroller.frame[i] |= scroller.scroll.frame[scroller.scrolled * display_height / 8 + i]; + scroller.scrolled += scroller.by; + display->draw_cbr(scroller.frame, scroller.width, display_height); + + // short sleep & don't need background update + scroller.wake = scroller.speed; + } else if (scroller.first || !scroller.mode) { + // at least one round done + scroller.first = false; + + // see if we need to pause or if we are done + if (scroller.mode) { + // can't call directly send_packet from slimproto as it's not re-entrant + ANIC_resp = ANIM_SCROLL_ONCE | ANIM_SCREEN_1; + LOG_INFO("scroll-once terminated"); + } else { + scroller.wake = scroller.pause; + LOG_DEBUG("scroll cycle done, pausing for %u (ms)", scroller.pause); + } + + // need to reset pointers for next scroll + scroller.scrolled = scroller.by < 0 ? scroller.scroll.width : 0; + } + } + + // update visu if active + if (visu.mode && visu.wake <= 0) { + visu_update(); + visu.wake = 100; + } + + display->update(); + + // release semaphore and sleep what's needed + xSemaphoreGive(displayer.mutex); + + sleep = min(visu.wake, scroller.wake); + vTaskDelay(sleep / portTICK_PERIOD_MS); + scroller.wake -= sleep; + visu.wake -= sleep; } } diff --git a/components/squeezelite/embedded.h b/components/squeezelite/embedded.h index f8fb1e03..90926dca 100644 --- a/components/squeezelite/embedded.h +++ b/components/squeezelite/embedded.h @@ -54,6 +54,17 @@ void register_external(void); void deregister_external(void); void decode_restore(int external); +// to be defined to nothing if you don't want to support these +extern struct visu_export_s { + pthread_mutex_t mutex; + u32_t level, size, rate; + s16_t *buffer; + bool running; +} visu_export; +void output_visu_export(s16_t *frames, frames_t out_frames, u32_t rate, bool silence); +void output_visu_init(log_level level); +void output_visu_close(void); + // optional, please chain if used bool (*slimp_handler)(u8_t *data, int len); void (*slimp_loop)(void); diff --git a/components/squeezelite/output_embedded.c b/components/squeezelite/output_embedded.c index c9073458..cfd1a544 100644 --- a/components/squeezelite/output_embedded.c +++ b/components/squeezelite/output_embedded.c @@ -68,6 +68,8 @@ void output_init_embedded(log_level level, char *device, unsigned output_buf_siz output_init_i2s(level, device, output_buf_size, params, rates, rate_delay, idle); } + output_visu_init(level); + LOG_INFO("init completed."); } @@ -75,6 +77,7 @@ void output_close_embedded(void) { LOG_INFO("close output"); if (close_cb) (*close_cb)(); output_close_common(); + output_visu_close(); } void set_volume(unsigned left, unsigned right) { diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index e36d98ec..666ec9ac 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -365,8 +365,11 @@ static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32 _scale_and_pack_frames(obuf + oframes * bytes_per_frame, optr, out_frames, gainL, gainR, output.format); #endif - oframes += out_frames; + // send data to visu + output_visu_export((s16_t*) (obuf + oframes * bytes_per_frame), out_frames, output.current_sample_rate, silence); + oframes += out_frames; + return out_frames; } diff --git a/components/squeezelite/output_visu.c b/components/squeezelite/output_visu.c new file mode 100644 index 00000000..20a9da0a --- /dev/null +++ b/components/squeezelite/output_visu.c @@ -0,0 +1,76 @@ +/* + * Squeezelite - lightweight headless squeezebox emulator + * + * (c) Adrian Smith 2012-2015, triode1@btinternet.com + * Ralph Irving 2015-2017, ralph_irving@hotmail.com + * Philippe_44 2020, philippe_44@outloook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "squeezelite.h" + +#define VISUEXPORT_SIZE 2048 + +EXT_BSS struct visu_export_s visu_export; +static struct visu_export_s *visu = &visu_export; + +static log_level loglevel = lINFO; + +void output_visu_export(s16_t *frames, frames_t out_frames, u32_t rate, bool silence) { + + // no data to process + if (silence) { + visu->running = false; + return; + } + + // do not block, try to stuff data put wait for consumer to have used them + if (!pthread_mutex_trylock(&visu->mutex)) { + // don't mix sample rates + if (visu->rate != rate) visu->level = 0; + + // stuff buffer up and wait for consumer to read it (should reset level) + if (visu->level < visu->size) { + u32_t space = min(visu->size - visu->level, out_frames * 2) * 2; + memcpy(visu->buffer + visu->level, frames, space); + + visu->level += space / 2; + visu->running = true; + visu->rate = rate ? rate : 44100; + } + + // mutex must be released + pthread_mutex_unlock(&visu->mutex); + } +} + +void output_visu_close(void) { + pthread_mutex_lock(&visu->mutex); + visu->running = false; + free(visu->buffer); + pthread_mutex_unlock(&visu->mutex); +} + +void output_visu_init(log_level level) { + loglevel = level; + pthread_mutex_init(&visu->mutex, NULL); + visu->size = VISUEXPORT_SIZE; + visu->running = false; + visu->rate = 44100; + visu->buffer = malloc(VISUEXPORT_SIZE * sizeof(s16_t) * 2); + LOG_INFO("Initialize VISUEXPORT %u 16 bits samples", VISUEXPORT_SIZE); +} + diff --git a/components/squeezelite/squeezelite.h b/components/squeezelite/squeezelite.h index 2544de34..ef3a6397 100644 --- a/components/squeezelite/squeezelite.h +++ b/components/squeezelite/squeezelite.h @@ -387,6 +387,21 @@ typedef BOOL bool; #endif +typedef u32_t frames_t; +typedef int sockfd; + +// logging +typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level; + +const char *logtime(void); +void logprint(const char *fmt, ...); + +#define LOG_ERROR(fmt, ...) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_WARN(fmt, ...) if (loglevel >= lWARN) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_INFO(fmt, ...) if (loglevel >= lINFO) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_DEBUG(fmt, ...) if (loglevel >= lDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) +#define LOG_SDEBUG(fmt, ...) if (loglevel >= lSDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) + #if EMBEDDED #include "embedded.h" #endif @@ -395,9 +410,6 @@ typedef BOOL bool; #define MSG_NOSIGNAL 0 #endif -typedef u32_t frames_t; -typedef int sockfd; - #if EVENTFD #include #define event_event int @@ -473,18 +485,6 @@ void _wake_create(event_event*); #define min(a,b) (((a) < (b)) ? (a) : (b)) -// logging -typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level; - -const char *logtime(void); -void logprint(const char *fmt, ...); - -#define LOG_ERROR(fmt, ...) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) -#define LOG_WARN(fmt, ...) if (loglevel >= lWARN) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) -#define LOG_INFO(fmt, ...) if (loglevel >= lINFO) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) -#define LOG_DEBUG(fmt, ...) if (loglevel >= lDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) -#define LOG_SDEBUG(fmt, ...) if (loglevel >= lSDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__) - // utils.c (non logging) typedef enum { EVENT_TIMEOUT = 0, EVENT_READ, EVENT_WAKE } event_type; #if WIN && USE_SSL diff --git a/components/squeezelite/utils.c b/components/squeezelite/utils.c index 1971ed65..7be5cf59 100644 --- a/components/squeezelite/utils.c +++ b/components/squeezelite/utils.c @@ -78,6 +78,7 @@ void logprint(const char *fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); + va_end(args); fflush(stderr); } diff --git a/plugin/SqueezeESP32.zip b/plugin/SqueezeESP32.zip index 1e5af765..626d46d9 100644 Binary files a/plugin/SqueezeESP32.zip and b/plugin/SqueezeESP32.zip differ diff --git a/plugin/SqueezeESP32/Graphics.pm b/plugin/SqueezeESP32/Graphics.pm index 811e2008..42ab788e 100644 --- a/plugin/SqueezeESP32/Graphics.pm +++ b/plugin/SqueezeESP32/Graphics.pm @@ -11,6 +11,10 @@ my $prefs = preferences('plugin.squeezeesp32'); my $log = logger('plugin.squeezeesp32'); my $VISUALIZER_NONE = 0; +my $VISUALIZER_VUMETER = 1; +my $VISUALIZER_SPECTRUM_ANALYZER = 2; +my $VISUALIZER_WAVEFORM = 3; + my $width = $prefs->get('width') || 128; my @modes = ( @@ -42,6 +46,44 @@ my @modes = ( { desc => ['SETUP_SHOWBUFFERFULLNESS'], bar => 0, secs => 0, width => $width, fullness => 1, params => [$VISUALIZER_NONE] }, + # mode 7 + { desc => ['VISUALIZER_VUMETER_SMALL'], + bar => 0, secs => 0, width => $width, _width => -20, + # extra parameters (width, height, col (< 0 = from right), row (< 0 = from bottom), bars, left space) + params => [$VISUALIZER_VUMETER, 20, 32, -20, 0, 2] }, + # mode 8 + { desc => ['VISUALIZER_SPECTRUM_ANALYZER_SMALL'], + bar => 0, secs => 0, width => $width, _width => -32, + # extra parameters (width, height, col (< 0 = from right), row (< 0 = from bottom), bars, left space) + params => [$VISUALIZER_SPECTRUM_ANALYZER, 32, 32, -32, 0, 2, 6] }, + # mode 9 + { desc => ['VISUALIZER_VUMETER'], + bar => 0, secs => 0, width => $width, + params => [$VISUALIZER_VUMETER] }, + # mode 10 + { desc => ['VISUALIZER_SPECTRUM_ANALYZER'], + bar => 0, secs => 0, width => $width, + # extra parameters (bars) + params => [$VISUALIZER_SPECTRUM_ANALYZER, 16] }, + # mode 11 + { desc => ['VISUALIZER_VUMETER', 'AND', 'ELAPSED'], + bar => 0, secs => 1, width => $width, + params => [$VISUALIZER_VUMETER] }, + # mode 12 + { desc => ['VISUALIZER_SPECTRUM_ANALYZER', 'AND', 'ELAPSED'], + bar => 0, secs => 1, width => $width, + # extra parameters (bars) + params => [$VISUALIZER_SPECTRUM_ANALYZER, 16] }, + # mode 13 + { desc => ['VISUALIZER_VUMETER', 'AND', 'REMAINING'], + bar => 0, secs => -1, width => $width, + params => [$VISUALIZER_VUMETER] }, + # mode 14 + { desc => ['VISUALIZER_SPECTRUM_ANALYZER', 'AND', 'REMAINING'], + bar => 0, secs => -1, width => $width, + # extra parameters (bars) + params => [$VISUALIZER_SPECTRUM_ANALYZER, 16] }, + ); sub modes { @@ -52,6 +94,27 @@ sub nmodes { return $#modes; } +sub displayWidth { + my $display = shift; + my $client = $display->client; + + # if we're showing the always-on visualizer & the current buttonmode + # hasn't overridden, then use the playing display mode to index + # into the display width, otherwise, it's fullscreen. + my $mode = 0; + + if ( $display->showVisualizer() && !defined($client->modeParam('visu')) ) { + my $cprefs = preferences('server')->client($client); + $mode = $cprefs->get('playingDisplayModes')->[ $cprefs->get('playingDisplayMode') ]; + } + + if ($display->widthOverride) { + return $display->widthOverride + ($display->modes->[$mode || 0]{_width} || 0); + } else { + return $display->modes->[$mode || 0]{width}; + } +} + # I don't think LMS renderer handles properly screens other than 32 pixels. It # seems that all we get is a 32 pixel-tall data with anything else padded to 0 # i.e. if we try 64 pixels height, bytes 0..3 and 4..7 will contains the same @@ -72,8 +135,12 @@ sub displayHeight { return 32; } -sub displayWidth { - return shift->widthOverride(@_) || $width; +sub updateWidth { + my ($display, $width) = @_; + + foreach my $mode (@{$display->modes}) { + $mode->{width} = $width + 1 + $mode->{_width} || 0; + } } sub vfdmodel { diff --git a/plugin/SqueezeESP32/install.xml b/plugin/SqueezeESP32/install.xml index d92cd4f3..548170f5 100644 --- a/plugin/SqueezeESP32/install.xml +++ b/plugin/SqueezeESP32/install.xml @@ -10,6 +10,6 @@ PLUGIN_SQUEEZEESP32 PLUGIN_SQUEEZEESP32_DESC Plugins::SqueezeESP32::Plugin - 0.9 + 0.10 Philippe diff --git a/plugin/repo.xml b/plugin/repo.xml index 03f6c740..81067947 100644 --- a/plugin/repo.xml +++ b/plugin/repo.xml @@ -1,10 +1,10 @@ - + https://github.com/sle118/squeezelite-esp32 Philippe - 89c68b54ad4373df6c0cd37222a07b53013c4815 + 5a35a7b821e887baf869535f113b8b55bbc52a54 philippe_44@outlook.com SqueezeESP32 additional player id (100) http://github.com/sle118/squeezelite-esp32/raw/master/plugin/SqueezeESP32.zip