Razer Switchblade + Winamp Love
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:
- Reading values from the Winamp visualization library
- Calculating values to project on the display
- 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.