├── .gitignore ├── CMakeLists.txt ├── README.md ├── src └── foo_vis_projectM.cpp └── vcpkg.json /.gitignore: -------------------------------------------------------------------------------- 1 | /build* 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) 2 | 3 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 4 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 5 | 6 | project(foo_vis_projectM) 7 | 8 | if("${FOOBAR_SDK_ROOT}" STREQUAL "") 9 | message(FATAL_ERROR "Unable to find the foobar2000 SDK. Please set FOOBAR_SDK_ROOT to the root directory containing foobar2000 SDK. Get it here: http://www.foobar2000.org/SDK") 10 | elseif(NOT EXISTS "${FOOBAR_SDK_ROOT}/foobar2000/SDK/foobar2000.h") 11 | message(FATAL_ERROR "Unable to find the foobar2000 SDK in ${FOOBAR_SDK_ROOT}") 12 | else() 13 | include_directories("${FOOBAR_SDK_ROOT}") 14 | include_directories("${FOOBAR_SDK_ROOT}/foobar2000") 15 | endif() 16 | 17 | if("${WTL_ROOT}" STREQUAL "") 18 | message(FATAL_ERROR "Unable to find WTL. Please set WTL_ROOT to the root directory containing WTL. Get it here: http://wtl.sourceforge.net/") 19 | elseif(NOT EXISTS "${WTL_ROOT}/Include/atlapp.h") 20 | message(FATAL_ERROR "Unable to find WTL in ${WTL_ROOT}") 21 | else() 22 | include_directories("${WTL_ROOT}/Include") 23 | endif() 24 | 25 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 26 | set(ARCH "x64/") 27 | set(ARCH2 "-x64") 28 | else() 29 | set(ARCH "") 30 | set(ARCH2 "-Win32") 31 | endif() 32 | 33 | find_package(libprojectM REQUIRED) 34 | find_package(GLEW REQUIRED) 35 | find_package(ZLIB REQUIRED) 36 | 37 | file(GLOB SRCS_CXX "src/*.cpp") 38 | file(GLOB SRCS_H "src/*.h") 39 | file(GLOB RES_RC "src/*.rc") 40 | 41 | set(FOOBAR_SDK_LIBRARIES 42 | optimized "${FOOBAR_SDK_ROOT}/foobar2000/SDK/${ARCH}Release/foobar2000_SDK.lib" 43 | optimized "${FOOBAR_SDK_ROOT}/foobar2000/foobar2000_component_client/${ARCH}Release/foobar2000_component_client.lib" 44 | optimized "${FOOBAR_SDK_ROOT}/foobar2000/helpers/${ARCH}Release/foobar2000_sdk_helpers.lib" 45 | optimized "${FOOBAR_SDK_ROOT}/foobar2000/shared/shared${ARCH2}.lib" 46 | optimized "${FOOBAR_SDK_ROOT}/pfc/${ARCH}Release/pfc.lib" 47 | optimized "${FOOBAR_SDK_ROOT}/libPPUI/${ARCH}Release/libPPUI.lib" 48 | debug "${FOOBAR_SDK_ROOT}/foobar2000/SDK/${ARCH}Debug/foobar2000_SDK.lib" 49 | debug "${FOOBAR_SDK_ROOT}/foobar2000/foobar2000_component_client/${ARCH}Debug/foobar2000_component_client.lib" 50 | debug "${FOOBAR_SDK_ROOT}/foobar2000/helpers/${ARCH}Debug/foobar2000_sdk_helpers.lib" 51 | debug "${FOOBAR_SDK_ROOT}/foobar2000/shared/shared${ARCH2}.lib" 52 | debug "${FOOBAR_SDK_ROOT}/pfc/${ARCH}Debug/pfc.lib" 53 | debug "${FOOBAR_SDK_ROOT}/libPPUI/${ARCH}Debug/libPPUI.lib" 54 | ) 55 | 56 | add_library(foo_vis_projectM MODULE ${SRCS_CXX} ${SRCS_H} ${RES_RC}) 57 | target_link_libraries(foo_vis_projectM ${FOOBAR_SDK_LIBRARIES} libprojectM::static GLEW::GLEW ZLIB::ZLIB) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/projectM-visualizer/projectm/raw/master/web/logo.png) 2 | 3 | ## projectM - The most advanced open-source music visualizer 4 | projectM is an open-source project that reimplements the esteemed [Winamp Milkdrop](https://en.wikipedia.org/wiki/MilkDrop) by Geiss in a more modern, cross-platform reusable library. 5 | 6 | Its purpose in life is to read an audio input and to produce mesmerizing visuals, detecting tempo, and rendering advanced equations into a limitless array of user-contributed visualizations. 7 | 8 | This is [foobar2000](https://www.foobar2000.org) visualization plugin. 9 | 10 | Using [projectM](https://github.com/projectM-visualizer/projectm) source tree ([forked](https://github.com/djdron/projectm/tree/zipfs) with some improvements like access files in .zip archive). 11 | 12 | Some options added (press right mouse button to show menu). 13 | 14 | All presets & textures from ["Cream of the Crop" database](https://thefulldomeblog.com/2020/02/21/nestdrop-presets-collection-cream-of-the-crop/) added (9,795 Milkdrop presets). 15 | 16 | Database is ~120 Mb in size. 17 | 18 | Project now uses C-API and moved to separate github repository. 19 | -------------------------------------------------------------------------------- /src/foo_vis_projectM.cpp: -------------------------------------------------------------------------------- 1 | #define UNICODE 2 | #define _UNICODE 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | DECLARE_COMPONENT_VERSION("projectM visualizer", "0.0.5", 14 | "projectM - The most advanced open-source music visualizer\n" 15 | "Copyright (C) 2003 - 2022 projectM Team\n" 16 | "foobar2000 plugin by djdron (C) 2020 - 2022.\n\n" 17 | 18 | "Distributed under the terms of GNU LGPL v2.1\n" 19 | "Source code can be obtained from:\n" 20 | "https://github.com/projectM-visualizer/projectm\n" 21 | "https://github.com/djdron/foo_vis_projectM\n" 22 | ); 23 | 24 | VALIDATE_COMPONENT_FILENAME("foo_vis_projectM.dll"); 25 | 26 | namespace 27 | { 28 | 29 | static const GUID guid_cfg_preset_lock = { 0xe5be745e, 0xab65, 0x4b69, { 0xa1, 0xf3, 0x1e, 0xfb, 0x08, 0xff, 0x4e, 0xcf } }; 30 | static const GUID guid_cfg_preset_shuffle = { 0x659c6787, 0x97bb, 0x485b, { 0xa0, 0xfc, 0x45, 0xfb, 0x12, 0xb7, 0x3a, 0xa0 } }; 31 | static const GUID guid_cfg_preset_name = { 0x186c5741, 0x701e, 0x4f2c, { 0xb4, 0x41, 0xe5, 0x57, 0x5c, 0x18, 0xb0, 0xa8 } }; 32 | static const GUID guid_cfg_preset_duration = { 0x48d9b7f5, 0x4446, 0x4ab7, { 0xb8, 0x71, 0xef, 0xc7, 0x59, 0x43, 0xb9, 0xcd } }; 33 | 34 | static cfg_bool cfg_preset_lock(guid_cfg_preset_lock, false); 35 | static cfg_bool cfg_preset_shuffle(guid_cfg_preset_shuffle, true); 36 | static cfg_string cfg_preset_name(guid_cfg_preset_name, ""); 37 | static cfg_int cfg_preset_duration(guid_cfg_preset_duration, 20); 38 | 39 | static bool try_fullscreen = false; 40 | 41 | class ui_element_instance_projectM : public ui_element_instance, public CWindowImpl 42 | { 43 | public: 44 | DECLARE_WND_CLASS_EX(TEXT("{09E9C47E-87E7-45CD-9C16-1A0926E90FAD}"), CS_DBLCLKS|CS_OWNDC, (-1)); 45 | 46 | ui_element_instance_projectM(ui_element_config::ptr p_config, ui_element_instance_callback_ptr p_callback) : m_config(p_config), m_callback(p_callback) {} 47 | 48 | void initialize_window(HWND parent); 49 | 50 | BEGIN_MSG_MAP_EX(ui_element_instance_projectM) 51 | MSG_WM_CREATE(OnCreate) 52 | MSG_WM_DESTROY(OnDestroy) 53 | MSG_WM_LBUTTONDBLCLK(OnLButtonDblClk) 54 | MSG_WM_NCPAINT(OnNCPaint) 55 | MSG_WM_PAINT(OnPaint) 56 | MSG_WM_SIZE(OnSize) 57 | MSG_WM_TIMER(OnSizeTimer) 58 | MSG_WM_CONTEXTMENU(OnContextMenu) 59 | END_MSG_MAP() 60 | 61 | virtual void set_configuration(ui_element_config::ptr config) override { m_config = config; } 62 | virtual ui_element_config::ptr get_configuration() override { return m_config; } 63 | 64 | enum ui_menu_id 65 | { 66 | ID_FULLSCREEN = 1, 67 | ID_PRESET_LOCK, ID_PRESET_SHUFFLE, ID_PRESET_NEXT, ID_PRESET_PREVIOUS, ID_PRESET_RANDOM, 68 | ID_DURATION_5, ID_DURATION_10, ID_DURATION_20, ID_DURATION_30, ID_DURATION_45, ID_DURATION_60 69 | }; 70 | virtual bool edit_mode_context_menu_test(const POINT& p_point, bool p_fromkeyboard) { return true; } 71 | virtual void edit_mode_context_menu_build(const POINT& p_point, bool p_fromkeyboard, HMENU p_menu, unsigned p_id_base) override { ContextMenuBuild(p_menu, p_id_base); } 72 | virtual void edit_mode_context_menu_command(const POINT& p_point, bool p_fromkeyboard, unsigned p_id, unsigned p_id_base) override { ContextMenuCommand(p_id - p_id_base); } 73 | virtual bool edit_mode_context_menu_get_description(unsigned p_id, unsigned p_id_base, pfc::string_base& p_out) override { return ContextMenuGetDesc(p_id - p_id_base, p_out); } 74 | 75 | static GUID g_get_guid() { 76 | static const GUID guid_myelem = { 0x489c7f0e, 0x2073, 0x442b, {0xaf, 0x4a, 0x00, 0x51, 0x99, 0x12, 0xaf, 0x70 } }; 77 | return guid_myelem; 78 | } 79 | static GUID g_get_subclass() { return ui_element_subclass_playback_visualisation; } 80 | static void g_get_name(pfc::string_base & out) { out = "projectM"; } 81 | static ui_element_config::ptr g_get_default_configuration() { return ui_element_config::g_create_empty(g_get_guid()); } 82 | static const char * g_get_description() { return "projectM visualization."; } 83 | 84 | private: 85 | LRESULT OnCreate(LPCREATESTRUCT cs); 86 | void OnDestroy(); 87 | void CreateProjectM(int width, int height); 88 | void OnLButtonDblClk(UINT nFlags, CPoint point) { ToggleFullScreen(); } 89 | void OnNCPaint(CRgnHandle); 90 | void OnPaint(CDCHandle); 91 | void OnSize(UINT nType, CSize size); 92 | void OnSizeTimer(UINT_PTR id); 93 | void OnContextMenu(CWindow wnd, CPoint point); 94 | 95 | void AddPCM(); 96 | 97 | static VOID CALLBACK TimerRoutine(PVOID lpParam, BOOLEAN TimerOrWaitFired); 98 | static void PresetSwitchedCallback(bool is_hard_cut, unsigned int index, void* user_data); 99 | 100 | void OnTimer(); 101 | void OnPresetSwitched(bool is_hard_cut, unsigned int index); 102 | 103 | void ToggleFullScreen() 104 | { 105 | try_fullscreen = true; 106 | static_api_ptr_t()->toggle_fullscreen(g_get_guid(), core_api::get_main_window()); 107 | try_fullscreen = false; 108 | } 109 | 110 | bool SetupGLContext() 111 | { 112 | if(m_projectM && m_GLrc) 113 | { 114 | wglMakeCurrent(GetDC(), m_GLrc); 115 | return true; 116 | } 117 | return false; 118 | } 119 | 120 | void ContextMenuBuild(HMENU p_menu, unsigned p_id_base); 121 | bool ContextMenuGetDesc(int p_id, pfc::string_base& p_out); 122 | void ContextMenuCommand(int cmd); 123 | 124 | private: 125 | visualisation_stream_v2::ptr m_vis_stream; 126 | projectm* m_projectM = NULL; 127 | HGLRC m_GLrc = NULL; 128 | double m_last_time = 0.0; 129 | 130 | HANDLE m_timerQueue = CreateTimerQueue(); 131 | HANDLE m_timer = NULL; 132 | HANDLE m_timerDoneEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 133 | bool is_fullscreen = try_fullscreen; 134 | 135 | protected: 136 | ui_element_config::ptr m_config; 137 | const ui_element_instance_callback_ptr m_callback; 138 | }; 139 | 140 | void ui_element_instance_projectM::initialize_window(HWND parent) 141 | { 142 | WIN32_OP(Create(parent) != NULL); 143 | } 144 | 145 | typedef BOOL(WINAPI *PFNWGLSWAPINTERVALEXTPROC)(int interval); 146 | static void VsyncGL(bool on) 147 | { 148 | static bool inited = false; 149 | static PFNWGLSWAPINTERVALEXTPROC si = NULL; 150 | if(!inited) 151 | { 152 | si = (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalEXT"); 153 | inited = true; 154 | } 155 | if(si) si(on); 156 | } 157 | 158 | void ui_element_instance_projectM::CreateProjectM(int width, int height) 159 | { 160 | assert(!m_projectM); 161 | 162 | if(m_GLrc) 163 | wglMakeCurrent(GetDC(), m_GLrc); 164 | 165 | float heightWidthRatio = (float)height / (float)width; 166 | 167 | projectm_settings settings = {}; 168 | settings.window_width = width; 169 | settings.window_height = height; 170 | settings.mesh_x = 128; 171 | settings.mesh_y = int(settings.mesh_x * heightWidthRatio); 172 | settings.fps = 60; 173 | settings.soft_cut_duration = 3; // seconds 174 | settings.preset_duration = cfg_preset_duration; // seconds 175 | settings.hard_cut_enabled = true; 176 | settings.hard_cut_duration = 20; 177 | settings.hard_cut_sensitivity = 1.0; 178 | settings.beat_sensitivity = 1.0; 179 | settings.aspect_correction = true; 180 | settings.shuffle_enabled = cfg_preset_shuffle; 181 | 182 | std::string base_path = core_api::get_my_full_path(); 183 | std::string::size_type t = base_path.rfind('\\'); 184 | if(t != std::string::npos) base_path.erase(t + 1); 185 | std::string data_zip = base_path + "data.zip"; 186 | settings.data_dir = const_cast(data_zip.c_str()); 187 | // init with settings 188 | m_projectM = projectm_create_settings(&settings, PROJECTM_FLAG_NONE); 189 | projectm_set_preset_switched_event_callback(m_projectM, PresetSwitchedCallback, this); 190 | bool select_random_preset = true; 191 | if(!cfg_preset_name.isEmpty()) 192 | { 193 | auto idx = projectm_get_preset_index(m_projectM, cfg_preset_name.c_str()); 194 | if(idx > 0) 195 | { 196 | projectm_select_preset(m_projectM, idx, true); 197 | select_random_preset = false; 198 | } 199 | } 200 | if(select_random_preset) 201 | projectm_select_random_preset(m_projectM, true); 202 | if(cfg_preset_lock) 203 | projectm_lock_preset(m_projectM, true); 204 | 205 | VsyncGL(true); 206 | } 207 | 208 | LRESULT ui_element_instance_projectM::OnCreate(LPCREATESTRUCT cs) 209 | { 210 | if(is_fullscreen) 211 | { 212 | // prevent OpenGL driver to switch to Exclusive Full Screen mode - we need composition for popup menu 213 | // https://www.anthropicstudios.com/2021/02/20/fullscreen-exclusive-is-a-lie/ 214 | uAddWindowExStyle(*this, WS_EX_STATICEDGE); 215 | } 216 | 217 | PIXELFORMATDESCRIPTOR pfd = 218 | { 219 | sizeof(PIXELFORMATDESCRIPTOR), 1, 220 | PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, // Flags 221 | PFD_TYPE_RGBA, // The kind of framebuffer. RGBA or palette. 222 | 32, // Colordepth of the framebuffer. 223 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224 | 24, // Number of bits for the depthbuffer 225 | 8, // Number of bits for the stencilbuffer 226 | 0, // Number of Aux buffers in the framebuffer. 227 | PFD_MAIN_PLANE, 0, 0, 0, 0 228 | }; 229 | 230 | HDC dc = GetDC(); 231 | int pf = ChoosePixelFormat(dc, &pfd); 232 | SetPixelFormat(dc, pf, &pfd); 233 | 234 | m_GLrc = wglCreateContext(dc); 235 | 236 | static_api_ptr_t vis_manager; 237 | vis_manager->create_stream(m_vis_stream, 0); 238 | m_vis_stream->request_backlog(0.8); 239 | 240 | // console::formatter() << "projectM: OnCreate"; 241 | 242 | return 0; 243 | } 244 | void ui_element_instance_projectM::OnDestroy() 245 | { 246 | m_vis_stream.release(); 247 | SetupGLContext(); 248 | if(m_projectM) 249 | { 250 | projectm_destroy(m_projectM); 251 | m_projectM = NULL; 252 | } 253 | wglMakeCurrent(NULL, NULL); 254 | if(m_GLrc) 255 | { 256 | wglDeleteContext(m_GLrc); 257 | m_GLrc = NULL; 258 | } 259 | 260 | if(m_timer) 261 | WaitForSingleObject(m_timerDoneEvent, INFINITE); 262 | CloseHandle(m_timerDoneEvent); 263 | DeleteTimerQueue(m_timerQueue); 264 | 265 | // console::formatter() << "projectM: OnDestroy"; 266 | } 267 | 268 | void ui_element_instance_projectM::OnTimer() 269 | { 270 | Invalidate(); 271 | SetEvent(m_timerDoneEvent); 272 | } 273 | 274 | VOID CALLBACK ui_element_instance_projectM::TimerRoutine( 275 | PVOID lpParam, BOOLEAN TimerOrWaitFired) 276 | { 277 | auto ui = (ui_element_instance_projectM *)lpParam; 278 | ui->OnTimer(); 279 | } 280 | 281 | void ui_element_instance_projectM::OnPresetSwitched(bool is_hard_cut, unsigned int index) 282 | { 283 | const char* name = projectm_get_preset_name(m_projectM, index); 284 | cfg_preset_name = name; 285 | projectm_free_string(name); 286 | } 287 | 288 | void ui_element_instance_projectM::PresetSwitchedCallback(bool is_hard_cut, unsigned int index, void* user_data) 289 | { 290 | auto ui = (ui_element_instance_projectM*)user_data; 291 | ui->OnPresetSwitched(is_hard_cut, index); 292 | } 293 | 294 | void ui_element_instance_projectM::OnNCPaint(CRgnHandle rgn) 295 | { 296 | if(!is_fullscreen) 297 | { 298 | SetMsgHandled(FALSE); 299 | return; 300 | } 301 | RECT rect; 302 | GetWindowRect(&rect); 303 | HRGN region = CreateRectRgn(rect.left, rect.top, rect.right, rect.bottom); 304 | if(HDC dc = GetDCEx(region, DCX_WINDOW | DCX_CACHE | DCX_INTERSECTRGN | DCX_LOCKWINDOWUPDATE)) 305 | { 306 | if(HBRUSH brush = CreateSolidBrush(RGB(0, 0, 0))) 307 | { 308 | RECT r; 309 | r.left = r.top = 0; 310 | r.right = rect.right - rect.left; 311 | r.bottom = rect.bottom - rect.top; 312 | FillRect(dc, &r, brush); 313 | DeleteObject(brush); 314 | } 315 | ReleaseDC(dc); 316 | } 317 | else 318 | { 319 | DeleteObject(region); 320 | } 321 | } 322 | 323 | void ui_element_instance_projectM::OnPaint(CDCHandle) 324 | { 325 | if(SetupGLContext()) 326 | { 327 | glClearColor(0.0, 0.0, 0.0, 0.0); 328 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 329 | 330 | projectm_render_frame(m_projectM); 331 | 332 | glFlush(); 333 | 334 | SwapBuffers(GetDC()); 335 | } 336 | 337 | ValidateRect(NULL); 338 | 339 | AddPCM(); 340 | 341 | ResetEvent(m_timerDoneEvent); 342 | CreateTimerQueueTimer(&m_timer, m_timerQueue, (WAITORTIMERCALLBACK)TimerRoutine, this, 10, 0, 0); 343 | } 344 | 345 | void ui_element_instance_projectM::OnSize(UINT nType, CSize size) 346 | { 347 | if(size.cx && size.cy) 348 | { 349 | SetTimer(1, 100); 350 | // console::formatter() << "projectM: OnSize " << size.cx << ", " << size.cy; 351 | } 352 | } 353 | void ui_element_instance_projectM::OnSizeTimer(UINT_PTR id) 354 | { 355 | KillTimer(1); 356 | 357 | RECT r; 358 | GetClientRect(&r); 359 | 360 | int width = r.right - r.left; 361 | int height = r.bottom - r.top; 362 | 363 | // console::formatter() << "projectM: OnSizeTimer " << width << ", " << height; 364 | 365 | if(!width || !height) 366 | return; 367 | 368 | if(width < 128) width = 128; 369 | if(height < 128) height = 128; 370 | 371 | if(!m_projectM) 372 | { 373 | CreateProjectM(width, height); 374 | } 375 | else if(SetupGLContext()) 376 | { 377 | projectm_set_window_size(m_projectM, width, height); 378 | } 379 | } 380 | 381 | void ui_element_instance_projectM::OnContextMenu(CWindow wnd, CPoint point) 382 | { 383 | if(m_callback->is_edit_mode_enabled()) 384 | { 385 | SetMsgHandled(FALSE); 386 | return; 387 | } 388 | if(point == CPoint(-1, -1)) 389 | { 390 | CRect rc; 391 | WIN32_OP(wnd.GetWindowRect(&rc)); 392 | point = rc.CenterPoint(); 393 | } 394 | CMenu menu; 395 | WIN32_OP(menu.CreatePopupMenu()); 396 | ContextMenuBuild(menu, 0); 397 | menu.SetMenuDefaultItem(ID_FULLSCREEN); 398 | 399 | CMenuSelectionReceiver_UiElement descriptions(this, 0); 400 | auto cmd = menu.TrackPopupMenuEx(TPM_RIGHTBUTTON|TPM_NONOTIFY|TPM_RETURNCMD, point.x, point.y, descriptions, NULL); 401 | ContextMenuCommand(cmd); 402 | } 403 | 404 | void ui_element_instance_projectM::ContextMenuBuild(HMENU p_menu, unsigned p_id_base) 405 | { 406 | CMenuHandle menu(p_menu); 407 | menu.AppendMenu(MF_STRING, p_id_base + ID_FULLSCREEN, L"Toggle Full-Screen Mode"); 408 | menu.AppendMenu(MF_SEPARATOR); 409 | 410 | menu.AppendMenu(MF_STRING|(cfg_preset_lock ? MF_CHECKED : 0), p_id_base + ID_PRESET_LOCK, L"Lock Current Preset"); 411 | menu.AppendMenu(MF_STRING|(cfg_preset_shuffle ? MF_CHECKED : 0), p_id_base + ID_PRESET_SHUFFLE, L"Shuffle Presets"); 412 | menu.AppendMenu(MF_STRING, p_id_base + ID_PRESET_NEXT, L"Next Preset"); 413 | menu.AppendMenu(MF_STRING, p_id_base + ID_PRESET_PREVIOUS, L"Previous Preset"); 414 | menu.AppendMenu(MF_STRING, p_id_base + ID_PRESET_RANDOM, L"Random Preset"); 415 | 416 | CMenuHandle menu_duration; 417 | WIN32_OP(menu_duration.CreatePopupMenu()); 418 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_5, L"5"); 419 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_10, L"10"); 420 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_20, L"20"); 421 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_30, L"30"); 422 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_45, L"45"); 423 | menu_duration.AppendMenu(MF_STRING, p_id_base + ID_DURATION_60, L"60"); 424 | auto DurationToId = [](int duration) 425 | { 426 | switch(duration) 427 | { 428 | case 5: return ID_DURATION_5; 429 | case 10: return ID_DURATION_10; 430 | case 20: return ID_DURATION_20; 431 | case 30: return ID_DURATION_30; 432 | case 45: return ID_DURATION_45; 433 | case 60: return ID_DURATION_60; 434 | } 435 | return ID_DURATION_20; 436 | }; 437 | menu_duration.CheckMenuRadioItem(p_id_base + ID_DURATION_5, p_id_base + ID_DURATION_60, p_id_base + DurationToId(cfg_preset_duration), MF_BYCOMMAND); 438 | menu.AppendMenu(MF_STRING, menu_duration, L"Preset Duration"); 439 | } 440 | 441 | bool ui_element_instance_projectM::ContextMenuGetDesc(int p_id, pfc::string_base& p_out) 442 | { 443 | switch(p_id) 444 | { 445 | case ID_FULLSCREEN: p_out = "Toggles full-screen mode."; break; 446 | case ID_PRESET_LOCK: p_out = "Lock the current preset."; break; 447 | case ID_PRESET_SHUFFLE: p_out = "Shuffle presets."; break; 448 | case ID_PRESET_NEXT: p_out = "Switch to next preset (without shuffle)."; break; 449 | case ID_PRESET_PREVIOUS:p_out = "Switch to previous preset (without shuffle)."; break; 450 | case ID_PRESET_RANDOM: p_out = "Switch to random preset."; break; 451 | case ID_DURATION_5: p_out = "Duration 5 seconds."; break; 452 | case ID_DURATION_10: p_out = "Duration 10 seconds."; break; 453 | case ID_DURATION_20: p_out = "Duration 20 seconds."; break; 454 | case ID_DURATION_30: p_out = "Duration 30 seconds."; break; 455 | case ID_DURATION_45: p_out = "Duration 45 seconds."; break; 456 | case ID_DURATION_60: p_out = "Duration 60 seconds."; break; 457 | default: return false; 458 | } 459 | return true; 460 | } 461 | 462 | void ui_element_instance_projectM::ContextMenuCommand(int cmd) 463 | { 464 | switch(cmd) 465 | { 466 | case ID_FULLSCREEN: 467 | ToggleFullScreen(); 468 | break; 469 | case ID_PRESET_LOCK: 470 | cfg_preset_lock = !cfg_preset_lock; 471 | projectm_lock_preset(m_projectM, cfg_preset_lock); 472 | break; 473 | case ID_PRESET_SHUFFLE: 474 | cfg_preset_shuffle = !cfg_preset_shuffle; 475 | projectm_set_shuffle_enabled(m_projectM, cfg_preset_shuffle); 476 | break; 477 | case ID_PRESET_NEXT: 478 | if(SetupGLContext()) 479 | projectm_select_next_preset(m_projectM, true); 480 | break; 481 | case ID_PRESET_PREVIOUS: 482 | if(SetupGLContext()) 483 | projectm_select_previous_preset(m_projectM, true); 484 | break; 485 | case ID_PRESET_RANDOM: 486 | if(SetupGLContext()) 487 | projectm_select_random_preset(m_projectM, true); 488 | break; 489 | case ID_DURATION_5: 490 | cfg_preset_duration = 5; 491 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 492 | break; 493 | case ID_DURATION_10: 494 | cfg_preset_duration = 10; 495 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 496 | break; 497 | case ID_DURATION_20: 498 | cfg_preset_duration = 20; 499 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 500 | break; 501 | case ID_DURATION_30: 502 | cfg_preset_duration = 30; 503 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 504 | break; 505 | case ID_DURATION_45: 506 | cfg_preset_duration = 45; 507 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 508 | break; 509 | case ID_DURATION_60: 510 | cfg_preset_duration = 60; 511 | projectm_set_preset_duration(m_projectM, cfg_preset_duration); 512 | break; 513 | } 514 | } 515 | 516 | void ui_element_instance_projectM::AddPCM() 517 | { 518 | if(!m_vis_stream.is_valid() || !m_projectM) return; 519 | 520 | double time; 521 | if(!m_vis_stream->get_absolute_time(time)) return; 522 | 523 | double dt = time - m_last_time; 524 | m_last_time = time; 525 | 526 | double min_time = 1.0/1000.0; 527 | double max_time = 1.0/10.0; 528 | 529 | bool use_fake = false; 530 | 531 | if(dt < min_time) 532 | { 533 | dt = min_time; 534 | use_fake = true; 535 | } 536 | if(dt > max_time) dt = max_time; 537 | 538 | audio_chunk_impl chunk; 539 | if(use_fake || !m_vis_stream->get_chunk_absolute(chunk, time - dt, dt)) 540 | m_vis_stream->make_fake_chunk_absolute(chunk, time - dt, dt); 541 | t_size count = chunk.get_sample_count(); 542 | auto channels = chunk.get_channel_count(); 543 | std::vector data(count*channels, 0); 544 | audio_math::convert_to_int16(chunk.get_data(), count*channels, data.data(), 1.0); 545 | if(channels == 2) 546 | projectm_pcm_add_int16(m_projectM, data.data(), count, PROJECTM_STEREO); 547 | else 548 | projectm_pcm_add_int16(m_projectM, data.data(), count, PROJECTM_MONO); 549 | } 550 | 551 | class ui_element_projectM : public ui_element_impl_visualisation {}; 552 | static service_factory_single_t g_ui_element_projectM_factory; 553 | 554 | } 555 | //namespace 556 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo-vis-projectm", 3 | "version-string": "0.0.5-dev", 4 | "dependencies": [ 5 | "glew", 6 | "zlib" 7 | ] 8 | } 9 | --------------------------------------------------------------------------------