Skip to content

Instantly share code, notes, and snippets.

@jackoalan
Last active March 31, 2019 00:58
Show Gist options
  • Save jackoalan/944a10950791e04487620e8c826c4ffb to your computer and use it in GitHub Desktop.
Save jackoalan/944a10950791e04487620e8c826c4ffb to your computer and use it in GitHub Desktop.
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <pulse/xmalloc.h>
#include <pulsecore/namereg.h>
#include <pulsecore/sink.h>
#include <pulsecore/module.h>
#include <pulsecore/core-util.h>
#include <libcec/cecc.h>
#define CEC_LOGGING 0
PA_MODULE_AUTHOR("Jack Andersen");
PA_MODULE_DESCRIPTION(_("HDMI CEC Volume"));
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_LOAD_ONCE(true);
static libcec_configuration g_config;
struct userdata {
pa_module *module;
pa_sink *sink;
pa_hook_slot *volume_changed_slot;
pa_hook_slot *sink_put_slot;
pa_defer_event *recover_defer;
libcec_connection_t cec;
int target_vol;
int key_pressed;
#if CEC_LOGGING
FILE* log_file;
#endif
};
#if CEC_LOGGING
static void cb_cec_log_message(void* usr_data, const cec_log_message* message) {
struct userdata* u = usr_data;
const char* strLevel;
switch (message->level)
{
case CEC_LOG_ERROR:
strLevel = "ERROR: ";
break;
case CEC_LOG_WARNING:
strLevel = "WARNING: ";
break;
case CEC_LOG_NOTICE:
strLevel = "NOTICE: ";
break;
case CEC_LOG_TRAFFIC:
strLevel = "TRAFFIC: ";
break;
case CEC_LOG_DEBUG:
strLevel = "DEBUG: ";
break;
default:
break;
}
fprintf(u->log_file, "%s[%" PRId64 "]\t%s\n", strLevel, message->time, message->message);
fflush(u->log_file);
}
#endif
static void give_audio_status(struct userdata* u) {
cec_command give_status = {};
give_status.initiator = CECDEVICE_RECORDINGDEVICE1;
give_status.destination = CECDEVICE_AUDIOSYSTEM;
give_status.opcode = CEC_OPCODE_GIVE_AUDIO_STATUS;
give_status.opcode_set = 1;
give_status.transmit_timeout = CEC_DEFAULT_TRANSMIT_TIMEOUT;
libcec_transmit(u->cec, &give_status);
}
static void cb_cec_command_received(void* usr_data, const cec_command* command) {
struct userdata* u = usr_data;
if (command->opcode == CEC_OPCODE_REPORT_AUDIO_STATUS) {
/* Volume reply from receiver. */
int cur_vol = command->parameters.data[0];
/* Press volume key in a closed-loop fashion. */
if (cur_vol > u->target_vol) {
libcec_send_keypress(u->cec, CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_DOWN, 1);
u->key_pressed = 1;
} else if (cur_vol < u->target_vol) {
libcec_send_keypress(u->cec, CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_UP, 1);
u->key_pressed = 1;
}
/* Release volume key if close to avoid severe overshoot */
if (u->key_pressed && abs(cur_vol - u->target_vol) < 8) {
libcec_send_key_release(u->cec, CECDEVICE_AUDIOSYSTEM, 1);
u->key_pressed = 0;
}
/* Request new volume (next loop iteration). */
give_audio_status(u);
} else if (command->opcode == CEC_OPCODE_SET_SYSTEM_AUDIO_MODE) {
/* Receiver turned on; initiate request loop. */
if (command->parameters.data[0])
give_audio_status(u);
}
}
static void cb_cec_alert(void* usr_data, const libcec_alert alert, const libcec_parameter param) {
struct userdata* u = usr_data;
/* This generally happens after waking from sleep. Defer libcec reset. */
if (alert == CEC_ALERT_CONNECTION_LOST)
u->module->core->mainloop->defer_enable(u->recover_defer, 1);
}
static ICECCallbacks g_callbacks = {
#if CEC_LOGGING
.logMessage = cb_cec_log_message,
#endif
.commandReceived = cb_cec_command_received,
.alert = cb_cec_alert,
};
static pa_hook_result_t volume_changed_cb(pa_core *c, pa_sink *sink, struct userdata* u) {
if (u->sink != sink)
return PA_HOOK_OK;
u->target_vol = pa_cvolume_avg(&sink->reference_volume) / (double)PA_VOLUME_NORM * 100;
return PA_HOOK_OK;
}
static void pa_sink_set_volume_cb(pa_sink *sink) {
/* Empty callback to use "hardware" volume.
*
* We use the PA_CORE_HOOK_SINK_VOLUME_CHANGED hook to actually handle
* the volume change since the module userdata is not available this way. */
}
static pa_hook_result_t sink_put_cb(pa_core *c, pa_sink *sink, struct userdata* u) {
if (strstr(sink->name, "hdmi")) {
/* Reregister HDMI sink (typically happens with new user session). */
u->sink = sink;
u->target_vol = pa_cvolume_avg(&sink->reference_volume) / (double)PA_VOLUME_NORM * 100;
pa_sink_set_set_volume_callback(sink, pa_sink_set_volume_cb);
pa_cvolume_reset(&sink->soft_volume, sink->sample_spec.channels);
}
return PA_HOOK_OK;
}
static void recover_cb(pa_mainloop_api*a, pa_defer_event* e, struct userdata *u) {
a->defer_enable(e, 0);
/* Completely reset libcec. */
if (u->cec)
libcec_destroy(u->cec);
u->cec = libcec_initialise(&g_config);
if (u->cec) {
cec_adapter devices[1];
int8_t iDevicesFound;
iDevicesFound = libcec_find_adapters(u->cec, devices, 1, NULL);
if (iDevicesFound) {
libcec_open(u->cec, devices[0].comm, 5000);
/* Turn on receiver. */
libcec_power_on_devices(u->cec, CECDEVICE_AUDIOSYSTEM);
/* If receiver is already on, initiate volume request loop. */
give_audio_status(u);
}
}
}
int pa__init(pa_module*m) {
struct userdata *u;
m->userdata = u = pa_xnew(struct userdata, 1);
u->module = m;
u->sink = NULL;
#if CEC_LOGGING
u->log_file = fopen("/home/jacko/Desktop/audlog.txt", "w");
#endif
u->target_vol = 0;
u->key_pressed = 0;
if (m->core->sinks) {
pa_sink *sink;
uint32_t sidx;
PA_IDXSET_FOREACH(sink, m->core->sinks, sidx)
sink_put_cb(m->core, sink, u);
}
libcec_clear_configuration(&g_config);
g_config.clientVersion = LIBCEC_VERSION_CURRENT;
g_config.bActivateSource = 0;
g_config.iComboKeyTimeoutMs = 0;
g_config.iButtonReleaseDelayMs = 0;
g_config.callbacks = &g_callbacks;
g_config.callbackParam = u;
snprintf(g_config.strDeviceName, sizeof(g_config.strDeviceName), "PA CEC");
g_config.deviceTypes.types[0] = CEC_DEVICE_TYPE_RECORDING_DEVICE;
u->cec = NULL;
u->recover_defer = m->core->mainloop->defer_new(m->core->mainloop, (pa_defer_event_cb_t)recover_cb, u);
u->volume_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_VOLUME_CHANGED], PA_HOOK_LATE,
(pa_hook_cb_t)volume_changed_cb, u);
u->sink_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT], PA_HOOK_LATE,
(pa_hook_cb_t)sink_put_cb, u);
return 0;
}
void pa__done(pa_module*m) {
struct userdata *u;
pa_assert(m);
if (!(u = m->userdata))
return;
if (u->cec) {
/* Turn off receiver. */
libcec_standby_devices(u->cec, CECDEVICE_AUDIOSYSTEM);
libcec_destroy(u->cec);
}
#if CEC_LOGGING
fclose(u->log_file);
#endif
if (u->volume_changed_slot)
pa_hook_slot_free(u->volume_changed_slot);
if (u->sink_put_slot)
pa_hook_slot_free(u->sink_put_slot);
if (u->recover_defer)
m->core->mainloop->defer_free(u->recover_defer);
pa_xfree(u);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment