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