diff --git a/components/display/SSD1306.c b/components/display/SSD1306.c index e1280b8b..fec88996 100644 --- a/components/display/SSD1306.c +++ b/components/display/SSD1306.c @@ -74,7 +74,7 @@ static void Update( struct GDS_Device* Device ) { CurrentPage = p + 1; // actual write - Device->WriteData( Device, Private->Shadowbuffer + p*width + first, last - first + 1 ); + Device->WriteData( Device, Private->Shadowbuffer + p*width + first, last - first + 1); } } #else @@ -114,10 +114,6 @@ static bool Init( struct GDS_Device* Device ) { // charge pump regulator, do direct init Device->WriteCommand( Device, 0x8D ); Device->WriteCommand( Device, 0x14 ); - - // set Clocks - Device->WriteCommand( Device, 0xD5 ); - Device->WriteCommand( Device, ( 0x08 << 4 ) | 0x00 ); // COM pins HW config (alternative:EN if 64, DIS if 32, remap:DIS) - some display might need something different Device->WriteCommand( Device, 0xDA ); @@ -126,12 +122,6 @@ static bool Init( struct GDS_Device* Device ) { // MUX Ratio Device->WriteCommand( Device, 0xA8 ); Device->WriteCommand( Device, Device->Height - 1); - // Page & GDDRAM Start Column High/Low - /* - Device->WriteCommand( Device, 0x00 ); - Device->WriteCommand( Device, 0x10 ); - Device->WriteCommand( Device, 0xB0 ); - */ // Display Offset Device->WriteCommand( Device, 0xD3 ); Device->WriteCommand( Device, 0 ); @@ -143,6 +133,9 @@ static bool Init( struct GDS_Device* Device ) { Device->SetHFlip( Device, false ); // no Display Inversion Device->WriteCommand( Device, 0xA6 ); + // set Clocks + Device->WriteCommand( Device, 0xD5 ); + Device->WriteCommand( Device, ( 0x08 << 4 ) | 0x00 ); // set Adressing Mode Horizontal Device->WriteCommand( Device, 0x20 ); Device->WriteCommand( Device, 0 ); diff --git a/components/display/core/gds.c b/components/display/core/gds.c index d315b027..26242a60 100644 --- a/components/display/core/gds.c +++ b/components/display/core/gds.c @@ -60,6 +60,10 @@ void GDS_Clear( struct GDS_Device* Device, int Color ) { } void GDS_ClearWindow( struct GDS_Device* Device, int x1, int y1, int x2, int y2, int Color ) { + // -1 means up to width/height + if (x2 < 0) x2 = Device->Width - 1; + if (y2 < 0) y2 = Device->Height - 1; + // driver can provide own optimized clear window if (Device->ClearWindow) { Device->ClearWindow( Device, x1, y1, x2, y2, Color ); diff --git a/components/display/core/gds_draw.c b/components/display/core/gds_draw.c index 51789e10..c78102eb 100644 --- a/components/display/core/gds_draw.c +++ b/components/display/core/gds_draw.c @@ -212,6 +212,7 @@ void GDS_DrawBitmapCBR(struct GDS_Device* Device, uint8_t *Data, int Width, int uint8_t *optr = Device->Framebuffer; int LineLen = Device->Width >> 1; Height >>= 3; + Color &= 0x0f; for (int i = Width * Height, r = 0, c = 0; --i >= 0;) { uint8_t Byte = BitReverseTable256[*Data++]; // we need to linearize code to let compiler better optimize @@ -223,7 +224,7 @@ void GDS_DrawBitmapCBR(struct GDS_Device* Device, uint8_t *Data, int Width, int *optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1; *optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1; *optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1; - *optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; Byte >>= 1; + *optr = (*optr & 0x0f) | (((Byte & 0x01)*Color)<<4); optr += LineLen; } else { *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; @@ -232,7 +233,7 @@ void GDS_DrawBitmapCBR(struct GDS_Device* Device, uint8_t *Data, int Width, int *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; - *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; Byte >>= 1; + *optr = (*optr & 0xf0) | (((Byte & 0x01)*Color)); optr += LineLen; } // end of a column, move to next one if (++r == Height) { c++; r = 0; optr = Device->Framebuffer + (c >> 1); } diff --git a/components/display/core/gds_image.c b/components/display/core/gds_image.c index a5919f74..ebd274e1 100644 --- a/components/display/core/gds_image.c +++ b/components/display/core/gds_image.c @@ -218,8 +218,10 @@ bool GDS_DrawJPEG( struct GDS_Device* Device, uint8_t *Source, int x, int y, int // then place it if (Fit & GDS_IMAGE_CENTER_X) Context.XOfs = (Device->Width + x - Context.Width) / 2; + else if (Fit & GDS_IMAGE_RIGHT) Context.XOfs = Device->Width - Context.Width; if (Fit & GDS_IMAGE_CENTER_Y) Context.YOfs = (Device->Height + y - Context.Height) / 2; - + else if (Fit & GDS_IMAGE_BOTTOM) Context.YOfs = Device->Height - Context.Height; + Context.XMin = x - Context.XOfs; Context.YMin = y - Context.YOfs; diff --git a/components/display/core/gds_image.h b/components/display/core/gds_image.h index fa87001a..0becd94a 100644 --- a/components/display/core/gds_image.h +++ b/components/display/core/gds_image.h @@ -9,8 +9,11 @@ struct GDS_Device; enum { GDS_RGB565, GDS_RGB555, GDS_RGB444 }; -#define GDS_IMAGE_TOP 0x00 +#define GDS_IMAGE_LEFT 0x00 #define GDS_IMAGE_CENTER_X 0x01 +#define GDS_IMAGE_RIGHT 0x04 +#define GDS_IMAGE_TOP 0x00 +#define GDS_IMAGE_BOTTOM 0x08 #define GDS_IMAGE_CENTER_Y 0x02 #define GDS_IMAGE_CENTER (GDS_IMAGE_CENTER_X | GDS_IMAGE_CENTER_Y) #define GDS_IMAGE_FIT 0x10 diff --git a/components/display/core/gds_private.h b/components/display/core/gds_private.h index 73b9d13c..0b26eaf8 100644 --- a/components/display/core/gds_private.h +++ b/components/display/core/gds_private.h @@ -167,7 +167,7 @@ static inline void IRAM_ATTR GDS_DrawPixel4Fast( struct GDS_Device* Device, int uint8_t* FBOffset; FBOffset = Device->Framebuffer + ( (Y * Device->Width >> 1) + (X >> 1)); - *FBOffset = X & 0x01 ? (*FBOffset & 0x0f) | ((Color & 0x0f) << 4) : ((*FBOffset & 0xf0) | (Color & 0x0f)); + *FBOffset = X & 0x01 ? (*FBOffset & 0x0f) | ((Color & 0x0f) << 4) : ((*FBOffset & 0xf0) | (Color & 0x0f)); } static inline void IRAM_ATTR GDS_DrawPixelFast( struct GDS_Device* Device, int X, int Y, int Color ) { diff --git a/components/squeezelite/decode_external.c b/components/squeezelite/decode_external.c index 40682ae7..e778813f 100644 --- a/components/squeezelite/decode_external.c +++ b/components/squeezelite/decode_external.c @@ -53,7 +53,7 @@ static raop_event_t raop_state; static EXT_RAM_ATTR struct { bool enabled; int sum, count, win, errors[SYNC_WIN_RUN]; - u32_t len; + s32_t len; u32_t start_time, playtime; } raop_sync; @@ -210,20 +210,21 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args) // how many ms have we really played ms = now - output.updated + ((u64_t) (output.frames_played_dmp - output.device_frames) * 1000) / RAOP_SAMPLE_RATE; error = ms - (now - raop_sync.start_time); + LOG_INFO("backend played %u, desired %u, (delta:%d)", ms, now - raop_sync.start_time, ms - (now - raop_sync.start_time)); } else { - // in how many ms will the most recent block play - ms = ((u64_t) ((_buf_used(outputbuf) - raop_sync.len) / BYTES_PER_FRAME + output.device_frames + output.frames_in_process) * 1000) / RAOP_SAMPLE_RATE - (now - output.updated); - error = (raop_sync.playtime - now) - ms; + u32_t level = _buf_used(outputbuf); - // make sure impact of erroneous point is limited, but taken into account - if (abs(error) > 1000) { - LOG_INFO("limiting erroneous sync point %d", error); - error = (error > 0) ? SYNC_WIN_RUN : -SYNC_WIN_RUN; - } - - LOG_DEBUG("head local:%u, remote:%u (delta:%d)", ms, raop_sync.playtime - now, error); - LOG_DEBUG("obuf:%u, sync_len:%u, devframes:%u, inproc:%u", _buf_used(outputbuf), raop_sync.len, output.device_frames, output.frames_in_process); + // in how many ms will the most recent block play + ms = (((s32_t)(level - raop_sync.len) / BYTES_PER_FRAME + output.device_frames + output.frames_in_process) * 10) / (RAOP_SAMPLE_RATE / 100) - (s32_t) (now - output.updated); + + // when outputbuf is empty, it means we have a network black-out or something + error = level ? (raop_sync.playtime - now) - ms : 0; + + if (loglevel == lDEBUG || !level) { + LOG_INFO("head local:%d, remote:%d (delta:%d)", ms, raop_sync.playtime - now, error); + LOG_INFO("obuf:%u, sync_len:%u, devframes:%u, inproc:%u", _buf_used(outputbuf), raop_sync.len, output.device_frames, output.frames_in_process); + } } // calculate sum, error and update sliding window @@ -232,7 +233,10 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args) error = raop_sync.sum / min(raop_sync.count, raop_sync.win); // move to normal mode if possible - if (raop_sync.win == SYNC_WIN_START && raop_sync.count >= SYNC_WIN_START && abs(error) < 10) raop_sync.win = SYNC_WIN_RUN; + if (raop_sync.win == SYNC_WIN_START && raop_sync.count >= SYNC_WIN_START && abs(error) < 10) { + raop_sync.win = SYNC_WIN_RUN; + LOG_INFO("switching to slow sync mode %u", raop_sync.win); + } // wait till e have enough data or there is a strong deviation if ((raop_sync.count >= raop_sync.win && abs(error) > 10) || (raop_sync.count >= SYNC_WIN_CHECK && abs(error) > 100)) { @@ -241,11 +245,11 @@ static bool raop_sink_cmd_handler(raop_event_t event, va_list args) if (error < 0) { output.skip_frames = -(error * RAOP_SAMPLE_RATE) / 1000; output.state = OUTPUT_SKIP_FRAMES; - LOG_INFO("skipping %u frames", output.skip_frames); + LOG_INFO("skipping %u frames (count:%d)", output.skip_frames, raop_sync.count); } else { output.pause_frames = (error * RAOP_SAMPLE_RATE) / 1000; output.state = OUTPUT_PAUSE_FRAMES; - LOG_INFO("pausing for %u frames", output.pause_frames); + LOG_INFO("pausing for %u frames (count: %d)", output.pause_frames, raop_sync.count); } // reset sliding window diff --git a/components/squeezelite/display.c b/components/squeezelite/display.c index e7061477..b20f1be5 100644 --- a/components/squeezelite/display.c +++ b/components/squeezelite/display.c @@ -26,6 +26,7 @@ #include "gds.h" #include "gds_text.h" #include "gds_draw.h" +#include "gds_image.h" #pragma pack(push, 1) @@ -59,6 +60,14 @@ struct grfg_packet { u16_t width; // # of pixels of scrollable }; +struct grfa_packet { + char opcode[4]; + u32_t length; + u16_t x; + u16_t y; + u32_t offset; +}; + struct visu_packet { char opcode[4]; u8_t which; @@ -77,6 +86,29 @@ struct visu_packet { u32_t bars; u32_t spectrum_scale; }; + struct { + u32_t mono; + u32_t bandwidth; + u32_t preemph; + struct { + u32_t pos; + u32_t width; + u32_t orient; + u32_t bar_width; + u32_t bar_space; + u32_t clipping; + u32_t bar_intens; + u32_t bar_cap_intens; + } channels[2]; + }; + struct { + u32_t mono; + u32_t style; + struct { + u32_t pos; + u32_t width; + } channels[2]; + } classical_vu; }; }; @@ -130,7 +162,15 @@ static struct scroller_s { u32_t width; } scroller; +static struct { + u8_t *data; + u32_t size; + u16_t x, y; + bool enable; +} artwork; + #define MAX_BARS 32 +#define VISU_ESP32 0x10 static EXT_RAM_ATTR struct { int bar_gap, bar_width, bar_border; struct { @@ -142,6 +182,11 @@ static EXT_RAM_ATTR struct { 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]; + struct { + u8_t *frame; + int width; + bool active; + } back; } visu; #define ANIM_NONE 0x00 @@ -174,6 +219,7 @@ 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 grfa_handler(u8_t *data, int len); static void visu_handler(u8_t *data, int len); static void displayer_task(void* arg); @@ -205,6 +251,33 @@ static void displayer_task(void* arg); ANIM_SCREEN_2 0x08 # For scrollonce only, screen 2 was scrolling */ +/* classical visu not our specific version) + Parameters for the spectrum analyzer: + 0 - Channels: stereo == 0, mono == 1 + 1 - Bandwidth: 0..22050Hz == 0, 0..11025Hz == 1 + 2 - Preemphasis in dB per KHz + Left channel parameters: + 3 - Position in pixels + 4 - Width in pixels + 5 - orientation: left to right == 0, right to left == 1 + 6 - Bar width in pixels + 7 - Bar spacing in pixels + 8 - Clipping: show all subbands == 0, clip higher subbands == 1 + 9 - Bar intensity (greyscale): 1-3 + 10 - Bar cap intensity (greyscale): 1-3 + Right channel parameters (not required for mono): + 11-18 - same as left channel parameters + + Parameters for the vumeter: + 0 - Channels: stereo == 0, mono == 1 + 1 - Style: digital == 0, analog == 1 + Left channel parameters: + 2 - Position in pixels + 3 - Width in pixels + Right channel parameters (not required for mono): + 4-5 - same as left channel parameters +*/ + /**************************************************************************************** * */ @@ -226,6 +299,7 @@ bool sb_display_init(void) { // create visu configuration visu.bar_gap = 1; visu.speed = 100; + visu.back.frame = calloc(1, (displayer.width * displayer.height) / 8); dsps_fft2r_init_fc32(visu.fft, FFT_LEN); dsps_wind_hann_f32(visu.hanning, FFT_LEN); @@ -300,7 +374,7 @@ static void send_server(void) { pkt_header.length = htonl(sizeof(pkt_header) - 8); pkt_header.mode = ANIC_resp; - send_packet((u8_t *)&pkt_header, sizeof(pkt_header)); + send_packet((uint8_t *) &pkt_header, sizeof(pkt_header)); ANIC_resp = ANIM_NONE; } @@ -308,18 +382,22 @@ static void send_server(void) { if (SETD_width) { struct SETD_header pkt_header; - LOG_INFO("sending width %u", SETD_width); - memset(&pkt_header, 0, sizeof(pkt_header)); memcpy(&pkt_header.opcode, "SETD", 4); pkt_header.id = 0xfe; // id 0xfe is width S:P:Squeezebox2 - pkt_header.length = htonl(sizeof(pkt_header) + 2 - 8); + pkt_header.length = htonl(sizeof(pkt_header) + 4 - 8); + + u16_t height = GDS_GetHeight(display); + LOG_INFO("sending dimension %ux%u", SETD_width, height); SETD_width = htons(SETD_width); - send_packet((u8_t *)&pkt_header, sizeof(pkt_header)); - send_packet((uint8_t*) &SETD_width, 2); - + height = htons(height); + + send_packet((uint8_t *) &pkt_header, sizeof(pkt_header)); + send_packet((uint8_t *) &SETD_width, 2); + send_packet((uint8_t *) &height, 2); + SETD_width = 0; } @@ -360,6 +438,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, "grfa", 4)) { + grfa_handler(data, len); } else if (!strncmp((char*) data, "visu", 4)) { visu_handler(data, len); } else { @@ -495,7 +575,7 @@ static void grfe_handler( u8_t *data, int len) { scroller.active = false; // we are not in control or we are displaying visu on a small screen, do not do screen update - if (visu.mode && !visu.col && visu.row < SB_HEIGHT) { + if ((visu.mode & VISU_ESP32) && !visu.col && visu.row < SB_HEIGHT) { xSemaphoreGive(displayer.mutex); return; } @@ -509,6 +589,16 @@ static void grfe_handler( u8_t *data, int len) { // draw new frame, it might be less than full screen (small visu) int width = ((len - sizeof(struct grfe_packet)) * 8) / displayer.height; + + // when doing screensaver, that frame becomes a visu background + if (!(visu.mode & VISU_ESP32)) { + visu.back.width = width; + memset(visu.back.frame, 0, (displayer.width * displayer.height) / 8); + memcpy(visu.back.frame, data + sizeof(struct grfe_packet), (width * displayer.height) / 8); + // this is a bit tricky but basically that checks if frame if full of 0 + visu.back.active = *visu.back.frame || memcmp(visu.back.frame, visu.back.frame + 1, width - 1); + } + GDS_DrawBitmapCBR(display, data + sizeof(struct grfe_packet), width, displayer.height, GDS_COLOR_WHITE); GDS_Update(display); } @@ -548,7 +638,7 @@ static void grfs_handler(u8_t *data, int len) { int size = len - sizeof(struct grfs_packet); int offset = htons(pkt->offset); - LOG_DEBUG("gfrs s:%u d:%u p:%u sp:%u by:%hu m:%hu w:%hu o:%hu", + LOG_DEBUG("grfs s:%u d:%u p:%u sp:%u by:%hu m:%hu w:%hu o:%hu", (int) pkt->screen, (int) pkt->direction, // 1=left, 2=right htonl(pkt->pause), // in ms @@ -573,9 +663,6 @@ static void grfs_handler(u8_t *data, int len) { scroller.first = true; scroller.overflow = false; - // background excludes space taken by visu (if any) - scroller.back.width = displayer.width - ((visu.mode && visu.row < SB_HEIGHT) ? visu.width : 0); - // set scroller steps & beginning if (pkt->direction == 1) { scroller.scrolled = 0; @@ -612,6 +699,7 @@ static void grfg_handler(u8_t *data, int len) { // size of scrollable area (less than background) scroller.width = htons(pkt->width); + scroller.back.width = ((len - sizeof(struct grfg_packet)) * 8) / displayer.height; memcpy(scroller.back.frame, data + sizeof(struct grfg_packet), len - sizeof(struct grfg_packet)); // update display asynchronously (frames are organized by columns) @@ -636,15 +724,63 @@ static void grfg_handler(u8_t *data, int len) { vTaskResume(displayer.task); } + +/**************************************************************************************** + * Artwork + */ +static void grfa_handler(u8_t *data, int len) { + struct grfa_packet *pkt = (struct grfa_packet*) data; + int size = len - sizeof(struct grfa_packet); + int offset = htonl(pkt->offset); + int length = htonl(pkt->length); + + artwork.enable = (length != 0); + + // clean up if we are disabling previously enabled artwork + if (!artwork.enable) { + if (artwork.size) GDS_ClearWindow(display, artwork.x, artwork.y, -1, -1, GDS_COLOR_BLACK); + return; + } + + // new grfa artwork, allocate memory + if (!offset) { + // same trick to clean current/previous window + if (artwork.size) { + GDS_ClearWindow(display, artwork.x, artwork.y, -1, -1, GDS_COLOR_BLACK); + artwork.size = 0; + } + + // now use new parameters + artwork.x = htons(pkt->x); + artwork.y = htons(pkt->y); + if (artwork.data) free(artwork.data); + artwork.data = malloc(length); + } + + // copy artwork data + memcpy(artwork.data + offset, data + sizeof(struct grfa_packet), size); + artwork.size += size; + if (artwork.size == length) { + GDS_ClearWindow(display, artwork.x, artwork.y, -1, -1, GDS_COLOR_BLACK); + GDS_DrawJPEG(display, artwork.data, artwork.x, artwork.y, artwork.y < SB_HEIGHT ? (GDS_IMAGE_RIGHT | GDS_IMAGE_TOP) : GDS_IMAGE_CENTER); + free(artwork.data); + artwork.data = NULL; + } + + LOG_INFO("gfra l:%u x:%hu, y:%hu, o:%u s:%u", length, artwork.x, artwork.y, offset, size); +} + /**************************************************************************************** * Update visualization bars */ static void visu_update(void) { // no need to protect against no woning the display as we are playing if (pthread_mutex_trylock(&visu_export.mutex)) return; + + int mode = visu.mode & ~VISU_ESP32; // not enough samples - if (visu_export.level < (visu.mode == VISU_VUMETER ? RMS_LEN : FFT_LEN) * 2 && visu_export.running) { + if (visu_export.level < (mode == VISU_VUMETER ? RMS_LEN : FFT_LEN) * 2 && visu_export.running) { pthread_mutex_unlock(&visu_export.mutex); return; } @@ -654,7 +790,7 @@ static void visu_update(void) { if (visu_export.running) { - if (visu.mode == VISU_VUMETER) { + if (mode == VISU_VUMETER) { s16_t *iptr = visu_export.buffer; // calculate sum(L²+R²), try to not overflow at the expense of some precision @@ -667,7 +803,7 @@ static void visu_update(void) { // 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); + visu.bars[i].current = SB_HEIGHT * (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; } @@ -709,7 +845,7 @@ static void visu_update(void) { } // convert to dB and bars, same back-off - if (power) visu.bars[i].current = 32 * (0.01667f*10*(log10f(power) - log10f(FFT_LEN/2*2)) - 0.2543f); + if (power) visu.bars[i].current = SB_HEIGHT * (0.01667f*10*(log10f(power) - log10f(FFT_LEN/2*2)) - 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; } @@ -724,6 +860,11 @@ static void visu_update(void) { int clear = 0; for (int i = visu.n; --i >= 0;) clear = max(clear, visu.bars[i].max); if (clear) GDS_ClearExt(display, false, false, visu.col, visu.row, visu.col + visu.width - 1, visu.row + visu.height - 1); + + // draw background if we are in screensaver mode + if (!(visu.mode & VISU_ESP32) && visu.back.active) { + GDS_DrawBitmapCBR(display, visu.back.frame, visu.back.width, displayer.height, GDS_COLOR_WHITE); + } // there is much more optimization to be done here, like not redrawing bars unless needed for (int i = visu.n; --i >= 0;) { @@ -778,37 +919,47 @@ static void visu_handler( u8_t *data, int len) { visu.mode = pkt->which; // little trick to clean the taller screens when switching visu - if (visu.row >= SB_HEIGHT) GDS_ClearExt(display, false, true, visu.col, visu.row, visu.col + visu.width - 1, visu.row - visu.height - 1); + if (visu.row >= SB_HEIGHT) GDS_ClearExt(display, false, true, visu.col, visu.row, visu.col + visu.width - 1, visu.row + visu.height - 1); if (visu.mode) { - if (pkt->count >= 4) { - // small visu, then go were we are told to - pkt->height = htonl(pkt->height); - pkt->row = htonl(pkt->row); - pkt->col = htonl(pkt->col); + // these will be overidden if necessary + visu.col = visu.border = 0; + visu.width = displayer.width; + + // what type of visu + if (visu.mode & VISU_ESP32) { + if (pkt->count >= 4) { + // more than 4 parameters, this is small visu, then go were we are told to + pkt->height = htonl(pkt->height); + pkt->row = htonl(pkt->row); + pkt->col = htonl(pkt->col); - visu.width = htonl(pkt->width); - visu.height = pkt->height ? pkt->height : SB_HEIGHT; - visu.col = pkt->col < 0 ? displayer.width + pkt->col : pkt->col; - visu.row = pkt->row < 0 ? GDS_GetHeight(display) + pkt->row : pkt->row; - visu.border = htonl(pkt->border); - bars = htonl(pkt->bars); - visu.spectrum_scale = htonl(pkt->spectrum_scale) / 100.; - - // might have a race condition with scroller message, so update width in case - if (scroller.active) scroller.back.width = displayer.width - visu.width; + visu.width = htonl(pkt->width); + visu.height = pkt->height ? pkt->height : SB_HEIGHT; + visu.col = pkt->col < 0 ? displayer.width + pkt->col : pkt->col; + visu.row = pkt->row < 0 ? GDS_GetHeight(display) + pkt->row : pkt->row; + visu.border = htonl(pkt->border); + bars = htonl(pkt->bars); + visu.spectrum_scale = htonl(pkt->spectrum_scale) / 100.; + } else { + // full screen visu, try to use bottom screen if available + visu.height = GDS_GetHeight(display) > SB_HEIGHT ? GDS_GetHeight(display) - SB_HEIGHT : GDS_GetHeight(display); + bars = htonl(pkt->full.bars); + visu.spectrum_scale = htonl(pkt->full.spectrum_scale) / 100.; + visu.row = GDS_GetHeight(display) - visu.height; + } } else { - // full screen visu, try to use bottom screen if available - visu.width = displayer.width; - visu.height = GDS_GetHeight(display) > SB_HEIGHT ? GDS_GetHeight(display) - SB_HEIGHT : GDS_GetHeight(display); - visu.col = visu.border = 0; - visu.row = GDS_GetHeight(display) - visu.height; - bars = htonl(pkt->full.bars); - visu.spectrum_scale = htonl(pkt->full.spectrum_scale) / 100.; - } + // classical (screensaver) mode, don't try to optimize screen usage & force some params + visu.row = 0; + visu.height = SB_HEIGHT; + visu.spectrum_scale = 0.25; + if (artwork.enable && artwork.y < SB_HEIGHT) visu.width = artwork.x - 1; + if (visu.mode == VISU_SPECTRUM) bars = visu.width / (htonl(pkt->channels[0].bar_width) + htonl(pkt->channels[0].bar_space)); + if (bars > MAX_BARS) bars = MAX_BARS; + } // try to adapt to what we have - if (visu.mode == VISU_SPECTRUM) { + if ((visu.mode & ~VISU_ESP32) == VISU_SPECTRUM) { visu.n = bars ? bars : MAX_BARS; if (visu.spectrum_scale <= 0 || visu.spectrum_scale > 0.5) visu.spectrum_scale = 0.5; spectrum_limits(0, visu.n, 0); @@ -836,7 +987,7 @@ static void visu_handler( u8_t *data, int len) { // reset bars maximum for (int i = visu.n; --i >= 0;) visu.bars[i].max = 0; - GDS_ClearExt(display, false, true, visu.col, visu.row, visu.col + visu.width - 1, visu.row - visu.height - 1); + GDS_ClearExt(display, 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,s:%.02f)", visu.n, visu.bar_border, visu.bar_width, visu.bar_gap, visu.border, visu.width, visu.height, visu.col, visu.row, visu.spectrum_scale); } else { diff --git a/plugin/SqueezeESP32.zip b/plugin/SqueezeESP32.zip index 871345cc..239a3142 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 225c907b..db62982a 100644 --- a/plugin/SqueezeESP32/Graphics.pm +++ b/plugin/SqueezeESP32/Graphics.pm @@ -14,6 +14,8 @@ my $VISUALIZER_NONE = 0; my $VISUALIZER_VUMETER = 1; my $VISUALIZER_SPECTRUM_ANALYZER = 2; my $VISUALIZER_WAVEFORM = 3; +my $VISUALIZER_VUMETER_ESP32 = 0x11; +my $VISUALIZER_SPECTRUM_ANALYZER_ESP32 = 0x12; { #__PACKAGE__->mk_accessor('array', 'modes'); @@ -71,7 +73,12 @@ sub displayWidth { } if ($display->widthOverride) { - return $display->widthOverride + ($display->modes->[$mode || 0]{_width} || 0); + my $artwork = $prefs->client($client)->get('artwork'); + if ($artwork->{'enable'} && $artwork->{'y'} < 32 && $client->isPlaying) { + return $artwork->{x} + ($display->modes->[$mode || 0]{_width} || 0); + } else { + return $display->widthOverride + ($display->modes->[$mode || 0]{_width} || 0); + } } else { return $display->modes->[$mode || 0]{width}; } @@ -100,10 +107,22 @@ sub build_modes { my $client = shift->client; my $cprefs = $prefs->client($client); - my $width = shift || $cprefs->get('width') || 128; + my $width = $cprefs->get('width') || 128; + my $artwork = $cprefs->get('artwork'); + + # if artwork is in main display, reduce width + $width = $artwork->{'x'} if $artwork->{'enable'} && $artwork->{y} < 32; + my $small_VU = $cprefs->get('small_VU'); my $spectrum = $cprefs->get('spectrum'); + my $small_spectrum_pos = { x => $width - int ($spectrum->{small}->{size} * $width / 100), + width => int ($spectrum->{small}->{size} * $width / 100), + }; + my $small_VU_pos = { x => $width - int ($small_VU * $width / 100), + width => int ($small_VU * $width / 100), + }; + my @modes = ( # mode 0 { desc => ['BLANK'], @@ -135,41 +154,41 @@ sub build_modes { params => [$VISUALIZER_NONE] }, # mode 7 { desc => ['VISUALIZER_VUMETER_SMALL'], - bar => 0, secs => 0, width => $width, _width => int -($small_VU*$width/100), + bar => 0, secs => 0, width => $width, _width => -$small_VU_pos->{'width'}, # extra parameters (width, height, col (< 0 = from right), row (< 0 = from bottom), left_space) - params => [$VISUALIZER_VUMETER, int ($small_VU*$width/100), 32, int -($small_VU*$width/100), 0, 2] }, + params => [$VISUALIZER_VUMETER_ESP32, $small_VU_pos->{'width'}, 32, $small_VU_pos->{'x'}, 0, 2] }, # mode 8 { desc => ['VISUALIZER_SPECTRUM_ANALYZER_SMALL'], - bar => 0, secs => 0, width => $width, _width => int -($spectrum->{small}->{size}*$width/100), - # extra parameters (width, height, col (< 0 = from right), row (< 0 = from bottom), left_space, bars) - params => [$VISUALIZER_SPECTRUM_ANALYZER, int ($spectrum->{small}->{size}*$width/100), 32, int -($spectrum->{small}->{size}*$width/100), 0, 2, int ($spectrum->{small}->{size}/100*$width/$spectrum->{small}->{band}), $spectrum->{scale}] }, + bar => 0, secs => 0, width => $width, _width => -$small_spectrum_pos->{'width'}, + # extra parameters (width, height, col (< 0 = from right), row (< 0 = from bottom), left_space, #bars, scale) + params => [$VISUALIZER_SPECTRUM_ANALYZER_ESP32, $small_spectrum_pos->{width}, 32, $small_spectrum_pos->{'x'}, 0, 2, $small_spectrum_pos->{'width'} / $spectrum->{small}->{band}, $spectrum->{scale}] }, # mode 9 { desc => ['VISUALIZER_VUMETER'], bar => 0, secs => 0, width => $width, - params => [$VISUALIZER_VUMETER] }, + params => [$VISUALIZER_VUMETER_ESP32] }, # mode 10 { desc => ['VISUALIZER_SPECTRUM_ANALYZER'], bar => 0, secs => 0, width => $width, # extra parameters (bars) - params => [$VISUALIZER_SPECTRUM_ANALYZER, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, + params => [$VISUALIZER_SPECTRUM_ANALYZER_ESP32, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, # mode 11 { desc => ['VISUALIZER_VUMETER', 'AND', 'ELAPSED'], bar => 0, secs => 1, width => $width, - params => [$VISUALIZER_VUMETER] }, + params => [$VISUALIZER_VUMETER_ESP32] }, # mode 12 { desc => ['VISUALIZER_SPECTRUM_ANALYZER', 'AND', 'ELAPSED'], bar => 0, secs => 1, width => $width, # extra parameters (bars) - params => [$VISUALIZER_SPECTRUM_ANALYZER, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, + params => [$VISUALIZER_SPECTRUM_ANALYZER_ESP32, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, # mode 13 { desc => ['VISUALIZER_VUMETER', 'AND', 'REMAINING'], bar => 0, secs => -1, width => $width, - params => [$VISUALIZER_VUMETER] }, + params => [$VISUALIZER_VUMETER_ESP32] }, # mode 14 { desc => ['VISUALIZER_SPECTRUM_ANALYZER', 'AND', 'REMAINING'], bar => 0, secs => -1, width => $width, # extra parameters (bars) - params => [$VISUALIZER_SPECTRUM_ANALYZER, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, + params => [$VISUALIZER_SPECTRUM_ANALYZER_ESP32, int ($width/$spectrum->{full}->{band}), $spectrum->{scale}] }, ); return \@modes; diff --git a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/basic.html b/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/basic.html deleted file mode 100644 index fdf77916..00000000 --- a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/basic.html +++ /dev/null @@ -1,20 +0,0 @@ -[% PROCESS settings/header.html %] - - [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_BANNER" %] -
[% "PLUGIN_SQUEEZEESP32_BANNER_TEXT" | string %]
- [% END %] - -
- - [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_WIDTH" desc="PLUGIN_SQUEEZEESP32_WIDTH_DESC" %] - - [% END %] - - [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_SPECTRUM_SCALE" desc="PLUGIN_SQUEEZEESP32_SPECTRUM_SCALE_DESC" %] - - [% END %] - - -
- -[% PROCESS settings/footer.html %] diff --git a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html b/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html index 17589674..5c0520fe 100644 --- a/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html +++ b/plugin/SqueezeESP32/HTML/EN/plugins/SqueezeESP32/settings/player.html @@ -24,5 +24,14 @@ [% END %] + [% WRAPPER setting title="PLUGIN_SQUEEZEESP32_ARTWORK" desc="PLUGIN_SQUEEZEESP32_ARTWORK_DESC" %] + [% "PLUGIN_SQUEEZEESP32_ARTWORK_ENABLE" | string %]  + + [% "PLUGIN_SQUEEZEESP32_ARTWORK_X" | string %]  + + [% "PLUGIN_SQUEEZEESP32_ARTWORK_Y" | string %]  + + [% END %] + [% PROCESS settings/footer.html %] diff --git a/plugin/SqueezeESP32/Player.pm b/plugin/SqueezeESP32/Player.pm index 946eb303..3ae11b17 100644 --- a/plugin/SqueezeESP32/Player.pm +++ b/plugin/SqueezeESP32/Player.pm @@ -20,24 +20,32 @@ sub playerSettingsFrame { my $value; my $id = unpack('C', $$data_ref); - - # New SETD command 0xfe for display width + + # New SETD command 0xfe for display width & height if ($id == 0xfe) { $value = (unpack('Cn', $$data_ref))[1]; if ($value > 100 && $value < 400) { $prefs->client($client)->set('width', $value); - $client->display->modes($client->display->build_modes($value)); + $client->display->modes($client->display->build_modes); $client->display->widthOverride(1, $value); $client->update; } - $log->info("Setting player width $value for ", $client->name); + my $height = (unpack('Cnn', $$data_ref))[2]; + $prefs->client($client)->set('height', $height || 0); + $log->info("Setting player $value" . "x" . "$height for ", $client->name); } $client->SUPER::playerSettingsFrame($data_ref); } -sub hasScrolling { +sub hasScrolling { return 1; } +sub reconnect { + my $client = shift; + $client->pluginData('artwork_md5', ''); + $client->SUPER::reconnect(@_); +} + 1; diff --git a/plugin/SqueezeESP32/PlayerSettings.pm b/plugin/SqueezeESP32/PlayerSettings.pm index 4c88dcfd..eb95f098 100644 --- a/plugin/SqueezeESP32/PlayerSettings.pm +++ b/plugin/SqueezeESP32/PlayerSettings.pm @@ -30,7 +30,7 @@ sub page { sub prefs { my ($class, $client) = @_; - my @prefs = qw(width small_VU spectrum); + my @prefs = qw(width small_VU spectrum artwork); return ($prefs->client($client), @prefs); } @@ -47,8 +47,20 @@ sub handler { full => { band => $paramRef->{'pref_spectrum_full_band'} }, }; $cprefs->set('spectrum', $spectrum); + my $artwork = { enable => $paramRef->{'pref_artwork_enable'}, + x => $paramRef->{'pref_artwork_x'}, + y => $paramRef->{'pref_artwork_y'}, + }; + $cprefs->set('artwork', $artwork); $client->display->modes($client->display->build_modes); $client->display->update; + + # force update or disable artwork + if ($artwork->{'enable'}) { + Plugins::SqueezeESP32::Plugin::update_artwork($client, 1); + } else { + Plugins::SqueezeESP32::Plugin::disable_artwork($client); + } } # as there is nothing captured, we need to re-set these variables @@ -56,9 +68,10 @@ sub handler { # here I don't know why you need to set again spectrum which is a reference # to a hash. Using $paramRef->{prefs} does not work either. It seems that - # soem are copies of value, some are references, can't figure out.This whole + # some are copies of value, some are references, can't figure out. This whole # logic of "Settings" is beyond me and I really hate it $paramRef->{'pref_spectrum'} = $cprefs->get('spectrum'); + $paramRef->{'pref_artwork'} = $cprefs->get('artwork'); return $class->SUPER::handler($client, $paramRef); } diff --git a/plugin/SqueezeESP32/Plugin.pm b/plugin/SqueezeESP32/Plugin.pm index 5078f66c..3e845542 100644 --- a/plugin/SqueezeESP32/Plugin.pm +++ b/plugin/SqueezeESP32/Plugin.pm @@ -3,8 +3,12 @@ package Plugins::SqueezeESP32::Plugin; use strict; use base qw(Slim::Plugin::Base); + +use Digest::MD5 qw(md5); +use List::Util qw(min); use Slim::Utils::Prefs; use Slim::Utils::Log; +use Slim::Web::ImageProxy; my $prefs = preferences('plugin.squeezeesp32'); @@ -28,6 +32,73 @@ sub initPlugin { $class->SUPER::initPlugin(@_); Slim::Networking::Slimproto::addPlayerClass($class, 100, 'squeezeesp32', { client => 'Plugins::SqueezeESP32::Player', display => 'Plugins::SqueezeESP32::Graphics' }); $log->info("Added class 100 for SqueezeESP32"); + + Slim::Control::Request::subscribe( sub { onNotification(@_) }, [ ['newmetadata'] ] ); + Slim::Control::Request::subscribe( sub { onNotification(@_) }, [ ['playlist'], ['open', 'newsong'] ]); +} + +sub onNotification { + my $request = shift; + my $client = $request->client; + + my $reqstr = $request->getRequestString(); + $log->info("artwork update notification $reqstr"); + #my $path = $request->getParam('_path'); + + update_artwork($client); +} + +sub update_artwork { + my $client = shift; + my $force = shift || 0; + my $cprefs = $prefs->client($client); + my $artwork = $cprefs->get('artwork'); + + return unless $client->model eq 'squeezeesp32' && $artwork->{'enable'}; + + my $s = $artwork->{'y'} >= 32 ? $cprefs->get('height') - $artwork->{'y'} : 32; + $s = min($s, $cprefs->get('width') - $artwork->{'x'}); + + my $path = 'music/current/cover_' . $s . 'x' . $s . '_o.jpg'; + my $body = Slim::Web::Graphics::artworkRequest($client, $path, $force, \&send_artwork, undef, HTTP::Response->new); + + send_artwork($client, undef, \$body) if $body; +} + +sub send_artwork { + my ($client, $force, $dataref) = @_; + + # I'm not sure why we are called so often, so only send when needed + my $md5 = md5($$dataref); + return if $client->pluginData('artwork_md5') eq $md5 && !$force; + + $client->pluginData('artwork', $dataref); + $client->pluginData('artwork_md5', $md5); + + my $artwork = $prefs->client($client)->get('artwork'); + my $length = length $$dataref; + my $offset = 0; + + $log->info("got resized artwork (length: ", length $$dataref, ")"); + + my $header = pack('Nnn', $length, $artwork->{'x'}, $artwork->{'y'}); + + while ($length > 0) { + $length = 1280 if $length > 1280; + $log->info("sending grfa $length"); + + my $data = $header . pack('N', $offset) . substr( $$dataref, 0, $length, '' ); + + $client->sendFrame( grfa => \$data ); + $offset += $length; + $length = length $$dataref; + } +} + +sub disable_artwork { + my ($client) = @_; + my $header = pack('N', 0); + $client->sendFrame( grfa => \$header ); } 1; diff --git a/plugin/SqueezeESP32/Settings.pm b/plugin/SqueezeESP32/Settings.pm deleted file mode 100644 index 1ef18504..00000000 --- a/plugin/SqueezeESP32/Settings.pm +++ /dev/null @@ -1,30 +0,0 @@ -package Plugins::SqueezeESP32::Settings; -use base qw(Slim::Web::Settings); - -use strict; - -use Slim::Utils::Prefs; -use Slim::Utils::Log; - -my $log = logger('plugin.squeezeesp32'); - -sub name { - return 'PLUGIN_SQUEEZEESP32'; -} - -sub page { - return 'plugins/SqueezeESP32/settings/basic.html'; -} - -sub prefs { - return (preferences('plugin.squeezeesp32'), qw(width spectrum_scale)); -} - -sub handler { - my ($class, $client, $params, $callback, @args) = @_; - - $callback->($client, $params, $class->SUPER::handler($client, $params), @args); -} - - -1; diff --git a/plugin/SqueezeESP32/install.xml b/plugin/SqueezeESP32/install.xml index 338f4283..d28e7f08 100644 --- a/plugin/SqueezeESP32/install.xml +++ b/plugin/SqueezeESP32/install.xml @@ -10,6 +10,6 @@ PLUGIN_SQUEEZEESP32 PLUGIN_SQUEEZEESP32_DESC Plugins::SqueezeESP32::Plugin - 0.31 + 0.50 Philippe diff --git a/plugin/SqueezeESP32/strings.txt b/plugin/SqueezeESP32/strings.txt index 1dd9e492..d5ed82c2 100644 --- a/plugin/SqueezeESP32/strings.txt +++ b/plugin/SqueezeESP32/strings.txt @@ -53,3 +53,19 @@ PLUGIN_SQUEEZEESP32_FULL_SPECTRUM_BAND PLUGIN_SQUEEZEESP32_FULL_SPECTRUM_BAND_DESC EN The number of bands is the width of the screen divided by this factor + +PLUGIN_SQUEEZEESP32_ARTWORK + EN Artwork + +PLUGIN_SQUEEZEESP32_ARTWORK_DESC + EN When Y position is less than 32, then artwork is display at the right of the main screen and x defines the starting position + EN Using artwork on less than 16-levels grayscale display if really poor quality + +PLUGIN_SQUEEZEESP32_ARTWORK_ENABLE + EN Enable + +PLUGIN_SQUEEZEESP32_ARTWORK_X + EN X + +PLUGIN_SQUEEZEESP32_ARTWORK_Y + EN Y diff --git a/plugin/repo.xml b/plugin/repo.xml index 45a2e536..b0266385 100644 --- a/plugin/repo.xml +++ b/plugin/repo.xml @@ -1,10 +1,10 @@ - + https://github.com/sle118/squeezelite-esp32 Philippe - 6dc35a0f9f9b287d205f7532cbb642b08407a284 + 47feaf69a40ad4f87c58b34212d71e60dca99d3e philippe_44@outlook.com SqueezeESP32 additional player id (100) http://github.com/sle118/squeezelite-esp32/raw/master/plugin/SqueezeESP32.zip