Razer Switchblade + Winamp Love

cover-switchblade

 

 

 

 

 

I have been working on a side project to extend the Winamp visualization to the Razer Switchblade UI. My project is entirely based upon the Tiny3D visualization framework’s wrapper for Winamp plugins because the Winamp IP has been in transition and developer resources are currently pretty scarce. Big thanks to the contributors on that project. Anyways, let’s talk about the Winamp plugin.

About

This plugin takes Winamp spectrum analysis data and projects it onto Switchblade displays. So, if you have a DeathStriker Ultimate or are lucky enough to have the Razer “Pro” models, you can check the plugin out. What happens when you run the plugin is that your trackpad is replaced with a music visualization. The following videos highlight the plugin features, sorry for the loud mouthbreathing, I had to normalize the audio:

[NSFW - HEADS UP, language ahead]

The rest is kid-safe.

Implementation

Alright, so there’s the pretty lights, how does it work? I’ll break it down into 3 sections:

  1. Reading values from the Winamp visualization library
  2. Calculating values to project on the display
  3. Rendering to the display

In short, you read current audio values from the plugin, create a visualization based on the values, and render to the Switchblade display.

Reading Values from Winamp

To read values from Winamp, the Tiny3d framework makes things simple. First, you initialize the module:

int visInit(struct winampVisModule *this_mod)
{
	// init Win32 stuff
	int styles; // our Window styles

	WNDCLASS wc; // our Window class
	HWND (*e)(embedWindowState *v);


	// OpenGL pixel format related
	PIXELFORMATDESCRIPTOR pfd;
	int nPixelFormat;

	getVisInstance()->myWindowState.flags |= EMBED_FLAGS_NOTRANSPARENCY;   
	getVisInstance()->myWindowState.r.left		= 0;
	getVisInstance()->myWindowState.r.top		= 0;
	getVisInstance()->myWindowState.r.right		= VIS_SCENE_WIDTH;
	getVisInstance()->myWindowState.r.bottom	= VIS_SCENE_HEIGHT;
   
	*(void**)&e = (void *)SendMessage(this_mod->hwndParent,WM_WA_IPC,(LPARAM)0,IPC_GET_EMBEDIF);    

	if (!e)
	{
		MessageBox(this_mod->hwndParent,"This plugin requires Winamp 5.0+.","Error",MB_OK | MB_ICONERROR);
		return 1;
	}

	parent = e(&getVisInstance()->myWindowState);

	SetWindowText(getVisInstance()->myWindowState.me, this_mod->description);
	
	memset(&wc,0,sizeof(wc));
	wc.lpfnWndProc = WndProc;
	wc.hInstance = this_mod->hDllInstance;
	wc.lpszClassName = VIS_USER_CLASS;
	
	if (!RegisterClass(&wc)) 
	{
		MessageBox(this_mod->hwndParent,"Error registering window class.","Error",MB_OK | MB_ICONERROR);
		return 1;
	}

	styles = WS_VISIBLE|WS_CHILDWINDOW|WS_OVERLAPPED|WS_CLIPCHILDREN|WS_CLIPSIBLINGS;
	styles|= CS_HREDRAW | CS_VREDRAW | CS_OWNDC; //add more

	getVisInstance()->hWnd = CreateWindowEx(
		0,
		VIS_USER_CLASS,
		NULL,
		styles,
		0,0,
		VIS_SCENE_WIDTH,VIS_SCENE_HEIGHT,
		parent,
		NULL,
		this_mod->hDllInstance,
		0);

	if (!getVisInstance()->hWnd) 
	{
		MessageBox(this_mod->hwndParent,"Error while creating window.","Error",MB_OK | MB_ICONERROR);
		return 1;
	}

	SetWindowLong(getVisInstance()->hWnd,GWL_USERDATA,(LONG)this_mod); 
	SendMessage(this_mod->hwndParent, WM_WA_IPC, (WPARAM)getVisInstance()->hWnd, IPC_SETVISWND);

	// Enable OpenGL on the created Window
	if (!(getVisInstance()->hDC=GetDC(getVisInstance()->hWnd)))
	{
		return 1;
	}

	memset(&pfd,0,sizeof(pfd));
	pfd.nSize      = sizeof(pfd);
	pfd.nVersion   = 1;
	pfd.dwFlags    = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 16; // we use 16bit color depth for better compatibility
	pfd.cDepthBits = 16; // Z-Buffer
	pfd.iLayerType = PFD_MAIN_PLANE;

	nPixelFormat = ChoosePixelFormat(getVisInstance()->hDC, &pfd);
	SetPixelFormat(getVisInstance()->hDC, nPixelFormat, &pfd);

	if (!(getVisInstance()->hRC=wglCreateContext( getVisInstance()->hDC )))
	{
		return 1;
	}
   
    
	if(!wglMakeCurrent(getVisInstance()->hDC,getVisInstance()->hRC))
	{
		return 1;
	}
	
	glShadeModel(GL_SMOOTH);
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	glClearDepth(1.0f);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	// setup the perspective :P
	resizeGLWindow(VIS_SCENE_HEIGHT,VIS_SCENE_WIDTH);
	// show the window
	ShowWindow(parent,SW_SHOWNORMAL);

    	
    RzSBStart();

    // Next time use noun project
    // TODO: Are these images correctly getting added as a DLL resource?    
    LoadKeyImageToRazer(".\\imagedata\\rewind.png",RZSBSDK_DK_6, RZSBSDK_KEYSTATE_UP);
    LoadKeyImageToRazer(".\\imagedata\\play.png",RZSBSDK_DK_7, RZSBSDK_KEYSTATE_UP);
    LoadKeyImageToRazer(".\\imagedata\\fforward.png",RZSBSDK_DK_8, RZSBSDK_KEYSTATE_UP);
    LoadKeyImageToRazer(".\\imagedata\\volup.png",RZSBSDK_DK_9, RZSBSDK_KEYSTATE_UP);
    LoadKeyImageToRazer(".\\imagedata\\voldown.png",RZSBSDK_DK_10, RZSBSDK_KEYSTATE_UP);
    RzSBDynamicKeySetCallback(OnDkClickedButton);

    // END UNSTABLE

	return 0;
}

Next, you reference the structure and read values:

this_mod->spectrumData[0][<0...255>]

It’s as easy as that. Once you have your values, you are ready to render.

Calculating the vizualization

Now that you have values for various parts of the waveform, you can determine how to render them. I calculate a gradient of colors and then either draw parts of it based on the waveform data or ignore it and draw black. The following code shows how it’s done:

unsigned short __inline COLORFROMROW(int row)
{
    if (colormode == 0){
        if (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) > 0){
            return ARGB2RGB565( (int)
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 0) | 
                ((int)(row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE)) << 16)
            );
        }
        return ARGB2RGB565(0);
    } else if (colormode == 1){
    
        if (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) > 0){
            return ARGB2RGB565( (int)
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 8) | 
                ((int)(row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE)))
            );
        }
        return ARGB2RGB565(0);
    } else  if (colormode == 2){        
        if (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) > 0){
            return ARGB2RGB565( (int)
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) ) | 
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 8) | 
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 16)
            );
        }
        return ARGB2RGB565(0);
    } else if (colormode == 3) {        
        if (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) > 0){
            return ARGB2RGB565( (int)
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 16) | 
                ((int)(row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE)) << 8)
            );
        }
        return ARGB2RGB565(0);
    } else {        
        if (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) > 0){
            return ARGB2RGB565( (int)
                ((int)(256 - (row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE) ) ) << 8) | 
                ((int)(row * (256.0 / SWITCHBLADE_TOUCHPAD_Y_SIZE)) << 16)
            );
        }
        return ARGB2RGB565(0);
    }
}

int visRender(struct winampVisModule *this_mod)
{
    // C - Counter for pixels
    // row - counter for screen rows
    // col - counter for screen cols
    // amplitude - scale for wave drawn
    // DIVSCALE - calculated scale for mapping winamp waveform data
    // DIVS - number of ?? unused?
    // divCount - counter for number of columns traversed
    // divLimit - Number of columns
    // blkAreaStep - step counter for blank area between bars
    // BLK_LIMIT - the limit in pixels for the blank area.
    int c=0, row=0, col=0, DIVSCALE=57, DIVS=1, divCount=0, divLimit=50, blkAreaStep=0, BLK_LIMIT=3;
    double AMPLITUDE=.15;
    // step - counter for speed regulation
    // rows - limiter for drawing to rows
    // rowsDir - direction to move rows
    // stepSpeed - speed for redrawing the bars, higher value = lower speed, smoother
    // speed - rate of drawing the columns, higher value = faster motion
    //         note: should be a factor of 576, e.g. 1,2,3,4,6,8,16,18,24, ... 576
    static int step = 0, rows = 0, speed = 16, rowsDir = 50, stepSpeed = 2;

    // stores cached waveform data
    int* limitBuffer;

    // used to trigger blank / nonblank areas
    bool drawNextPixels = true;
    // RGB buffer for keyboard screen
    unsigned short* g_rgb565;
    RZSBSDK_BUFFERPARAMS bp;
    HRESULT ret = S_OK;
    size_t sNumPixels;

    //divLimit = SWITCHBLADE_TOUCHPAD_X_SIZE / DIVS;
    limitBuffer = (int*)malloc(sizeof(int) * divLimit);
    rows += rowsDir;
    DIVSCALE = 576 / divLimit;

	// start OpenGL rendering
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
    
    // RENDERING CODE GOES HERE

	// Buffer size--how many pixels in the buffers
    sNumPixels = SWITCHBLADE_TOUCHPAD_X_SIZE * SWITCHBLADE_TOUCHPAD_Y_SIZE;
    g_rgb565 = (unsigned short*)malloc(sizeof(unsigned short) * sNumPixels);

        
    for (divCount = 0; divCount < divLimit; divCount++)
    {
        // TODO: smooth amplitude based on average window.
        limitBuffer[divCount] = 
            (int)((this_mod->spectrumData[0][divCount] * DIVSCALE + 
            this_mod->spectrumData[1][divCount] * DIVSCALE)  * AMPLITUDE);
    }
        
    drawNextPixels = limitBuffer[divCount] > row;

    // Traverse the display space and set pixels.
    for (row = SWITCHBLADE_TOUCHPAD_Y_SIZE - 1; row >= 0; row--) 
    {          
        for (col=0; col < SWITCHBLADE_TOUCHPAD_X_SIZE; col++)
        {
            if ((col % (SWITCHBLADE_TOUCHPAD_X_SIZE / divLimit) == 0))
            {
                if (divCount >= (divLimit-1))
                {
                    divCount = 0;                
                }
                else
                {
                    divCount++;
                }                
                drawNextPixels = limitBuffer[divCount] > row;
                for (blkAreaStep = 0; blkAreaStep < BLK_LIMIT; blkAreaStep++)
                {
                    if (col < SWITCHBLADE_TOUCHPAD_X_SIZE){
                        g_rgb565[c++] = ARGB2RGB565(0x00000000);
                        col++;
                    }                        
                }
            }

            // Set blank pixels in the RGB buffer for values out of range or draw the current pixel color.
            if (drawNextPixels)
            {
                g_rgb565[c++] = COLORFROMROW(row);
            }
            else
            {
                g_rgb565[c++] = ARGB2RGB565(0x00000000);
            }
        }

        // Reverse directions when reaching ends of the display.
        if (rows < 1)
        {
            rowsDir = speed;
        }
        if (rows > SWITCHBLADE_TOUCHPAD_Y_SIZE)
        {
            rowsDir = -1*speed;
        }
	}

Rendering the visualization

The Razer switchblade UI has a custom data format for rendering pixels. It also has various methods to simplify rendering. The following code shows how to pass your render buffer to the Razer for rendering:

    free(limitBuffer);

    // Set the buffer parameters for SwitchBlade LCD
	memset(&bp, 0, sizeof(RZSBSDK_BUFFERPARAMS));
	bp.pData = (BYTE *)g_rgb565;
	bp.DataSize = sNumPixels * sizeof(WORD);
	bp.PixelType = RGB565;

	// Send the stream and render the buffer
    ret = RzSBRenderBuffer(RZSBSDK_DISPLAY_WIDGET, &bp);

    //TODO: render stuff to the keys!
    //ret = RzSBRenderBuffer(RZSBSDK_DISPLAY_DK_1, &bp);
    
    free(g_rgb565);
  
	return 0;
}

Happy Hacking!

So that’s all there is to it. If you want to make your own visualizations, check out the code in the visRender method and hack away. The scaffolding for the sample I created is the Winamp spectrum data from Tiny3D and the demonstration of how to calculate and render pixel data to the display. All you need to do is fork my git repo and experiment with changes to the visualization code.

There’s more to the plugin than what I’ve covered here (dynamic keys, message sending, init and destroy, oh my!). Let me know if you have any questions! I’d be happy to ramp up a collaborator.

See Also