comparison foosdk/sdk/foobar2000/foo_sample/ui_and_threads.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
comparison
equal deleted inserted replaced
0:e9bb126753e7 1:20d02a178406
1 #include "stdafx.h"
2 #include "resource.h"
3
4 // Modern multi threading with C++
5 // Or: how I learned to stop worrying and love the lambdas
6
7 #ifdef _WIN32
8
9 #include <memory> // shared_ptr
10 #include <libPPUI/CDialogResizeHelper.h>
11 #include <helpers/filetimetools.h>
12 #include <helpers/duration_counter.h>
13 #include <helpers/atl-misc.h>
14
15
16
17 namespace { // anon namespace local classes for good measure
18
19
20 class CDemoDialog; // forward declaration
21
22 // This is kept a separate shared_ptr'd struct because it may outlive CDemoDialog instance.
23 struct sharedData_t {
24 metadb_handle_list items;
25 CDemoDialog * owner; // weak reference to the owning dialog; can be only used after checking the validity by other means.
26 };
27
28 static const CDialogResizeHelper::Param resizeData[] = {
29 // Dialog resize handling matrix, defines how the controls scale with the dialog
30 // L T R B
31 {IDOK, 1,1,1,1 },
32 {IDCANCEL, 1,1,1,1 },
33 {IDC_HEADER, 0,0,1,0 },
34 {IDC_LIST, 0,0,1,1 },
35
36 // current position of a control is determined by initial_position + factor * (current_dialog_size - initial_dialog_size)
37 // where factor is the value from the table above
38 // applied to all four values - left, top, right, bottom
39 // 0,0,0,0 means that a control doesn't react to dialog resizing (aligned to top+left, no resize)
40 // 1,1,1,1 means that the control is aligned to bottom+right but doesn't resize
41 // 0,0,1,0 means that the control disregards vertical resize (aligned to top) and changes its width with the dialog
42 };
43
44 // Minimum/maximum size, in dialog box units; see MSDN MapDialogRect for more info about dialog box units.
45 // The values can be declared constant here and will be scaled appropriately depending on display DPI.
46 static const CRect resizeMinMax(150, 100, 1000, 1000);
47
48 class CDemoDialog : public CDialogImpl<CDemoDialog> {
49 public:
50 enum { IDD = IDD_THREADS };
51 CDemoDialog( metadb_handle_list_cref items ) : m_resizer(resizeData, resizeMinMax) {
52 m_sharedData = std::make_shared< sharedData_t > ();
53 m_sharedData->items = items;
54 m_sharedData->owner = this;
55 }
56
57 BEGIN_MSG_MAP_EX(CDemoDialog)
58 CHAIN_MSG_MAP_MEMBER(m_resizer)
59 MSG_WM_INITDIALOG(OnInitDialog)
60 COMMAND_HANDLER_EX(IDOK, BN_CLICKED, OnOK)
61 COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
62 MSG_WM_CLOSE(OnClose)
63 MSG_WM_DESTROY(OnDestroy)
64 MSG_WM_SIZE(OnSize)
65 END_MSG_MAP()
66 private:
67 BOOL OnInitDialog(CWindow, LPARAM) {
68 uSetDlgItemText(*this, IDC_HEADER, PFC_string_formatter() << "Selected: " << m_sharedData->items.get_size() << " tracks." );
69 m_listBox = GetDlgItem(IDC_LIST);
70
71 m_statusBar.Create(*this, NULL, TEXT(""), WS_CHILD | WS_VISIBLE);
72
73 m_statusBar.SetWindowText(L"Ready");
74
75 ShowWindow(SW_SHOW);
76
77 return TRUE; // system should set focus
78 }
79 void OnSize(UINT nType, CSize size) {
80 // Tell statusbar that we got resized. CDialogResizeHelper can't do this for us.
81 m_statusBar.SendMessage(WM_SIZE);
82 }
83 void OnDestroy() {
84 cancelTask();
85 }
86
87 void OnClose() {
88 // NOTE if we do not handle WM_CLOSE, WM_COMMAND with IDCANCEL will be invoked, executing our cancel handler.
89 // We provide our own WM_CLOSE handler to provide a different response to closing the window.
90 DestroyWindow();
91 }
92
93 void OnCancel(UINT, int, CWindow) {
94 // If a task is active, cancel it
95 // otherwise destroy the dialog
96 if (! cancelTask() ) {
97 DestroyWindow();
98 } else {
99 // Refresh UI
100 taskCompleted();
101 }
102 }
103 void OnOK(UINT, int, CWindow) {
104 startTask();
105 }
106 void startTask() {
107 cancelTask(); // cancel any running task
108
109 GetDlgItem(IDCANCEL).SetWindowText(L"Cancel");
110 m_statusBar.SetWindowText(L"Working...");
111
112 auto shared = m_sharedData;
113 auto aborter = std::make_shared<abort_callback_impl>();
114 m_aborter = aborter;
115
116 // New in fb2k 1.4.5: async_task_manager & splitTask
117 // Use fb2k::splitTask() for starting detached threads.
118 // In fb2k < 1.4.5, it will fall back to just starting a detached thread.
119 // fb2k 1.4.5+ async_task_manager cleanly deals with user exiting foobar2000 while a detached async task is running.
120 // Shutdown of foobar2000 process will be stalled until your task completes.
121 // If you use other means to ensure that the thread has finished, such as waiting for the thread to exit in your dialog's destructor, there's no need for this.
122
123 fb2k::splitTask( [aborter, shared] {
124 // In worker thread!
125 try {
126 work( shared, aborter );
127 } catch(exception_aborted const &) {
128 return; // user abort?
129 } catch(std::exception const & e) {
130 // should not really get here
131 logLineProc( shared, aborter, PFC_string_formatter() << "Critical error: " << e);
132 }
133 try {
134 mainThreadOp( aborter, [shared] {
135 shared->owner->taskCompleted();
136 } );
137 } catch(...) {} // mainThreadOp may throw exception_aborted
138 } );
139 }
140 void taskCompleted() {
141 m_aborter.reset();
142 GetDlgItem(IDCANCEL).SetWindowText(L"Close");
143 m_statusBar.SetWindowText(L"Finished, ready");
144 }
145
146 static void mainThreadOp(std::shared_ptr<abort_callback_impl> aborter, std::function<void ()> op ) {
147 aborter->check(); // are we getting aborted?
148 fb2k::inMainThread( [=] {
149 if ( aborter->is_set() ) return; // final user abort check
150 // Past this, we're main thread, the task has not been cancelled by the user and the dialog is still alive
151 // and any dialog methods can be safely called
152 op();
153 } );
154 }
155
156 static void logLineProc(std::shared_ptr<sharedData_t> shared, std::shared_ptr<abort_callback_impl> aborter, const char * line_ ) {
157 pfc::string8 line( line_ ); // can't hold to the const char* we got passed, we have no guarantees about its lifetime
158 mainThreadOp( aborter, [shared, line] {
159 shared->owner->logLine(line);
160 } );
161 }
162 static void work( std::shared_ptr<sharedData_t> shared, std::shared_ptr<abort_callback_impl> aborter ) {
163 // clear the log
164 mainThreadOp(aborter, [shared] {
165 shared->owner->clearLog();
166 } );
167
168 // A convenience wrapper that calls logLineProc()
169 auto log = [shared, aborter] ( const char * line ) {
170 logLineProc(shared, aborter, line);
171 };
172 // Use log(X) instead of logLineProc(shared, aborter, X)
173
174 for( size_t trackWalk = 0; trackWalk < shared->items.get_size(); ++ trackWalk ) {
175 aborter->check();
176 auto track = shared->items[trackWalk];
177 log( PFC_string_formatter() << "Track: " << track );
178
179 try {
180 const auto path = track->get_path();
181 const auto subsong = track->get_subsong_index();
182
183 // Not strictly needed, but we do it anyway
184 // Acquire a read lock on the file, so anyone trying to acquire a write lock will just wait till we have finished
185 auto lock = file_lock_manager::get()->acquire_read(path, *aborter);
186
187 {
188 input_decoder::ptr dec;
189 input_entry::g_open_for_decoding(dec, nullptr, path, *aborter);
190
191 file_info_impl info;
192 dec->get_info( subsong, info, *aborter );
193 auto title = info.meta_get("title",0);
194 if ( title == nullptr ) log("Untitled");
195 else log(PFC_string_formatter() << "Title: " << title );
196 if ( info.get_length() > 0 ) log(PFC_string_formatter() << "Duration: " << pfc::format_time_ex(info.get_length(),6) );
197 auto stats = dec->get_file_stats( *aborter );
198 if ( stats.m_size != filesize_invalid ) log( PFC_string_formatter() << "Size: " << pfc::format_file_size_short(stats.m_size) );
199 if ( stats.m_timestamp != filetimestamp_invalid ) log( PFC_string_formatter() << "Last modified: " << format_filetimestamp( stats.m_timestamp ) );
200
201
202 dec->initialize( subsong, input_flag_simpledecode, * aborter );
203 audio_chunk_impl chunk;
204 uint64_t numChunks = 0, numSamples = 0;
205 // duration_counter tool is a strictly accurate audio duration counter retaining all sample counts passed to it, immune to floatingpoint accuracy errors
206 duration_counter duration;
207 bool firstChunk = true;
208 while(dec->run(chunk, *aborter) ) {
209 if ( firstChunk ) {
210 auto spec = chunk.get_spec();
211 log(PFC_string_formatter() << "Audio sample rate: " << spec.sampleRate );
212 log(PFC_string_formatter() << "Audio channels: " << audio_chunk::g_formatChannelMaskDesc( spec.chanMask ) );
213 firstChunk = false;
214 }
215 ++ numChunks;
216 duration += chunk;
217 numSamples += chunk.get_sample_count();
218 }
219 log(PFC_string_formatter() << "Decoded " << numChunks << " chunks");
220 log(PFC_string_formatter() << "Exact duration decoded: " << pfc::format_time_ex(duration.query(), 6) << ", " << numSamples << " samples" );
221 }
222
223 try {
224 auto aa = album_art_extractor::g_open( nullptr, path, *aborter );
225
226 if ( aa->have_entry( album_art_ids::cover_front, *aborter ) ) {
227 log("Album art: front cover found");
228 }
229 if ( aa->have_entry( album_art_ids::cover_back, *aborter ) ) {
230 log("Album art: back cover found");
231 }
232 if (aa->have_entry( album_art_ids::artist, *aborter ) ) {
233 log("Album art: artist picture found");
234 }
235 } catch(exception_album_art_not_found) {
236 } catch(exception_album_art_unsupported_format) {
237 }
238
239 } catch(exception_aborted) {
240 throw;
241 } catch(std::exception const & e) {
242 log( PFC_string_formatter() << "Failure: " << e);
243 }
244 }
245 log("All done.");
246 }
247
248 bool cancelTask() {
249 bool ret = false;
250 auto aborter = pfc::replace_null_t(m_aborter);
251 if (aborter) {
252 ret = true;
253 aborter->set();
254 logLine("Aborted by user.");
255 }
256 return ret;
257 }
258
259 void logLine( const char * line ) {
260 m_listBox.AddString( pfc::stringcvt::string_os_from_utf8(line) );
261 }
262
263 void clearLog() {
264 m_listBox.ResetContent();
265 }
266
267
268 // Worker thread aborter. It's re-created with the thread. If the task is ran more than once, each time it gets a new one.
269 // A commonly used alternative is to have abort_callback_impl m_aborter, and a blocking cancelTask() that waits for the thread to exit, without all the shared_ptrs and recreation of aborters.
270 // However that approach will freeze the UI if the worker thread is taking a long time to exit, as well as require some other shared_ptr based means for fb2k::inMainThread() ops to verify that the task is not being aborted / the dialog still exists.
271 // Therefore we use a shared_ptr'd aborter, which is used both to abort worker threads, and for main thread callbacks to know if the task that sent them is still valid.
272 std::shared_ptr<abort_callback_impl> m_aborter;
273
274 // Data shared with the worker thread. It is created only once per dialog lifetime.
275 std::shared_ptr< sharedData_t > m_sharedData;
276
277 CListBox m_listBox;
278
279 CStatusBarCtrl m_statusBar;
280
281 CDialogResizeHelper m_resizer;
282 };
283 }
284
285 void RunUIAndThreads(metadb_handle_list_cref data) {
286 // Equivalent to new CDemoDialog(data), with modeless registration and auto lifetime
287 fb2k::newDialog<CDemoDialog>( data );
288 }
289
290 #else
291
292 void RunUIAndThreads(metadb_handle_list_cref data) {
293 popup_message::g_showToast("Not implemented for Mac OS yet");
294 }
295
296 #endif