Mercurial > audlegacy-plugins
comparison src/rootvis/rootvis.c @ 900:d985f0dcdeb0 trunk
[svn] - add a starting point for xmms-rootvis port. giacomo will need to
finish this up, as my XLib skills are not enough at this time.
| author | nenolod |
|---|---|
| date | Mon, 26 Mar 2007 01:19:26 -0700 |
| parents | |
| children | 5aaf6c282617 |
comparison
equal
deleted
inserted
replaced
| 899:68508f8cdf25 | 900:d985f0dcdeb0 |
|---|---|
| 1 #include <string.h> | |
| 2 #include <math.h> | |
| 3 #include <pthread.h> | |
| 4 #include <time.h> | |
| 5 | |
| 6 #include "rootvis.h" | |
| 7 // as imlib2 uses X definitions, it has to be included after the X includes, which are done in rootvis.h | |
| 8 #include <Imlib2.h> | |
| 9 #include "config.h" | |
| 10 | |
| 11 extern Window ToonGetRootWindow(Display*, int, Window*); | |
| 12 | |
| 13 // Forward declarations | |
| 14 static void rootvis_init(void); | |
| 15 static void rootvis_cleanup(void); | |
| 16 static void rootvis_about(void); | |
| 17 static void rootvis_configure(void); | |
| 18 static void rootvis_playback_start(void); | |
| 19 static void rootvis_playback_stop(void); | |
| 20 static void rootvis_render_freq(gint16 freq_data[2][256]); | |
| 21 | |
| 22 // Callback functions | |
| 23 VisPlugin rootvis_vtable = { | |
| 24 0, // Handle, filled in by xmms | |
| 25 0, // Filename, filled in by xmms | |
| 26 | |
| 27 0, // Session ID | |
| 28 "Root Spectrum Analyzer 0.0.8", // description | |
| 29 | |
| 30 0, // # of PCM channels for render_pcm() | |
| 31 2, // # of freq channels wanted for render_freq() | |
| 32 | |
| 33 rootvis_init, // Called when plugin is enabled | |
| 34 rootvis_cleanup, // Called when plugin is disabled | |
| 35 NULL,//rootvis_about, // Show the about box | |
| 36 rootvis_configure, // Show the configure box | |
| 37 0, // Called to disable plugin, filled in by xmms | |
| 38 rootvis_playback_start, // Called when playback starts | |
| 39 rootvis_playback_stop, // Called when playback stops | |
| 40 0, // Render the PCM data, must return quickly | |
| 41 rootvis_render_freq // Render the freq data, must return quickly | |
| 42 }; | |
| 43 | |
| 44 // XMMS entry point | |
| 45 VisPlugin *get_vplugin_info(void) { | |
| 46 return &rootvis_vtable; | |
| 47 } | |
| 48 | |
| 49 // X related | |
| 50 struct rootvis_x { | |
| 51 int screen; | |
| 52 Display *display; | |
| 53 Window rootWin, Parent; | |
| 54 Pixmap rootBg; | |
| 55 GC gc; | |
| 56 | |
| 57 Visual *vis; | |
| 58 Colormap cm; | |
| 59 Imlib_Image background; | |
| 60 Imlib_Image buffer; | |
| 61 }; | |
| 62 | |
| 63 // thread talk | |
| 64 | |
| 65 struct rootvis_threads { | |
| 66 gint16 freq_data[2][256]; | |
| 67 pthread_t worker[2]; | |
| 68 pthread_mutex_t mutex1; | |
| 69 enum {GO, STOP} control; | |
| 70 char dirty; | |
| 71 /*** dirty flaglist *** | |
| 72 1: channel 1 geometry change | |
| 73 2: channel 1 color change | |
| 74 4: channel 2 geometry change | |
| 75 8: channel 2 color change | |
| 76 16: no data yet (don't do anything) | |
| 77 32: switch mono/stereo | |
| 78 */ | |
| 79 } threads; | |
| 80 | |
| 81 // For use in config_backend: | |
| 82 | |
| 83 void threads_lock(void) { | |
| 84 print_status("Locking"); | |
| 85 pthread_mutex_lock(&threads.mutex1); | |
| 86 } | |
| 87 | |
| 88 void threads_unlock(char dirty) { | |
| 89 print_status("Unlocking"); | |
| 90 threads.dirty = threads.dirty & dirty; | |
| 91 pthread_mutex_unlock(&threads.mutex1); | |
| 92 } | |
| 93 | |
| 94 // Some helper stuff | |
| 95 | |
| 96 void clean_data(void) { | |
| 97 pthread_mutex_lock(&threads.mutex1); | |
| 98 memset(threads.freq_data, 0, sizeof(gint16) * 2 * 256); | |
| 99 pthread_mutex_unlock(&threads.mutex1); | |
| 100 } | |
| 101 | |
| 102 void print_status(char msg[]) { | |
| 103 if (conf.debug == 1) printf(">> rootvis >> %s\n", msg); // for debug purposes, but doesn't tell much anyway | |
| 104 } | |
| 105 | |
| 106 void error_exit(char msg[]) { | |
| 107 printf("*** ERROR (rootvis): %s\n", msg); | |
| 108 rootvis_vtable.disable_plugin(&rootvis_vtable); | |
| 109 } | |
| 110 | |
| 111 void initialize_X(struct rootvis_x* drw, char* display) { | |
| 112 print_status("Opening X Display"); | |
| 113 drw->display = XOpenDisplay(display); | |
| 114 if (drw->display == NULL) { | |
| 115 fprintf(stderr, "cannot connect to X server %s\n", | |
| 116 getenv("DISPLAY") ? getenv("DISPLAY") : "(default)"); | |
| 117 error_exit("Connecting to X server failed"); | |
| 118 pthread_exit(NULL); | |
| 119 } | |
| 120 print_status("Getting screen and window"); | |
| 121 drw->screen = DefaultScreen(drw->display); | |
| 122 drw->rootWin = ToonGetRootWindow(drw->display, drw->screen, &drw->Parent); | |
| 123 | |
| 124 print_status("Initializing Imlib2"); | |
| 125 | |
| 126 drw->vis = DefaultVisual(drw->display, drw->screen); | |
| 127 drw->cm = DefaultColormap(drw->display, drw->screen); | |
| 128 | |
| 129 imlib_context_set_display(drw->display); | |
| 130 imlib_context_set_visual(drw->vis); | |
| 131 imlib_context_set_colormap(drw->cm); | |
| 132 | |
| 133 imlib_context_set_dither(0); | |
| 134 imlib_context_set_blend(1); | |
| 135 } | |
| 136 | |
| 137 void draw_init(struct rootvis_x* drw, unsigned short damage_coords[4]) | |
| 138 { | |
| 139 Atom tmp_rootmapid, tmp_type; | |
| 140 int tmp_format; | |
| 141 unsigned long tmp_length, tmp_after; | |
| 142 unsigned char *data = NULL; | |
| 143 | |
| 144 if ((tmp_rootmapid = XInternAtom(drw->display, "_XROOTPMAP_ID", True)) != None) | |
| 145 { | |
| 146 int ret = XGetWindowProperty(drw->display, drw->rootWin, tmp_rootmapid, 0L, 1L, False, AnyPropertyType, | |
| 147 &tmp_type, &tmp_format, &tmp_length, &tmp_after,&data); | |
| 148 if ((ret == Success)&&(tmp_type == XA_PIXMAP)&&((drw->rootBg = *((Pixmap *)data)) != None)) { | |
| 149 pthread_mutex_lock(&threads.mutex1); | |
| 150 imlib_context_set_drawable(drw->rootBg); | |
| 151 drw->background = imlib_create_image_from_drawable(0, damage_coords[0], damage_coords[1], damage_coords[2], damage_coords[3], 1); | |
| 152 pthread_mutex_unlock(&threads.mutex1); | |
| 153 } | |
| 154 if (drw->background == NULL) | |
| 155 error_exit("Initial image could not be created"); | |
| 156 } | |
| 157 } | |
| 158 | |
| 159 void draw_close(struct rootvis_x* drw, unsigned short damage_coords[4]) { | |
| 160 pthread_mutex_lock(&threads.mutex1); | |
| 161 imlib_context_set_image(drw->background); | |
| 162 imlib_render_image_on_drawable(damage_coords[0], damage_coords[1]); | |
| 163 XClearArea(drw->display, drw->rootWin, damage_coords[0], damage_coords[1], damage_coords[2], damage_coords[3], True); | |
| 164 imlib_free_image(); | |
| 165 pthread_mutex_unlock(&threads.mutex1); | |
| 166 } | |
| 167 | |
| 168 void draw_start(struct rootvis_x* drw, unsigned short damage_coords[4]) { | |
| 169 imlib_context_set_image(drw->background); | |
| 170 drw->buffer = imlib_clone_image(); | |
| 171 imlib_context_set_image(drw->buffer); | |
| 172 } | |
| 173 | |
| 174 void draw_end(struct rootvis_x* drw, unsigned short damage_coords[4]) { | |
| 175 imlib_context_set_drawable(drw->rootWin); | |
| 176 imlib_render_image_on_drawable(damage_coords[0], damage_coords[1]); | |
| 177 imlib_free_image(); | |
| 178 } | |
| 179 | |
| 180 void draw_bar(struct rootvis_x* drw, int t, int i, unsigned short level, unsigned short oldlevel, unsigned short peak, unsigned short oldpeak) { | |
| 181 | |
| 182 /* to make following cleaner, we work with redundant helper variables | |
| 183 this also avoids some calculations */ | |
| 184 register int a, b, c, d; | |
| 185 float angle; | |
| 186 Imlib_Color_Range range = imlib_create_color_range(); | |
| 187 | |
| 188 if (conf.geo[t].orientation < 2) { | |
| 189 a = i*(conf.bar[t].width + conf.bar[t].shadow + conf.geo[t].space); | |
| 190 c = conf.bar[t].width; | |
| 191 b = d = 0; | |
| 192 } else { | |
| 193 b = (conf.data[t].cutoff/conf.data[t].div - i - 1) | |
| 194 *(conf.bar[t].width + conf.bar[t].shadow + conf.geo[t].space); | |
| 195 d = conf.bar[t].width; | |
| 196 a = c = 0; | |
| 197 } | |
| 198 | |
| 199 if (conf.geo[t].orientation == 0) { b = conf.geo[t].height - level; d = level; } | |
| 200 else if (conf.geo[t].orientation == 1) { b = 0; d = level; } | |
| 201 else if (conf.geo[t].orientation == 2) { a = 0; c = level; } | |
| 202 else { a = conf.geo[t].height - level; c = level; } | |
| 203 | |
| 204 if (conf.bar[t].shadow > 0) { | |
| 205 imlib_context_set_color(conf.bar[t].shadow_color[0], conf.bar[t].shadow_color[1], | |
| 206 conf.bar[t].shadow_color[2], conf.bar[t].shadow_color[3]); | |
| 207 if (conf.bar[t].gradient) | |
| 208 imlib_image_fill_rectangle(a + conf.bar[t].shadow, b + conf.bar[t].shadow, c, d); | |
| 209 else if (conf.bar[t].bevel) | |
| 210 imlib_image_draw_rectangle(a + conf.bar[t].shadow, b + conf.bar[t].shadow, c, d); | |
| 211 | |
| 212 if (conf.peak[t].shadow > 0) | |
| 213 { | |
| 214 int aa = a, bb = b, cc = c, dd = d; | |
| 215 if (conf.geo[t].orientation == 0) { bb = conf.geo[t].height - peak; dd = 1; } | |
| 216 else if (conf.geo[t].orientation == 1) { bb = peak - 1; dd = 1; } | |
| 217 else if (conf.geo[t].orientation == 2) { aa = peak - 1; cc = 1; } | |
| 218 else { aa = conf.geo[t].height - peak; cc = 1; } | |
| 219 imlib_image_fill_rectangle(aa + conf.bar[t].shadow, bb + conf.bar[t].shadow, cc, dd); | |
| 220 } | |
| 221 } | |
| 222 | |
| 223 if (conf.bar[t].gradient) | |
| 224 { | |
| 225 switch (conf.geo[t].orientation) { | |
| 226 case 0: angle = 0.0; break; | |
| 227 case 1: angle = 180.0; break; | |
| 228 case 2: angle = 90.0; break; | |
| 229 case 3: default: | |
| 230 angle = -90.0; | |
| 231 } | |
| 232 | |
| 233 imlib_context_set_color_range(range); | |
| 234 imlib_context_set_color(conf.bar[t].color[3][0], conf.bar[t].color[3][1], conf.bar[t].color[3][2], conf.bar[t].color[3][3]); | |
| 235 imlib_add_color_to_color_range(0); | |
| 236 imlib_context_set_color(conf.bar[t].color[2][0], conf.bar[t].color[2][1], conf.bar[t].color[2][2], conf.bar[t].color[2][3]); | |
| 237 imlib_add_color_to_color_range(level * 2 / 5); | |
| 238 imlib_context_set_color(conf.bar[t].color[1][0], conf.bar[t].color[1][1], conf.bar[t].color[1][2], conf.bar[t].color[1][3]); | |
| 239 imlib_add_color_to_color_range(level * 4 / 5); | |
| 240 imlib_context_set_color(conf.bar[t].color[0][0], conf.bar[t].color[0][1], conf.bar[t].color[0][2], conf.bar[t].color[0][3]); | |
| 241 imlib_add_color_to_color_range(level); | |
| 242 imlib_image_fill_color_range_rectangle(a, b, c, d, angle); | |
| 243 imlib_free_color_range(); | |
| 244 } | |
| 245 | |
| 246 if (conf.bar[t].bevel) | |
| 247 { | |
| 248 imlib_context_set_color(conf.bar[t].bevel_color[0], conf.bar[t].bevel_color[1], | |
| 249 conf.bar[t].bevel_color[2], conf.bar[t].bevel_color[3]); | |
| 250 imlib_image_draw_rectangle(a, b, c, d); | |
| 251 } | |
| 252 | |
| 253 if (peak > 0) { | |
| 254 if (conf.geo[t].orientation == 0) { b = conf.geo[t].height - peak; d = 1; } | |
| 255 else if (conf.geo[t].orientation == 1) { b = peak - 1; d = 1; } | |
| 256 else if (conf.geo[t].orientation == 2) { a = peak - 1; c = 1; } | |
| 257 else { a = conf.geo[t].height - peak; c = 1; } | |
| 258 imlib_context_set_color(conf.peak[t].color[0], conf.peak[t].color[1], conf.peak[t].color[2], conf.peak[t].color[3]); | |
| 259 imlib_image_fill_rectangle(a, b, c, d); | |
| 260 } | |
| 261 } | |
| 262 | |
| 263 // Our worker thread | |
| 264 | |
| 265 void* worker_func(void* threadnump) { | |
| 266 struct rootvis_x draw; | |
| 267 gint16 freq_data[256]; | |
| 268 double scale = 0.0, x00 = 0.0, y00 = 0.0; | |
| 269 unsigned int threadnum, i, j, level; | |
| 270 unsigned short damage_coords[4]; | |
| 271 unsigned short *level1 = NULL, *level2 = NULL, *levelsw, *peak1 = NULL, *peak2 = NULL, *peakstep; | |
| 272 int barcount = 0; | |
| 273 | |
| 274 if (threadnump == NULL) threadnum = 0; else threadnum = 1; | |
| 275 | |
| 276 print_status("Memory allocations"); | |
| 277 level1 = (unsigned short*)calloc(256, sizeof(short)); // need to be zeroed out | |
| 278 level2 = (unsigned short*)malloc(256*sizeof(short)); | |
| 279 peak1 = (unsigned short*)calloc(256, sizeof(short)); // need to be zeroed out | |
| 280 peak2 = (unsigned short*)calloc(256, sizeof(short)); // need to be zeroed out for disabled peaks | |
| 281 peakstep = (unsigned short*)calloc(256, sizeof(short)); // need to be zeroed out | |
| 282 if ((level1 == NULL)||(level2 == NULL)||(peak1 == NULL)||(peak2 == NULL)||(peakstep == NULL)) { | |
| 283 error_exit("Allocation of memory failed"); | |
| 284 pthread_exit(NULL); | |
| 285 } | |
| 286 print_status("Allocations done"); | |
| 287 | |
| 288 draw.display = NULL; | |
| 289 | |
| 290 while (threads.control != STOP) { | |
| 291 | |
| 292 { | |
| 293 //print_status("start sleep"); | |
| 294 struct timespec sleeptime; | |
| 295 sleeptime.tv_sec = 0; | |
| 296 sleeptime.tv_nsec = 999999999 / conf.data[threadnum].fps; | |
| 297 while (nanosleep(&sleeptime, &sleeptime) == -1) {}; //print_status("INTR"); | |
| 298 //print_status("end sleep"); | |
| 299 } | |
| 300 | |
| 301 /* we will unset our own dirty flags after receiving them */ | |
| 302 pthread_mutex_lock(&threads.mutex1); | |
| 303 memcpy(&freq_data, &threads.freq_data[threadnum], sizeof(gint16)*256); | |
| 304 i = threads.dirty; | |
| 305 if ((i & 16) == 0) threads.dirty = i & (~(3 + threadnum*9)); | |
| 306 pthread_mutex_unlock(&threads.mutex1); | |
| 307 | |
| 308 if ((i & 16) == 0) { // we've gotten data | |
| 309 if (draw.display == NULL) initialize_X(&draw, conf.geo[threadnum].display); | |
| 310 else if (i & (1 + threadnum*3)) draw_close(&draw, damage_coords); | |
| 311 | |
| 312 if (i & (1 + threadnum*3)) { // geometry has changed | |
| 313 damage_coords[0] = conf.geo[threadnum].posx; | |
| 314 damage_coords[1] = conf.geo[threadnum].posy; | |
| 315 if (conf.geo[threadnum].orientation < 2) { | |
| 316 damage_coords[2] = conf.data[threadnum].cutoff/conf.data[threadnum].div | |
| 317 *(conf.bar[threadnum].width + conf.bar[threadnum].shadow + conf.geo[threadnum].space); | |
| 318 damage_coords[3] = conf.geo[threadnum].height + conf.bar[threadnum].shadow; | |
| 319 } else { | |
| 320 damage_coords[2] = conf.geo[threadnum].height + conf.bar[threadnum].shadow; | |
| 321 damage_coords[3] = conf.data[threadnum].cutoff/conf.data[threadnum].div | |
| 322 *(conf.bar[threadnum].width + conf.bar[threadnum].shadow + conf.geo[threadnum].space); | |
| 323 } | |
| 324 print_status("Geometry recalculations"); | |
| 325 scale = conf.geo[threadnum].height / | |
| 326 (log((1 - conf.data[threadnum].linearity) / conf.data[threadnum].linearity) * 4); | |
| 327 x00 = conf.data[threadnum].linearity*conf.data[threadnum].linearity*32768.0 / | |
| 328 (2*conf.data[threadnum].linearity - 1); | |
| 329 y00 = -log(-x00) * scale; | |
| 330 barcount = conf.data[threadnum].cutoff/conf.data[threadnum].div; | |
| 331 memset(level1, 0, 256*sizeof(short)); | |
| 332 memset(peak1, 0, 256*sizeof(short)); | |
| 333 memset(peak2, 0, 256*sizeof(short)); | |
| 334 | |
| 335 draw_init(&draw, damage_coords); | |
| 336 } | |
| 337 /*if (i & (2 + threadnum*6)) { // colors have changed | |
| 338 }*/ | |
| 339 | |
| 340 /* instead of copying the old level array to the second array, | |
| 341 we just tell the first is now the second one */ | |
| 342 levelsw = level1; | |
| 343 level1 = level2; | |
| 344 level2 = levelsw; | |
| 345 levelsw = peak1; | |
| 346 peak1 = peak2; | |
| 347 peak2 = levelsw; | |
| 348 | |
| 349 for (i = 0; i < barcount; i++) { | |
| 350 level = 0; | |
| 351 for (j = i*conf.data[threadnum].div; j < (i+1)*conf.data[threadnum].div; j++) | |
| 352 if (level < freq_data[j]) | |
| 353 level = freq_data[j]; | |
| 354 level = level * (i*conf.data[threadnum].div + 1); | |
| 355 level = floor(abs(log(level - x00)*scale + y00)); | |
| 356 if (level < conf.geo[threadnum].height) { | |
| 357 if ((level2[i] > conf.bar[threadnum].falloff)&&(level < level2[i] - conf.bar[threadnum].falloff)) | |
| 358 level1[i] = level2[i] - conf.bar[threadnum].falloff; | |
| 359 else level1[i] = level; | |
| 360 } else level1[i] = conf.geo[threadnum].height; | |
| 361 if (conf.peak[threadnum].enabled) { | |
| 362 if (level1[i] > peak2[i] - conf.peak[threadnum].falloff) { | |
| 363 peak1[i] = level1[i]; | |
| 364 peakstep[i] = 0; | |
| 365 } else if (peakstep[i] == conf.peak[threadnum].step) | |
| 366 if (peak2[i] > conf.peak[threadnum].falloff) | |
| 367 peak1[i] = peak2[i] - conf.peak[threadnum].falloff; | |
| 368 else peak1[i] = 0; | |
| 369 else { | |
| 370 peak1[i] = peak2[i]; | |
| 371 peakstep[i]++; | |
| 372 } | |
| 373 } | |
| 374 } | |
| 375 | |
| 376 pthread_mutex_lock(&threads.mutex1); | |
| 377 draw_start(&draw, damage_coords); | |
| 378 for (i = 0; i < barcount; i++) | |
| 379 draw_bar(&draw, threadnum, i, level1[i], level2[i], peak1[i], peak2[i]); | |
| 380 draw_end(&draw, damage_coords); | |
| 381 pthread_mutex_unlock(&threads.mutex1); | |
| 382 } | |
| 383 } | |
| 384 print_status("Worker thread: Exiting"); | |
| 385 if (draw.display != NULL) { | |
| 386 draw_close(&draw, damage_coords); | |
| 387 XCloseDisplay(draw.display); | |
| 388 } | |
| 389 free(level1); free(level2); free(peak1); free(peak2); free(peakstep); | |
| 390 return NULL; | |
| 391 } | |
| 392 | |
| 393 | |
| 394 // da xmms functions | |
| 395 | |
| 396 static void rootvis_init(void) { | |
| 397 int rc1; | |
| 398 print_status("Initializing"); | |
| 399 pthread_mutex_init(&threads.mutex1, NULL); | |
| 400 threads.control = GO; | |
| 401 clean_data(); | |
| 402 config_init(); | |
| 403 threads.dirty = 31; // this means simply everything has changed and there was no data | |
| 404 if ((rc1 = pthread_create(&threads.worker[0], NULL, worker_func, NULL))) { | |
| 405 fprintf(stderr, "Thread creation failed: %d\n", rc1); | |
| 406 error_exit("Thread creation failed"); | |
| 407 } | |
| 408 if ((conf.stereo)&&(rc1 = pthread_create(&threads.worker[1], NULL, worker_func, &rc1))) { | |
| 409 fprintf(stderr, "Thread creation failed: %d\n", rc1); | |
| 410 error_exit("Thread creation failed"); | |
| 411 } | |
| 412 } | |
| 413 | |
| 414 static void rootvis_cleanup(void) { | |
| 415 print_status("Cleanup... "); | |
| 416 threads.control = STOP; | |
| 417 pthread_join(threads.worker[0], NULL); | |
| 418 if (conf.stereo) pthread_join(threads.worker[1], NULL); | |
| 419 print_status("Clean Exit"); | |
| 420 } | |
| 421 | |
| 422 static void rootvis_about(void) | |
| 423 { | |
| 424 print_status("About"); | |
| 425 } | |
| 426 | |
| 427 static void rootvis_configure(void) | |
| 428 { | |
| 429 print_status("Configuration trigger"); | |
| 430 config_init(); | |
| 431 config_show(2); | |
| 432 } | |
| 433 | |
| 434 static void rootvis_playback_start(void) | |
| 435 { | |
| 436 print_status("Playback starting"); | |
| 437 } | |
| 438 | |
| 439 static void rootvis_playback_stop(void) | |
| 440 { | |
| 441 clean_data(); | |
| 442 } | |
| 443 | |
| 444 static void rootvis_render_freq(gint16 freq_data[2][256]) { | |
| 445 int channel, bucket; | |
| 446 pthread_mutex_lock(&threads.mutex1); | |
| 447 threads.dirty = threads.dirty & (~(16)); // unset no data yet flag | |
| 448 for (channel = 0; channel < 2; channel++) { | |
| 449 for (bucket = 0; bucket < 256; bucket++) { | |
| 450 if (conf.stereo) threads.freq_data[channel][bucket] = freq_data[channel][bucket]; | |
| 451 else if (channel == 0) threads.freq_data[0][bucket] = freq_data[channel][bucket] / 2; | |
| 452 else threads.freq_data[0][bucket] += freq_data[channel][bucket] / 2; | |
| 453 } | |
| 454 } | |
| 455 pthread_mutex_unlock(&threads.mutex1); | |
| 456 } |
