a7279e71be
Signed-off-by: Wade Berrier <wberrier@gmail.com>
599 lines
16 KiB
C++
599 lines
16 KiB
C++
//
|
|
// Mixer.app
|
|
//
|
|
// Copyright (c) 1998-2002 Per Liden
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1307,
|
|
// USA.
|
|
//
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <csignal>
|
|
#include "Xpm.h"
|
|
#include "Mixer.h"
|
|
|
|
#include "AMixer/AMixer.h"
|
|
|
|
#include "pixmaps/main.xpm"
|
|
#include "pixmaps/button.xpm"
|
|
#include "pixmaps/mutebutton.xpm"
|
|
#include "pixmaps/redlight.xpm"
|
|
|
|
#define ROUND_POS(x) (int ((x) + 0.5) > int (x)) ? (int) ((x) + 1) : (int) (x)
|
|
|
|
using namespace std;
|
|
|
|
static const int ButtonX[] = {6, 24, 42};
|
|
static const char* MixerSources[] = { "Master", "PCM", "CD" };
|
|
|
|
extern Mixer* app;
|
|
|
|
void catchBrokenPipe(int sig)
|
|
{
|
|
app->saveVolumeSettings();
|
|
exit(0);
|
|
}
|
|
|
|
int positionToPercent(int position) {
|
|
return ROUND_POS(100 - (((position - BUTTON_MAX) * 100.0) / (BUTTON_MIN - BUTTON_MAX)));
|
|
}
|
|
|
|
int percentToPosition(int percent) {
|
|
return ROUND_POS(BUTTON_MIN - (percent * (BUTTON_MIN - BUTTON_MAX)) / 100.0);
|
|
}
|
|
|
|
Mixer::Mixer(int argc, char** argv)
|
|
{
|
|
XClassHint classHint;
|
|
XSizeHints sizeHints;
|
|
XWMHints wmHints;
|
|
Atom deleteWindow;
|
|
Xpm* image;
|
|
char* displayName = NULL;
|
|
char* card = "default";
|
|
|
|
mError = 0;
|
|
mInstanceName = INSTANCENAME;
|
|
mVolumeSource[0] = -1;
|
|
mVolumeSource[1] = -1;
|
|
mVolumeSource[2] = -1;
|
|
mVolumeMute[0] = 0;
|
|
mVolumeMute[1] = 0;
|
|
mVolumeMute[2] = 0;
|
|
mWheelButton = 1;
|
|
mLabelText = 0;
|
|
mSettingsFile = 0;
|
|
mSaveSettings = false;
|
|
mLoadSettings = false;
|
|
mCommand = NULL;
|
|
|
|
// Parse command line
|
|
if (argc>1) {
|
|
for (int i=1; i<argc; i++) {
|
|
// Display
|
|
if (!strcmp(argv[i], "-d")) {
|
|
checkArgument(argv, argc, i);
|
|
displayName = argv[i+1];
|
|
i++;
|
|
}
|
|
|
|
// Sound source
|
|
else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "-2") || !strcmp(argv[i], "-3")) {
|
|
checkArgument(argv, argc, i);
|
|
MixerSources[argv[i][1] - '1'] = argv[i + 1];
|
|
i++;
|
|
}
|
|
|
|
// Wheel binding
|
|
else if (!strcmp(argv[i], "-w")) {
|
|
checkArgument(argv, argc, i);
|
|
mWheelButton = atoi(argv[i+1]);
|
|
|
|
if (mWheelButton < 1 || mWheelButton > 3) {
|
|
cerr << APPNAME << ": invalid wheel binding, must be 1, 2 or 3, not " << argv[i+1] << endl;
|
|
tryHelp(argv[0]);
|
|
exit(0);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// Label text
|
|
else if (!strcmp(argv[i], "-l")) {
|
|
checkArgument(argv, argc, i);
|
|
mLabelText = argv[i+1];
|
|
i++;
|
|
}
|
|
|
|
// Save settings on exit
|
|
else if (!strcmp(argv[i], "-S")) {
|
|
mSaveSettings = true;
|
|
}
|
|
|
|
// Load settings on startup
|
|
else if (!strcmp(argv[i], "-L")) {
|
|
mLoadSettings = true;
|
|
}
|
|
|
|
// Load/Save settings file
|
|
else if (!strcmp(argv[i], "-f")) {
|
|
checkArgument(argv, argc, i);
|
|
mSettingsFile = argv[i+1];
|
|
i++;
|
|
}
|
|
|
|
// Execute command on middle click
|
|
else if (!strcmp(argv[i], "-e")) {
|
|
checkArgument(argv, argc, i);
|
|
mCommand = argv[i + 1];
|
|
i++;
|
|
}
|
|
|
|
// Instance name
|
|
else if (!strcmp(argv[i], "-n")) {
|
|
checkArgument(argv, argc, i);
|
|
mInstanceName = argv[i+1];
|
|
i++;
|
|
}
|
|
|
|
// Version
|
|
else if (!strcmp(argv[i], "-v")) {
|
|
cerr << APPNAME << " version " << VERSION << endl;
|
|
exit(0);
|
|
}
|
|
|
|
// Help
|
|
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
|
showHelp();
|
|
exit(0);
|
|
}
|
|
|
|
// card
|
|
else if (!strcmp(argv[i], "--card")) {
|
|
card = AMixer::convertIDToCard(argv[i + 1]);
|
|
if (!card) {
|
|
cerr << APPNAME << ": invalid card number '" << argv[i + 1] << "'" << endl;
|
|
tryHelp(argv[0]);
|
|
exit(0);
|
|
}
|
|
i++;
|
|
}
|
|
|
|
// device
|
|
else if (!strcmp(argv[i], "--device")) {
|
|
card = argv[i + 1];
|
|
i++;
|
|
}
|
|
|
|
// Unknown option
|
|
else {
|
|
cerr << APPNAME << ": invalid option '" << argv[i] << "'" << endl;
|
|
tryHelp(argv[0]);
|
|
exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// default settings file
|
|
if (!mSettingsFile) {
|
|
char* home = getenv("HOME");
|
|
if (home) {
|
|
mSettingsFile = new char[strlen(home) + strlen(SETTINGS) + 1];
|
|
strcpy(mSettingsFile, home);
|
|
strcat(mSettingsFile, SETTINGS);
|
|
} else {
|
|
cerr << APPNAME << ": $HOME not set, could not find saved settings" << endl;
|
|
}
|
|
}
|
|
|
|
// init mixer
|
|
aMixer = new AMixer(card);
|
|
if (!aMixer->opened()) {
|
|
cerr << APPNAME << ": could not open mixer device for card '" << card << "'" << endl;
|
|
exit(0);
|
|
}
|
|
|
|
// open mixer sources
|
|
for (int i = 0; i < 3; i++) {
|
|
aMixer->attachItem(i, MixerSources[i]);
|
|
if (!aMixer->itemOK(i))
|
|
cerr << APPNAME << ": could not select mixer source '" << MixerSources[i] << "'" << endl;
|
|
}
|
|
|
|
// Open display
|
|
if ((mDisplay = XOpenDisplay(displayName)) == NULL) {
|
|
cerr << APPNAME << ": could not open display " << displayName << endl;
|
|
exit(0);
|
|
}
|
|
|
|
// Get root window
|
|
mRoot = RootWindow(mDisplay, DefaultScreen(mDisplay));
|
|
|
|
// Create windows
|
|
mAppWin = XCreateSimpleWindow(mDisplay, mRoot, 1, 1, 64, 64, 0, 0, 0);
|
|
mIconWin = XCreateSimpleWindow(mDisplay, mAppWin, 0, 0, 64, 64, 0, 0, 0);
|
|
|
|
// Set classhint
|
|
classHint.res_name = mInstanceName;
|
|
classHint.res_class = CLASSNAME;
|
|
XSetClassHint(mDisplay, mAppWin, &classHint);
|
|
|
|
// Create delete atom
|
|
deleteWindow = XInternAtom(mDisplay, "WM_DELETE_WINDOW", False);
|
|
XSetWMProtocols(mDisplay, mAppWin, &deleteWindow, 1);
|
|
XSetWMProtocols(mDisplay, mIconWin, &deleteWindow, 1);
|
|
|
|
// Set windowname
|
|
XStoreName(mDisplay, mAppWin, APPNAME);
|
|
XSetIconName(mDisplay, mAppWin, APPNAME);
|
|
|
|
// Set sizehints
|
|
sizeHints.flags= USPosition;
|
|
sizeHints.x = 0;
|
|
sizeHints.y = 0;
|
|
XSetWMNormalHints(mDisplay, mAppWin, &sizeHints);
|
|
|
|
// Set wmhints
|
|
wmHints.initial_state = WithdrawnState;
|
|
wmHints.icon_window = mIconWin;
|
|
wmHints.icon_x = 0;
|
|
wmHints.icon_y = 0;
|
|
wmHints.window_group = mAppWin;
|
|
wmHints.flags = StateHint | IconWindowHint | IconPositionHint | WindowGroupHint;
|
|
XSetWMHints(mDisplay, mAppWin, &wmHints);
|
|
|
|
// Set command
|
|
XSetCommand(mDisplay, mAppWin, argv, argc);
|
|
|
|
// Set background image
|
|
image = new Xpm(mDisplay, mRoot, main_xpm);
|
|
if (mLabelText) {
|
|
image->drawString(LABEL_X, LABEL_Y, mLabelText);
|
|
}
|
|
image->setWindowPixmapShaped(mIconWin);
|
|
delete image;
|
|
|
|
// Create buttons
|
|
mButton[0] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[0], BUTTON_MIN, 5, 5, 0, 0, 0);
|
|
mButton[1] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[1], BUTTON_MIN, 5, 5, 0, 0, 0);
|
|
mButton[2] = XCreateSimpleWindow(mDisplay, mIconWin, ButtonX[2], BUTTON_MIN, 5, 5, 0, 0, 0);
|
|
|
|
image = new Xpm(mDisplay, mRoot, button_xpm);
|
|
image->setWindowPixmap(mButton[0]);
|
|
image->setWindowPixmap(mButton[1]);
|
|
image->setWindowPixmap(mButton[2]);
|
|
delete image;
|
|
|
|
XSelectInput(mDisplay, mButton[0], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
|
|
XSelectInput(mDisplay, mButton[1], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
|
|
XSelectInput(mDisplay, mButton[2], ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
|
|
XSelectInput(mDisplay, mIconWin, ButtonPressMask);
|
|
|
|
XMapWindow(mDisplay, mButton[0]);
|
|
XMapWindow(mDisplay, mButton[1]);
|
|
XMapWindow(mDisplay, mButton[2]);
|
|
|
|
XMapWindow(mDisplay, mIconWin);
|
|
XMapWindow(mDisplay, mAppWin);
|
|
XSync(mDisplay, False);
|
|
|
|
// Catch broker pipe signal
|
|
signal(SIGPIPE, catchBrokenPipe);
|
|
|
|
// Check if error
|
|
if (mError) {
|
|
showErrorLed();
|
|
} else {
|
|
getVolume();
|
|
if (mLoadSettings)
|
|
loadVolumeSettings();
|
|
}
|
|
}
|
|
|
|
void Mixer::tryHelp(char* appname)
|
|
{
|
|
cerr << "Try `" << appname << " --help' for more information" << endl;
|
|
}
|
|
|
|
void Mixer::showHelp()
|
|
{
|
|
cerr << APPNAME << " Copyright (c) 1998-2002 by Per Liden (per@fukt.bth.se), Petr Hlavka (xhlavk00@stud.fit.vutbr.cz)" << endl << endl
|
|
<< "options:" << endl
|
|
<< " -1 <source> set sound source for control 1 (default is Master)" << endl
|
|
<< " -2 <source> set sound source for control 2 (default is PCM)" << endl
|
|
<< " -3 <source> set sound source for control 3 (default is CD)" << endl
|
|
<< " -w 1|2|3 bind a control button to the mouse wheel (default is 1)" << endl
|
|
<< " -l <text> set label text" << endl
|
|
<< " -S save volume settings on exit" << endl
|
|
<< " -L load volume settings on start up" << endl
|
|
<< " -f <file> use setting <file> instead of ~/GNUstep/Defaults/AlsaMixer" << endl
|
|
<< " --card <id> select card" << endl
|
|
<< " --device <dev> select device, default 'default'" << endl
|
|
<< " -e <command> execute <command> on middle click" << endl
|
|
<< " -n <name> set client instance name" << endl
|
|
<< " -d <disp> set display" << endl
|
|
<< " -v print version and exit" << endl
|
|
<< " -h, --help display this help and exit" << endl << endl;
|
|
}
|
|
|
|
void Mixer::checkArgument(char** argv, int argc, int index)
|
|
{
|
|
if (argc-1 < index+1) {
|
|
cerr << APPNAME << ": option '" << argv[index] << "' requires an argument" << endl;
|
|
tryHelp(argv[0]);
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
void Mixer::showErrorLed()
|
|
{
|
|
Window led;
|
|
Xpm* image;
|
|
|
|
led = XCreateSimpleWindow(mDisplay, mIconWin, LED_X, LED_Y, 3, 2, 0, 0, 0);
|
|
|
|
// Set background image
|
|
image = new Xpm(mDisplay, mRoot, redlight_xpm);
|
|
image->setWindowPixmap(led);
|
|
delete image;
|
|
|
|
// Show window
|
|
XMapWindow(mDisplay, led);
|
|
mError = 1;
|
|
}
|
|
|
|
void Mixer::loadVolumeSettings()
|
|
{
|
|
if (mSettingsFile) {
|
|
ifstream file(mSettingsFile);
|
|
if (file) {
|
|
// This could fail if the user has edited the file by hand and destroyed the structure
|
|
char dummy[1024];
|
|
file >> dummy; // {
|
|
file >> dummy; // Volume1
|
|
file >> dummy; // =
|
|
file >> mVolume[0];
|
|
file >> dummy; // ;
|
|
|
|
file >> dummy; // Volume2
|
|
file >> dummy; // =
|
|
file >> mVolume[1];
|
|
file >> dummy; // ;
|
|
|
|
file >> dummy; // Volume3
|
|
file >> dummy; // =
|
|
file >> mVolume[2];
|
|
|
|
file.close();
|
|
for (int i = 0; i < 3; i++) {
|
|
setVolume(i, mVolume[i]);
|
|
setButtonPosition(i, percentToPosition(mVolume[i]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mixer::saveVolumeSettings()
|
|
{
|
|
if (mSaveSettings) {
|
|
ofstream file(mSettingsFile);
|
|
if (file) {
|
|
// Files in ~/GNUstep/Defaults/ should follow the property list format
|
|
file << "{" << endl
|
|
<< " Volume1 = " << mVolumePos[0] << ";" << endl
|
|
<< " Volume2 = " << mVolumePos[1] << ";" << endl
|
|
<< " Volume3 = " << mVolumePos[2] << ";" << endl
|
|
<< "}" << endl;
|
|
file.close();
|
|
} else {
|
|
cerr << APPNAME << ": failed to save volume settings in " << mSettingsFile << endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mixer::getVolume()
|
|
{
|
|
static int lastVolume[3] = {-1, -1, -1};
|
|
static int lastVolumeMute[3] = {-1, -1, -1};
|
|
|
|
if (mError) {
|
|
return;
|
|
}
|
|
|
|
// Read from device
|
|
for (int i=0; i<3; i++) {
|
|
mVolume[i] = aMixer->itemGetVolume(i);
|
|
mVolumeMute[i] = aMixer->itemIsMuted(i);
|
|
|
|
if (lastVolume[i] != mVolume[i]) {
|
|
int y;
|
|
|
|
// Set button position
|
|
if (mError) {
|
|
y = BUTTON_MIN;
|
|
} else {
|
|
y = percentToPosition(mVolume[i]);
|
|
}
|
|
|
|
setButtonPosition(i, y);
|
|
lastVolume[i] = mVolume[i];
|
|
}
|
|
|
|
// set buttom type muted/unmuted
|
|
if (lastVolumeMute[i] != mVolumeMute[i]) {
|
|
setButtonType(i);
|
|
lastVolumeMute[i] = mVolumeMute[i];
|
|
}
|
|
}
|
|
|
|
if (mError) {
|
|
cerr << APPNAME << ": unable to read from " << mMixerDevice << endl;
|
|
showErrorLed();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Mixer::setVolume(int button, int volume)
|
|
{
|
|
if (mError) {
|
|
return;
|
|
}
|
|
|
|
// Store volume
|
|
mVolume[button] = volume;
|
|
|
|
// Write to device
|
|
aMixer->itemSetVolume(button, mVolume[button]);
|
|
}
|
|
|
|
void Mixer::toggleMute(int button)
|
|
{
|
|
mVolumeMute[button] = !mVolumeMute[button];
|
|
aMixer->itemToggleMute(button);
|
|
setButtonType(button);
|
|
}
|
|
|
|
void Mixer::setButtonType(int button)
|
|
{
|
|
Xpm* image;
|
|
|
|
if (mVolumeMute[button] == 1) { // muted
|
|
image = new Xpm(mDisplay, mRoot, mutebutton_xpm);
|
|
image->setWindowPixmap(mButton[button]);
|
|
delete image;
|
|
|
|
XClearWindow(mDisplay, mButton[button]);
|
|
} else {
|
|
image = new Xpm(mDisplay, mRoot, button_xpm);
|
|
image->setWindowPixmap(mButton[button]);
|
|
delete image;
|
|
|
|
XClearWindow(mDisplay, mButton[button]);
|
|
}
|
|
}
|
|
|
|
void Mixer::setButtonPosition(int button, int position) {
|
|
if (position > BUTTON_MIN) {
|
|
position = BUTTON_MIN;
|
|
} else if (position < BUTTON_MAX) {
|
|
position = BUTTON_MAX;
|
|
}
|
|
|
|
XMoveWindow(mDisplay, mButton[button], ButtonX[button], position);
|
|
|
|
mVolumePos[button] = position;
|
|
}
|
|
|
|
void Mixer::setButtonPositionRelative(int button, int relativePosition)
|
|
{
|
|
int y;
|
|
|
|
// Calc new button position
|
|
y = mVolumePos[button] + relativePosition;
|
|
|
|
if (y > BUTTON_MIN) {
|
|
y = BUTTON_MIN;
|
|
} else if (y < BUTTON_MAX) {
|
|
y = BUTTON_MAX;
|
|
}
|
|
|
|
// Set button position and volume
|
|
XMoveWindow(mDisplay, mButton[button], ButtonX[button], y);
|
|
|
|
mVolumePos[button] = y;
|
|
|
|
// set volume
|
|
setVolume(button, positionToPercent(y));
|
|
}
|
|
|
|
void Mixer::run()
|
|
{
|
|
XEvent event;
|
|
int buttonDown = 0;
|
|
int buttonDownPosition = 0;
|
|
|
|
// Start handling events
|
|
while(1) {
|
|
while(XPending(mDisplay) || buttonDown) {
|
|
XNextEvent(mDisplay, &event);
|
|
|
|
switch(event.type) {
|
|
case ButtonPress:
|
|
if (event.xbutton.button == Button4 || event.xbutton.button == Button5) {
|
|
// Wheel scroll
|
|
setButtonPositionRelative(mWheelButton - 1, event.xbutton.button == Button5? 3: -3);
|
|
} else if (event.xbutton.button == Button1 && event.xbutton.window != mIconWin) {
|
|
// Volume change
|
|
buttonDown = 1;
|
|
buttonDownPosition = event.xbutton.y;
|
|
} else if (event.xbutton.button == Button3 && buttonDown == 0 && event.xbutton.window != mIconWin) {
|
|
// Mute
|
|
for (int i=0; i<3; i++) {
|
|
if (mButton[i] == event.xbutton.window) {
|
|
toggleMute(i);
|
|
break;
|
|
}
|
|
}
|
|
} else if (event.xbutton.button == Button2) {
|
|
// Load defaults or execute command
|
|
if (mCommand) {
|
|
char command[512];
|
|
|
|
snprintf(command, 512, "%s &", mCommand);
|
|
system(command);
|
|
}
|
|
else
|
|
loadVolumeSettings();
|
|
}
|
|
break;
|
|
|
|
case ButtonRelease:
|
|
if (event.xbutton.button == Button1) {
|
|
buttonDown = 0;
|
|
}
|
|
break;
|
|
|
|
case MotionNotify:
|
|
if (buttonDown) {
|
|
// Find button
|
|
for (int i=0; i<3; i++) {
|
|
if (mButton[i] == event.xmotion.window) {
|
|
setButtonPositionRelative(i, event.xmotion.y - buttonDownPosition);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Idle for a moment
|
|
usleep(100000);
|
|
|
|
// Update volume status
|
|
aMixer->handleEvents();
|
|
if (AMixer::mixerChanged())
|
|
aMixer->reInit();
|
|
else if (AMixer::mixerElemsChanged())
|
|
getVolume();
|
|
XSync(mDisplay, False);
|
|
}
|
|
}
|
|
|