2
0
Эх сурвалжийг харах

refactor display(er) to welcome led extension

Philippe G 4 жил өмнө
parent
commit
d4cd400cd9

+ 4 - 4
components/display/core/gds.c

@@ -238,9 +238,9 @@ void GDS_SetContrast( struct GDS_Device* Device, uint8_t Contrast ) {
 	
 void GDS_SetLayout( struct GDS_Device* Device, bool HFlip, bool VFlip, bool Rotate ) { if (Device->SetLayout) Device->SetLayout( Device, HFlip, VFlip, Rotate ); }
 void GDS_SetDirty( struct GDS_Device* Device ) { Device->Dirty = true; }
-int	GDS_GetWidth( struct GDS_Device* Device ) { return Device->Width; }
-int	GDS_GetHeight( struct GDS_Device* Device ) { return Device->Height; }
-int	GDS_GetDepth( struct GDS_Device* Device ) { return Device->Depth; }
-int	GDS_GetMode( struct GDS_Device* Device ) { return Device->Mode; }
+int	GDS_GetWidth( struct GDS_Device* Device ) { return Device ? Device->Width : 0; }
+int	GDS_GetHeight( struct GDS_Device* Device ) { return Device ? Device->Height : 0; }
+int	GDS_GetDepth( struct GDS_Device* Device ) { return Device ? Device->Depth : 0; }
+int	GDS_GetMode( struct GDS_Device* Device ) { return Device ? Device->Mode : 0; }
 void GDS_DisplayOn( struct GDS_Device* Device ) { if (Device->DisplayOn) Device->DisplayOn( Device ); }
 void GDS_DisplayOff( struct GDS_Device* Device ) { if (Device->DisplayOff) Device->DisplayOff( Device ); }

+ 232 - 147
components/squeezelite/display.c → components/squeezelite/displayer.c

@@ -113,14 +113,23 @@ struct ANIC_header {
 	u8_t mode;
 };
 
+struct dmxt_packet {
+	char  opcode[4];
+	u16_t x;
+	u16_t length;
+};
+
 #pragma pack(pop)
 
 static struct {
 	TaskHandle_t task;
-	SemaphoreHandle_t mutex;
-	int width, height;
-	bool dirty;
-	bool owned;
+	int wake;
+	bool owned;	
+	struct {
+		SemaphoreHandle_t mutex;		
+		int width, height;
+		bool dirty;
+	};	
 } displayer = { .dirty = true, .owned = true };	
 
 static uint32_t *grayMap;
@@ -143,13 +152,13 @@ static uint32_t *grayMap;
 static struct scroller_s {
 	// copy of grfs content
 	u8_t  screen;	
-	u32_t pause, speed;
-	int wake;	
+	u32_t pause;
 	u16_t mode;	
 	s16_t by;
 	// scroller management & sharing between grfg and scrolling task
 	bool active, first, overflow;
 	int scrolled;
+	int speed, wake;	
 	struct {
 		u8_t *frame;
 		u32_t width;
@@ -167,7 +176,7 @@ static struct {
 	u8_t *data;
 	u32_t size;
 	u16_t x, y;
-	bool enable;
+	bool enable, full;
 } artwork;
 
 #define MAX_BARS	32
@@ -175,15 +184,13 @@ static struct {
 static EXT_RAM_ATTR struct {
 	int bar_gap, bar_width, bar_border;
 	bool rotate;
-	struct {
+	struct bar_s {
 		int current, max;
 		int limit;
 	} bars[MAX_BARS];
 	float spectrum_scale;
 	int n, col, row, height, width, border, style, max;
-	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];
+	enum { VISU_BLANK, VISU_VUMETER = 0x01, VISU_SPECTRUM = 0x02, VISU_WAVEFORM } mode;
 	struct {
 		u8_t *frame;
 		int width;
@@ -191,6 +198,18 @@ static EXT_RAM_ATTR struct {
 	} back;		
 } visu;
 
+static EXT_RAM_ATTR struct {
+	float fft[FFT_LEN*2], samples[FFT_LEN*2], hanning[FFT_LEN];
+	int levels[2];
+} meters;
+
+static EXT_RAM_ATTR struct {
+	int mode;
+	int max;
+	u16_t config;
+	struct bar_s bars[MAX_BARS] ;
+} led_visu;
+
 extern const uint8_t vu_bitmap[]   asm("_binary_vu_data_start");
 
 #define ANIM_NONE		  0x00
@@ -211,7 +230,7 @@ static bool (*display_bus_chain)(void *from, enum display_bus_cmd_e cmd);
 #define max(a,b) (((a) > (b)) ? (a) : (b))
 
 static void server(in_addr_t ip, u16_t hport, u16_t cport);
-static void sendSETD(u16_t width, u16_t height);
+static void sendSETD(u16_t width, u16_t height, u16_t led_config);
 static void sendANIC(u8_t code);
 static bool handler(u8_t *data, int len);
 static bool display_bus_handler(void *from, enum display_bus_cmd_e cmd);
@@ -222,8 +241,12 @@ 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 dmxt_handler(u8_t *data, int len);
 static void displayer_task(void* arg);
 
+// PLACEHOLDER
+void *led_display = 0x1000;
+
 /* scrolling undocumented information
 	grfs	
 		B: screen number
@@ -277,50 +300,61 @@ static void displayer_task(void* arg);
  Right channel parameters (not required for mono):
    4-5 - same as left channel parameters
 */
-
+ 
 /****************************************************************************************
  * 
  */
-bool sb_display_init(void) {
+bool sb_displayer_init(void) {
 	static DRAM_ATTR StaticTask_t xTaskBuffer __attribute__ ((aligned (4)));
 	static EXT_RAM_ATTR StackType_t xStack[SCROLL_STACK_SIZE] __attribute__ ((aligned (4)));
 	
 	// no display, just make sure we won't have requests
-	if (!display || GDS_GetWidth(display) <= 0 || GDS_GetHeight(display) <= 0) {
-		LOG_INFO("no display for LMS");
+	if ((GDS_GetWidth(display) <= 0 || GDS_GetHeight(display) <= 0) && !led_display) {
+		LOG_INFO("no display or led visualizer for LMS");
 		return false;
 	}	
 	
-	// inform LMS of our screen dimensions
-	sendSETD(GDS_GetWidth(display), GDS_GetHeight(display));
+	if (display) {
+		// need to force height to 32 maximum
+		displayer.width = GDS_GetWidth(display);
+		displayer.height = min(GDS_GetHeight(display), SB_HEIGHT);
 	
-	// need to force height to 32 maximum
-	displayer.width = GDS_GetWidth(display);
-	displayer.height = min(GDS_GetHeight(display), SB_HEIGHT);
+		// allocate gray-color mapping if needed;
+		if (GDS_GetMode(display) > GDS_GRAYSCALE) {
+			grayMap = malloc(256*sizeof(*grayMap));
+			for (int i = 0; i < 256; i++) grayMap[i] = GDS_GrayMap(display, i);
+		}
+	
+		// create visu configuration
+		visu.bar_gap = 1;
+		visu.back.frame = calloc(1, (displayer.width * displayer.height) / 8);
+		
+		// size scroller (width + current screen)
+		scroller.scroll.max = (displayer.width * displayer.height / 8) * (15 + 1);
+		scroller.scroll.frame = malloc(scroller.scroll.max);
+		scroller.back.frame = malloc(displayer.width * displayer.height / 8);
+		scroller.frame = malloc(displayer.width * displayer.height / 8);
+		
+		// chain handlers
+		display_bus_chain = display_bus;
+		display_bus = display_bus_handler;
+	}	
 	
-	// allocate gray-color mapping if needed;
-	if (GDS_GetMode(display) > GDS_GRAYSCALE) {
-		grayMap = malloc(256*sizeof(*grayMap));
-		for (int i = 0; i < 256; i++) grayMap[i] = GDS_GrayMap(display, i);
+	if (led_display) {
+		// PLACEHOLDER to init config
+		led_visu.mode = VISU_VUMETER;
 	}
 	
-	// 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);
+	// inform LMS of our screen/led dimensions
+	sendSETD(GDS_GetWidth(display), GDS_GetHeight(display), led_visu.config);
+	
+	dsps_fft2r_init_fc32(meters.fft, FFT_LEN);
+	dsps_wind_hann_f32(meters.hanning, FFT_LEN);
 		
-	// create scroll management task
+	// create displayer management task
 	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 (width + current screen)
-	scroller.scroll.max = (displayer.width * displayer.height / 8) * (15 + 1);
-	scroller.scroll.frame = malloc(scroller.scroll.max);
-	scroller.back.frame = malloc(displayer.width * displayer.height / 8);
-	scroller.frame = malloc(displayer.width * displayer.height / 8);
-
 	// chain handlers
 	slimp_handler_chain = slimp_handler;
 	slimp_handler = handler;
@@ -328,10 +362,7 @@ bool sb_display_init(void) {
 	notify_chain = server_notify;
 	server_notify = server;
 	
-	display_bus_chain = display_bus;
-	display_bus = display_bus_handler;
-	
-	return true;
+	return display != NULL;
 }
 
 /****************************************************************************************
@@ -380,14 +411,14 @@ static void sendANIC(u8_t code) {
 /****************************************************************************************
  * Send SETD for width
  */
-static void sendSETD(u16_t width, u16_t height) {
+static void sendSETD(u16_t width, u16_t height, u16_t led_config) {
 	struct SETD_header pkt_header;
 		
 	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) +  4 - 8);
+	pkt_header.length = htonl(sizeof(pkt_header) +  6 - 8);
 		
 	LOG_INFO("sending dimension %ux%u", width, height);	
 
@@ -398,6 +429,7 @@ static void sendSETD(u16_t width, u16_t height) {
 	send_packet((uint8_t *) &pkt_header, sizeof(pkt_header));
 	send_packet((uint8_t *) &width, 2);
 	send_packet((uint8_t *) &height, 2);
+	send_packet((uint8_t *) &led_config, 2);
 	UNLOCK_P;
 }
 
@@ -410,13 +442,13 @@ static void server(in_addr_t ip, u16_t hport, u16_t cport) {
 	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 	
 	sprintf(msg, "%s:%hu", inet_ntoa(ip), hport);
-	if (displayer.owned) GDS_TextPos(display, GDS_FONT_DEFAULT, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, msg);
+	if (display && displayer.owned) GDS_TextPos(display, GDS_FONT_DEFAULT, GDS_TEXT_CENTERED, GDS_TEXT_CLEAR | GDS_TEXT_UPDATE, msg);
 	displayer.dirty = true;
 	
 	xSemaphoreGive(displayer.mutex);
 		
 	// inform new LMS server of our capabilities
-	sendSETD(displayer.width, GDS_GetHeight(display));
+	sendSETD(GDS_GetWidth(display), GDS_GetHeight(display), led_visu.config);
 	
 	if (notify_chain) (*notify_chain)(ip, hport, cport);
 }
@@ -441,6 +473,8 @@ static bool handler(u8_t *data, int len){
 		grfa_handler(data, len);		
 	} else if (!strncmp((char*) data, "visu", 4)) {
 		visu_handler(data, len);
+	} else if (!strncmp((char*) data, "dmxt", 4)) {
+		dmxt_handler(data, len);		
 	} else {
 		res = false;
 	}
@@ -629,8 +663,7 @@ static void grfe_handler( u8_t *data, int len) {
 	scroller.active = false;
 	
 	// full screen artwork or for small screen, full screen visu has priority
-	if (((visu.mode & VISU_ESP32) && !visu.col && visu.row < displayer.height) ||
-		(artwork.enable && artwork.x == 0 && artwork.y == 0)) {
+	if (((visu.mode & VISU_ESP32) && !visu.col && visu.row < displayer.height) || artwork.full) {
 		xSemaphoreGive(displayer.mutex);
 		return;
 	}	
@@ -753,8 +786,7 @@ 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);
 
 	// full screen artwork or for small screen, visu has priority when full screen	
-	if (((visu.mode & VISU_ESP32) && !visu.col && visu.row < displayer.height) || 
-		(artwork.enable && artwork.x == 0 && artwork.y == 0)) {
+	if (((visu.mode & VISU_ESP32) && !visu.col && visu.row < displayer.height) || artwork.full) {
 		return;
 	}	
 	
@@ -808,6 +840,7 @@ static void grfa_handler(u8_t *data, int len) {
 			artwork.y = htons(pkt->y);		
 		} else if (artwork.size) GDS_ClearWindow(display, artwork.x, artwork.y, -1, -1, GDS_COLOR_BLACK);
 		
+		artwork.full = artwork.enable && artwork.x == 0 && artwork.y == 0;
 		LOG_INFO("gfra en:%u x:%hu, y:%hu", artwork.enable, artwork.x, artwork.y);
 		
 		// done in any case
@@ -825,6 +858,7 @@ static void grfa_handler(u8_t *data, int len) {
 		// now use new parameters
 		artwork.x = htons(pkt->x);
 		artwork.y = htons(pkt->y);
+		artwork.full = artwork.enable && artwork.x == 0 && artwork.y == 0;
 		if (artwork.data) free(artwork.data);
 		artwork.data = malloc(length);
 	}	
@@ -843,95 +877,57 @@ static void grfa_handler(u8_t *data, int len) {
 }
 
 /****************************************************************************************
- * Update visualization bars
+ * Fit spectrum into N bands and convert to dB
  */
-static void visu_update(void) {
-	// no update when artwork is full screen (but no need to protect against not owning the display as we are playing	
-	if ((artwork.enable && artwork.x == 0 && artwork.y == 0) || pthread_mutex_trylock(&visu_export.mutex)) {
-		return;
-	}	
-	
-	int mode = visu.mode & ~VISU_ESP32;
-				
-	// not enough frames
-	if (visu_export.level < (mode == VISU_VUMETER ? RMS_LEN : FFT_LEN) && visu_export.running) {
-		pthread_mutex_unlock(&visu_export.mutex);
-		return;
-	}
-	
-	// reset bars for all cases first	
-	for (int i = visu.n; --i >= 0;) visu.bars[i].current = 0;
-	
-	if (visu_export.running) {
+void spectrum_scale(int n, struct bar_s *bars, int max, float *samples) { 
+	float rate = visu_export.rate;			
+	// now arrange the result with the number of bar and sampling rate (don't want DC)
+	for (int i = 0, j = 1; i < 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_export.rate < bars[i].limit * FFT_LEN && j < FFT_LEN / 2; j++, count += 1) {
+			power += samples[2*j] * samples[2*j] + samples[2*j+1] * 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 - (bars[i].limit * FFT_LEN) / rate;
+			power += (samples[2*j] * samples[2*j] + samples[2*j+1] * samples[2*j+1]) * ratio;
 					
-		if (mode == VISU_VUMETER) {
-			s16_t *iptr = (s16_t*) visu_export.buffer + (BYTES_PER_FRAME / 4) - 1;
-			
-			// calculate sum(L²+R²), 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 += BYTES_PER_FRAME / 4;
-				visu.bars[1].current += (*iptr * *iptr + (1 << (RMS_LEN_BIT - 2))) >> (RMS_LEN_BIT - 1);
-				iptr += BYTES_PER_FRAME / 4;
-			}	
-		
-			// 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 = visu.max * (0.01667f*10*log10f(0.0000001f + (visu.bars[i].current >> (visu_export.gain == FIXED_ONE ? 8 : 1))) - 0.2543f);
-				if (visu.bars[i].current > visu.max) visu.bars[i].current = visu.max;
-				else if (visu.bars[i].current < 0) visu.bars[i].current = 0;
-			}
+			// normalize accumulated data
+			power /= (count + ratio) * 2;
 		} else {
-			s16_t *iptr = (s16_t*) visu_export.buffer + (BYTES_PER_FRAME / 4) - 1;
-			// 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) (*iptr + *(iptr+BYTES_PER_FRAME/4)) * visu.hanning[i];
-				visu.samples[i * 2 + 1] = 0;
-				iptr += 2 * BYTES_PER_FRAME / 4;
-			}
-
-			// 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);
-			float rate = visu_export.rate;
+			// no data for that band (sampling rate too high), just assume same as previous one
+			power = (samples[2*j] * samples[2*j] + samples[2*j+1] * samples[2*j+1]) / 2.;
+		}	
 			
-			// now arrange the result with the number of bar and sampling rate (don't want DC)
-			for (int i = 0, j = 1; i < visu.n && j < (FFT_LEN / 2); i++) {
-				float power, count;
+		// convert to dB and bars, same back-off
+		bars[i].current = max * (0.01667f*10*(log10f(0.0000001f + power) - log10f(FFT_LEN*(visu_export.gain == FIXED_ONE ? 256 : 2))) - 0.2543f);
+		if (bars[i].current > max) bars[i].current = max;
+		else if (bars[i].current < 0) bars[i].current = 0;
+	}	
+}		
 
-				// find the next point in FFT (this is real signal, so only half matters)
-				for (count = 0, power = 0; j * visu_export.rate < visu.bars[i].limit * FFT_LEN && 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 - (visu.bars[i].limit * FFT_LEN) / 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].current = visu.max * (0.01667f*10*(log10f(power) - log10f(FFT_LEN*(visu_export.gain == FIXED_ONE ? 256 : 2))) - 0.2543f);
-				if (visu.bars[i].current > visu.max) visu.bars[i].current = visu.max;
-				else if (visu.bars[i].current < 0) visu.bars[i].current = 0;
-			}	
-		}
-	} 
-		
-	// we took what we want, we can release the buffer
-	visu_export.level = 0;
-	pthread_mutex_unlock(&visu_export.mutex);
+/****************************************************************************************
+ * Fit levels to max and convert to dB
+ */
+void vu_scale(struct bar_s *bars, int max, int *levels) { 
+	// convert to dB (1 bit remaining for getting X²/N, 60dB dynamic starting from 0dBFS = 3 bits back-off)
+	for (int i = 2; --i >= 0;) {	 
+		bars[i].current = max * (0.01667f*10*log10f(0.0000001f + (levels[i] >> (visu_export.gain == FIXED_ONE ? 8 : 1))) - 0.2543f);
+		if (bars[i].current > max) bars[i].current = max;
+		else if (bars[i].current < 0) bars[i].current = 0;
+	}
+}	
 
+/****************************************************************************************
+ * visu draw
+ */
+void visu_draw(void) {
 	// don't refresh screen if all max are 0 (we were are somewhat idle)
 	int clear = 0;
 	for (int i = visu.n; --i >= 0;) clear = max(clear, visu.bars[i].max);
@@ -942,9 +938,8 @@ static void visu_update(void) {
 		GDS_DrawBitmapCBR(display, visu.back.frame, visu.back.width, displayer.height, GDS_COLOR_WHITE);
 	}	
 
-	if (mode != VISU_VUMETER || !visu.style) {
+	if ((visu.mode & ~VISU_ESP32) != VISU_VUMETER || !visu.style) {
 		// there is much more optimization to be done here, like not redrawing bars unless needed
-
 		for (int i = visu.n; --i >= 0;) {
 			// update maximum
 			if (visu.bars[i].current > visu.bars[i].max) visu.bars[i].max = visu.bars[i].current;
@@ -986,8 +981,79 @@ static void visu_update(void) {
 		int level = (visu.bars[0].current + visu.bars[1].current) / 2;
 		draw_VU(display, vu_bitmap, level, 0, visu.row, visu.rotate ? visu.height : visu.width, visu.rotate);		
 	}	
-}
+}	
 
+/****************************************************************************************
+ * Update displayer
+ */
+static void displayer_update(void) {
+	// no update when artwork is full screen and no led_strip (but no need to protect against not owning the display as we are playing	
+	if ((artwork.full && !led_visu.mode) || pthread_mutex_trylock(&visu_export.mutex)) {
+		return;
+	}	
+	
+	int mode = (visu.mode & ~VISU_ESP32) | led_visu.mode;
+				
+	// not enough frames
+	if (visu_export.level < (mode & VISU_SPECTRUM ? FFT_LEN : RMS_LEN) && visu_export.running) {
+		pthread_mutex_unlock(&visu_export.mutex);
+		return;
+	}
+	
+	// reset all levels no matter what
+	meters.levels[0] = meters.levels[1] = 0;
+	memset(meters.samples, 0, sizeof(meters.samples));	
+	
+	if (visu_export.running) {
+		
+		// calculate data for VU-meter						
+		if (mode & VISU_VUMETER) {
+			s16_t *iptr = (s16_t*) visu_export.buffer + (BYTES_PER_FRAME / 4) - 1;
+			int *left = &meters.levels[0], *right = &meters.levels[1];
+			// calculate sum(L²+R²), try to not overflow at the expense of some precision
+			for (int i = RMS_LEN; --i >= 0;) {
+				*left += (*iptr * *iptr + (1 << (RMS_LEN_BIT - 2))) >> (RMS_LEN_BIT - 1);
+				iptr += BYTES_PER_FRAME / 4;
+				*right += (*iptr * *iptr + (1 << (RMS_LEN_BIT - 2))) >> (RMS_LEN_BIT - 1);
+				iptr += BYTES_PER_FRAME / 4;
+			}	
+		}
+		
+		// calculate data for spectrum
+		if (mode & VISU_SPECTRUM) {
+			s16_t *iptr = (s16_t*) visu_export.buffer + (BYTES_PER_FRAME / 4) - 1;
+			// 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
+				meters.samples[i * 2 + 0] = (float) (*iptr + *(iptr+BYTES_PER_FRAME/4)) * meters.hanning[i];
+				meters.samples[i * 2 + 1] = 0;
+				iptr += 2 * BYTES_PER_FRAME / 4;
+			}
+
+			// actual FFT that might be less cycle than all the crap below		
+			dsps_fft2r_fc32_ae32(meters.samples, FFT_LEN);
+			dsps_bit_rev_fc32_ansi(meters.samples, FFT_LEN);
+		}	
+		
+	} 
+		
+	// we took what we want, we can release the buffer
+	visu_export.level = 0;
+	pthread_mutex_unlock(&visu_export.mutex);
+
+	// actualize the display
+	if (visu.mode && !artwork.full) {
+		if (visu.mode & VISU_SPECTRUM) spectrum_scale(visu.n, visu.bars, visu.max, meters.samples);
+		else for (int i = 2; --i >= 0;) vu_scale(visu.bars, visu.max, meters.levels);
+		visu_draw();
+	}	
+	
+	// actualize led_vu
+	if (led_visu.mode) {
+		// PLACEHOLDER to handle led_display. you need potentially scaling of spectrum (X and Y) 
+		// and scaling of levels (Y) and then call the 
+	}
+}
 
 /****************************************************************************************
  * Calculate spectrum spread
@@ -1129,7 +1195,7 @@ static void visu_handler( u8_t *data, int len) {
 			if (visu.row < displayer.height) scroller.active = false;
 			vTaskResume(displayer.task);
 		}	
-		visu.wake = 0;
+		displayer.wake = 0;
 		
 		// reset bars maximum
 		for (int i = visu.n; --i >= 0;) visu.bars[i].max = 0;
@@ -1144,6 +1210,25 @@ static void visu_handler( u8_t *data, int len) {
 	xSemaphoreGive(displayer.mutex);
 }	
 
+/****************************************************************************************
+ * Dmx style packet handler
+ * ToDo: make packet match dmx protocol format
+ */
+static void dmxt_handler( u8_t *data, int len) {
+	struct dmxt_packet *pkt = (struct dmxt_packet*) data;
+	uint16_t offset = htons(pkt->x);
+	uint16_t length = htons(pkt->length);
+
+	LOG_INFO("dmx packet len:%u offset:%u", length, offset);
+
+	xSemaphoreTake(displayer.mutex, portMAX_DELAY);
+
+	// PLACEHOLDER
+	//led_vu_data(data + sizeof(struct dmxt_packet), offset, length);
+
+	xSemaphoreGive(displayer.mutex);
+}	
+
 /****************************************************************************************
  * Scroll task
  *  - with the addition of the visualizer, it's a bit a 2-headed beast not easy to 
@@ -1156,15 +1241,15 @@ static void displayer_task(void *args) {
 		xSemaphoreTake(displayer.mutex, portMAX_DELAY);
 		
 		// suspend ourselves if nothing to do, grfg or visu will wake us up
-		if (!scroller.active && !visu.mode)  {
+		if (!scroller.active && !visu.mode && !led_visu.mode)  {
 			xSemaphoreGive(displayer.mutex);
 			vTaskSuspend(NULL);
 			xSemaphoreTake(displayer.mutex, portMAX_DELAY);
-			scroller.wake = visu.wake = 0;
+			scroller.wake = displayer.wake = 0;
 		}	
-		
+
 		// go for long sleep when either item is disabled
-		if (!visu.mode) visu.wake = LONG_WAKE;
+		if (!visu.mode && !led_visu.mode) displayer.wake = LONG_WAKE;
 		if (!scroller.active) scroller.wake = LONG_WAKE;
 								
 		// scroll required amount of columns (within the window)
@@ -1200,20 +1285,20 @@ static void displayer_task(void *args) {
 		}
 
 		// update visu if active
-		if (visu.mode && visu.wake <= 0) {
-			visu_update();
-			visu.wake = 100;
+		if ((visu.mode || led_visu.mode) && displayer.wake <= 0) {
+			displayer_update();
+			displayer.wake = 100;
 		}
 		
 		// need to make sure we own display
-		if (displayer.owned) GDS_Update(display);
+		if (display && displayer.owned) GDS_Update(display);
 		
 		// release semaphore and sleep what's needed
 		xSemaphoreGive(displayer.mutex);
 		
-		sleep = min(visu.wake, scroller.wake);
+		sleep = min(displayer.wake, scroller.wake);
 		vTaskDelay(sleep / portTICK_PERIOD_MS);
 		scroller.wake -= sleep;
-		visu.wake -= sleep;
+		displayer.wake -= sleep;
 	}	
 }	

+ 2 - 2
components/squeezelite/embedded.c

@@ -45,14 +45,14 @@ uint32_t _gettime_ms_(void) {
 }
 
 extern void sb_controls_init(void);
-extern bool sb_display_init(void);
+extern bool sb_displayer_init(void);
 
 u8_t custom_player_id = 12;
 
 void embedded_init(void) {
 	mutex_create(slimp_mutex);
 	sb_controls_init();
-	custom_player_id = sb_display_init() ? 100 : 101;
+	custom_player_id = sb_displayer_init() ? 100 : 101;
 }
 
 u16_t get_RSSI(void) {