1 //
2 // Mixer.app
3 //
4 // Copyright (c) 1998-2002 Per Liden
5 //
6 // This program is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software
18 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
19 // USA.
20 //
21
22 #include <X11/Xlib.h>
23 #include <sys/ioctl.h>
24 #include <iostream>
25 #include <fstream>
26 #include <cstdlib>
27 #include <cstring>
28 #include <csignal>
29 #include <fcntl.h>
30 #include <unistd.h>
31 #include "Xpm.h"
32 #include "Mixer.h"
33
34 #if defined(__Linux__)
35 #include <linux/soundcard.h>
36 #elif defined(__OpenBSD__)
37 #include <soundcard.h>
38 #else
39 #include <sys/soundcard.h>
40 #endif
41
42 #include "pixmaps/main.xpm"
43 #include "pixmaps/button.xpm"
44 #include "pixmaps/mutebutton.xpm"
45 #include "pixmaps/redlight.xpm"
46
47 using namespace std;
48
49 static const int ButtonX[] = {6, 24, 42};
50 static char* MixerDevices[] = { MIXERDEVICES };
51 static const char* SourceNames[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_NAMES;
52
53 extern Mixer* app;
54
catchBrokenPipe(int sig)55 void catchBrokenPipe(int sig)
56 {
57 app->saveVolumeSettings();
58 exit(0);
59 }
60
Mixer(int argc,char ** argv)61 Mixer::Mixer(int argc, char** argv)
62 {
63 XClassHint classHint;
64 XSizeHints sizeHints;
65 XWMHints wmHints;
66 Atom deleteWindow;
67 Xpm* image;
68 char* displayName = NULL;
69
70 mError = 0;
71 mInstanceName = INSTANCENAME;
72 mVolumeSource[0] = -1;
73 mVolumeSource[1] = -1;
74 mVolumeSource[2] = -1;
75 mVolumeMute[0] = 0;
76 mVolumeMute[1] = 0;
77 mVolumeMute[2] = 0;
78 mWheelButton = 1;
79 mLabelText = 0;
80 mSettingsFile = 0;
81
82 findMixerDevice();
83
84 // Parse command line
85 if (argc>1) {
86 for (int i=1; i<argc; i++) {
87 // Display
88 if (!strcmp(argv[i], "-d")) {
89 checkArgument(argv, argc, i);
90 displayName = argv[i+1];
91 i++;
92 }
93
94 // Sound source
95 else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "-2") || !strcmp(argv[i], "-3")) {
96 checkArgument(argv, argc, i);
97 for (int j=0; j<SOUND_MIXER_NRDEVICES; j++) {
98 if (!strcasecmp(SourceNames[j], argv[i+1])) {
99 mVolumeSource[argv[i][1]-'1'] = j;
100 }
101 }
102
103 if (mVolumeSource[argv[i][1]-'1'] == -1) {
104 cerr << APPNAME << ": invalid sound source " << argv[i+1] << endl;
105 tryHelp(argv[0]);
106 exit(0);
107 }
108 i++;
109 }
110
111 // Wheel binding
112 else if (!strcmp(argv[i], "-w")) {
113 checkArgument(argv, argc, i);
114 mWheelButton = atoi(argv[i+1]);
115
116 if (mWheelButton < 1 || mWheelButton > 3) {
117 cerr << APPNAME << ": invalid wheel binding, must be 1, 2 or 3, not " << argv[i+1] << endl;
118 tryHelp(argv[0]);
119 exit(0);
120 }
121
122 i++;
123 }
124
125 // Label text
126 else if (!strcmp(argv[i], "-l")) {
127 checkArgument(argv, argc, i);
128 mLabelText = argv[i+1];
129 i++;
130 }
131
132 // Load/Save settings (default file)
133 else if (!strcmp(argv[i], "-s")) {
134 char* home = getenv("HOME");
135 if (home) {
136 mSettingsFile = new char[strlen(home) + strlen(SETTINGS) + 1];
137 strcpy(mSettingsFile, home);
138 strcat(mSettingsFile, SETTINGS);
139 } else {
140 cerr << APPNAME << ": $HOME not set, could not find saved settings" << endl;
141 }
142 }
143
144 // Load/Save settings
145 else if (!strcmp(argv[i], "-S")) {
146 checkArgument(argv, argc, i);
147 mSettingsFile = argv[i+1];
148 i++;
149 }
150
151 // Mixer deice
152 else if (!strcmp(argv[i], "-m")) {
153 checkArgument(argv, argc, i);
154 mMixerDevice = argv[i+1];
155 i++;
156 }
157
158 // Instance name
159 else if (!strcmp(argv[i], "-n")) {
160 checkArgument(argv, argc, i);
161 mInstanceName = argv[i+1];
162 i++;
163 }
164
165 // Version
166 else if (!strcmp(argv[i], "-v")) {
167 cerr << APPNAME << " version " << VERSION << endl;
168 exit(0);
169 }
170
171 // Help
172 else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
173 showHelp();
174 exit(0);
175 }
176
177 // Unknown option
178 else {
179 cerr << APPNAME << ": invalid option '" << argv[i] << "'" << endl;
180 tryHelp(argv[0]);
181 exit(0);
182 }
183 }
184 }
185
186 // Set default if not specified in cmd-line
187 if (mVolumeSource[0] == -1) {
188 mVolumeSource[0] = SOUND_MIXER_VOLUME;
189 }
190
191 if (mVolumeSource[1] == -1) {
192 mVolumeSource[1] = SOUND_MIXER_CD;
193 }
194
195 if (mVolumeSource[2] == -1) {
196 mVolumeSource[2] = SOUND_MIXER_PCM;
197 }
198
199 // Open display
200 if ((mDisplay = XOpenDisplay(displayName)) == NULL) {
201 cerr << APPNAME << ": could not open display " << displayName << endl;
202 exit(0);
203 }
204
205 // Get root window
206 mRoot = RootWindow(mDisplay, DefaultScreen(mDisplay));
207
208 // Create windows
209 mAppWin = XCreateSimpleWindow(mDisplay, mRoot, 1, 1, 64, 64, 0, 0, 0);
210 mIconWin = XCreateSimpleWindow(mDisplay, mAppWin, 0, 0, 64, 64, 0, 0, 0);
211
212 // Set classhint
213 classHint.res_name = mInstanceName;
214 classHint.res_class = CLASSNAME;
215 XSetClassHint(mDisplay, mAppWin, &classHint);
216
217 // Create delete atom
218 deleteWindow = XInternAtom(mDisplay, "WM_DELETE_WINDOW", False);
219 XSetWMProtocols(mDisplay, mAppWin, &deleteWindow, 1);
220 XSetWMProtocols(mDisplay, mIconWin, &deleteWindow, 1);
221
222 // Set windowname
223 XStoreName(mDisplay, mAppWin, APPNAME);
224 XSetIconName(mDisplay, mAppWin, APPNAME);
225
226 // Set sizehints
227 sizeHints.flags= USPosition;
228 sizeHints.x = 0;
229 sizeHints.y = 0;
230 XSetWMNormalHints(mDisplay, mAppWin, &sizeHints);
231
232 // Set wmhints
233 wmHints.initial_state = WithdrawnState;
234 wmHints.icon_window = mIconWin;
235 wmHints.icon_x = 0;
236 wmHints.icon_y = 0;
237 wmHints.window_group = mAppWin;
238 wmHints.flags = StateHint | IconWindowHint | IconPositionHint | WindowGroupHint;
239 XSetWMHints(mDisplay, mAppWin, &wmHints);
240
241 // Set command
242 XSetCommand(mDisplay, mAppWin, argv, argc);
243
244 // Set background image
245 image = new Xpm(mDisplay, mRoot, main_xpm);
246 if (mLabelText) {
247 image->drawString(LABEL_X, LABEL_Y, mLabelText);
248 }
249 image->setWindowPixmapShaped(mIconWin);
250 delete image;
251
252 // Create buttons
253 mButton[0] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[0], BUTTON_MIN, 5, 5, 0, 0, 0);
254 mButton[1] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[1], BUTTON_MIN, 5, 5, 0, 0, 0);
255 mButton[2] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[2], BUTTON_MIN, 5, 5, 0, 0, 0);
256
257 image = new Xpm(mDisplay, mRoot, button_xpm);
258 image->setWindowPixmap(mButton[0]);
259 image->setWindowPixmap(mButton[1]);
260 image->setWindowPixmap(mButton[2]);
261 delete image;
262
263 XSelectInput(mDisplay, mButton[0], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
264 XSelectInput(mDisplay, mButton[1], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
265 XSelectInput(mDisplay, mButton[2], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
266 XSelectInput(mDisplay, mIconWin, ButtonPressMask);
267
268 XMapWindow(mDisplay, mButton[0]);
269 XMapWindow(mDisplay, mButton[1]);
270 XMapWindow(mDisplay, mButton[2]);
271
272 XMapWindow(mDisplay, mIconWin);
273 XMapWindow(mDisplay, mAppWin);
274 XSync(mDisplay, False);
275
276 // Catch broker pipe signal
277 signal(SIGPIPE, catchBrokenPipe);
278
279 // Check if error
280 if (mError) {
281 showErrorLed();
282 } else {
283 getVolume();
284 loadVolumeSettings();
285 }
286 }
287
tryHelp(char * appname)288 void Mixer::tryHelp(char* appname)
289 {
290 cerr << "Try `" << appname << " --help' for more information" << endl;
291 }
292
showHelp()293 void Mixer::showHelp()
294 {
295 cerr << APPNAME << " Copyright (c) 1998-2002 by Per Liden (per@fukt.bth.se)" << endl << endl
296 << "options:" << endl
297 << " -1 <source> set sound source for control 1 (default is vol)" << endl
298 << " -2 <source> set sound source for control 2 (default is cd)" << endl
299 << " -3 <source> set sound source for control 3 (default is pcm)" << endl
300 << " -w 1|2|3 bind a control button to the mouse wheel (default is 1)" << endl
301 << " -l <text> set label text" << endl
302 << " -s load/save volume settings using ~/GNUstep/Defaults/Mixer" << endl
303 << " -S <file> load/save volume settings using <file>" << endl
304 << " -m <device> set mixer device" << endl
305 << " -n <name> set client instance name" << endl
306 << " -d <disp> set display" << endl
307 << " -v print version and exit" << endl
308 << " -h, --help display this help and exit" << endl << endl
309 << "sound sources:" << endl;
310 for (int i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
311 cerr << " " << SourceNames[i];
312 if (i > 0 && i % 10 == 0)
313 cerr << "," << endl;
314 else if (i == SOUND_MIXER_NRDEVICES - 1)
315 cerr << "." << endl;
316 else
317 cerr << ",";
318 }
319 }
320
checkArgument(char ** argv,int argc,int index)321 void Mixer::checkArgument(char** argv, int argc, int index)
322 {
323 if (argc-1 < index+1) {
324 cerr << APPNAME << ": option '" << argv[index] << "' requires an argument" << endl;
325 tryHelp(argv[0]);
326 exit(0);
327 }
328 }
329
showErrorLed()330 void Mixer::showErrorLed()
331 {
332 Window led;
333 Xpm* image;
334
335 led = XCreateSimpleWindow(mDisplay, mIconWin, LED_X, LED_Y, 3, 2, 0, 0, 0);
336
337 // Set background image
338 image = new Xpm(mDisplay, mRoot, redlight_xpm);
339 image->setWindowPixmap(led);
340 delete image;
341
342 // Show window
343 XMapWindow(mDisplay, led);
344 mError = 1;
345 }
346
findMixerDevice()347 void Mixer::findMixerDevice()
348 {
349 for (int i = 0; MixerDevices[i]; i++) {
350 mMixerDevice = MixerDevices[i];
351 int fd = open(mMixerDevice, 0);
352 if (fd != -1) {
353 close(fd);
354 return;
355 }
356 }
357
358 cerr << APPNAME << ": unable to open mixer device, tried the following: ";
359 for (int i = 0; MixerDevices[i]; i++)
360 cerr << MixerDevices[i] << " ";
361 cerr << endl;
362 }
363
loadVolumeSettings()364 void Mixer::loadVolumeSettings()
365 {
366 if (mSettingsFile) {
367 ifstream file(mSettingsFile);
368 if (file) {
369 // This could fail if the user has edited the file by hand and destroyed the structure
370 char dummy[1024];
371 file >> dummy; // {
372 file >> dummy; // Volume1
373 file >> dummy; // =
374 file >> mVolumePos[0];
375 file >> dummy; // ;
376
377 file >> dummy; // Volume2
378 file >> dummy; // =
379 file >> mVolumePos[1];
380 file >> dummy; // ;
381
382 file >> dummy; // Volume3
383 file >> dummy; // =
384 file >> mVolumePos[2];
385
386 file.close();
387 setVolume(0, mVolumePos[0]);
388 setVolume(1, mVolumePos[1]);
389 setVolume(2, mVolumePos[2]);
390 }
391 }
392 }
393
saveVolumeSettings()394 void Mixer::saveVolumeSettings()
395 {
396 if (mSettingsFile) {
397 ofstream file(mSettingsFile);
398 if (file) {
399 // Files in ~/GNUstep/Defaults/ should follow the property list format
400 file << "{" << endl
401 << " Volume1 = " << mVolumePos[0] << ";" << endl
402 << " Volume2 = " << mVolumePos[1] << ";" << endl
403 << " Volume3 = " << mVolumePos[2] << ";" << endl
404 << "}" << endl;
405 file.close();
406 } else {
407 cerr << APPNAME << ": failed to save volume settings in " << mSettingsFile << endl;
408 }
409 }
410 }
411
getVolume()412 void Mixer::getVolume()
413 {
414 static int lastVolume[3] = {1, 1, 1};
415 int fd;
416
417 if (mError) {
418 return;
419 }
420
421 // Open device
422 fd = open(mMixerDevice, 0);
423 if (fd < 0) {
424 showErrorLed();
425 return;
426 }
427
428 // Read from device
429 for (int i=0; i<3; i++) {
430 if (ioctl(fd, MIXER_READ(mVolumeSource[i]), &mVolume[i]) < 0) {
431 mError = 1;
432 }
433
434 mVolume[i] = mVolume[i] >> 8;
435
436 if (lastVolume[i] != mVolume[i]) {
437 int y;
438
439 // Set button position
440 if (mError) {
441 y = BUTTON_MIN;
442 } else {
443 y = BUTTON_MIN - (mVolume[i] * (BUTTON_MIN - BUTTON_MAX)) / 100;
444 }
445
446 if (mVolumeMute[i] == 1) {
447 if (y != BUTTON_MIN) {
448 // Release mute if the volume was changed from another program
449 mVolumePos[i] = y; // Reset button position
450 mute(i); // Toggle mute
451 } else {
452 // Skip if muted
453 continue;
454 }
455 }
456
457 mVolumePos[i] = y;
458 XMoveWindow(mDisplay, mButton[i], ButtonX[i], y);
459 XFlush(mDisplay);
460 lastVolume[i] = mVolume[i];
461 }
462 }
463
464 // Close device
465 close(fd);
466
467 if (mError) {
468 cerr << APPNAME << ": unable to read from " << mMixerDevice << endl;
469 showErrorLed();
470 return;
471 }
472 }
473
setVolume(int button,int volume)474 void Mixer::setVolume(int button, int volume)
475 {
476 int fd;
477
478 if (mError) {
479 return;
480 }
481
482 // Open device
483 fd = open(mMixerDevice, 0);
484
485 // Calculate volume
486 mVolume[button] = 100 - (((volume - BUTTON_MAX) * 100) / (BUTTON_MIN - BUTTON_MAX));
487 mVolume[button] |= mVolume[button] << 8;
488
489 // Write to device
490 ioctl(fd, MIXER_WRITE(mVolumeSource[button]), &mVolume[button]);
491
492 // Close devicw
493 close(fd);
494 }
495
mute(int button)496 void Mixer::mute(int button)
497 {
498 Xpm* image;
499
500 if (mVolumeMute[button] == 0) {
501 // Mute
502 mVolumeMute[button] = 1;
503 setVolume(button, BUTTON_MIN);
504
505 image = new Xpm(mDisplay, mRoot, mutebutton_xpm);
506 image->setWindowPixmap(mButton[button]);
507 delete image;
508
509 XClearWindow(mDisplay, mButton[button]);
510 } else {
511 // Restore volume
512 mVolumeMute[button] = 0;
513 setVolume(button, mVolumePos[button]);
514
515 image = new Xpm(mDisplay, mRoot, button_xpm);
516 image->setWindowPixmap(mButton[button]);
517 delete image;
518
519 XClearWindow(mDisplay, mButton[button]);
520 }
521 }
522
setButtonPosition(int button,int relativePosition)523 void Mixer::setButtonPosition(int button, int relativePosition)
524 {
525 int y;
526
527 // Calc new button position
528 y = mVolumePos[button] + relativePosition;
529
530 if (y > BUTTON_MIN) {
531 y = BUTTON_MIN;
532 } else if (y < BUTTON_MAX) {
533 y = BUTTON_MAX;
534 }
535
536 // Set button position and volume
537 XMoveWindow(mDisplay, mButton[button], ButtonX[button], y);
538
539 mVolumePos[button] = y;
540
541 // Don't really set volume if muted
542 if (mVolumeMute[button] == 0) {
543 setVolume(button, y);
544 }
545 }
546
run()547 void Mixer::run()
548 {
549 XEvent event;
550 int buttonDown = 0;
551 int buttonDownPosition = 0;
552
553 // Start handling events
554 while(1) {
555 while(XPending(mDisplay) || buttonDown) {
556 XNextEvent(mDisplay, &event);
557
558 switch(event.type) {
559 case ButtonPress:
560 if (event.xbutton.button == Button4 || event.xbutton.button == Button5) {
561 // Wheel scroll
562 setButtonPosition(mWheelButton - 1, event.xbutton.button == Button5? 3: -3);
563 } else if (event.xbutton.button == Button1 && event.xbutton.window != mIconWin) {
564 // Volume change
565 buttonDown = 1;
566 buttonDownPosition = event.xbutton.y;
567 } else if (event.xbutton.button == Button3 && buttonDown == 0 && event.xbutton.window != mIconWin) {
568 // Mute
569 for (int i=0; i<3; i++) {
570 if (mButton[i] == event.xbutton.window) {
571 mute(i);
572 break;
573 }
574 }
575 }
576 break;
577
578 case ButtonRelease:
579 if (event.xbutton.button == Button1) {
580 buttonDown = 0;
581 }
582 break;
583
584 case MotionNotify:
585 if (buttonDown) {
586 // Find button
587 for (int i=0; i<3; i++) {
588 if (mButton[i] == event.xmotion.window) {
589 setButtonPosition(i, event.xmotion.y - buttonDownPosition);
590 break;
591 }
592 }
593 }
594 break;
595 }
596 }
597
598 // Idle for a moment
599 usleep(50000);
600
601 // Update volume status
602 getVolume();
603 XSync(mDisplay, False);
604 }
605 }
606
607