summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/buffer/audio.c69
-rw-r--r--src/buffer/audio.h15
-rw-r--r--src/buffer/video.c6
-rw-r--r--src/buffer/volume.h30
-rw-r--r--src/codec/ffmpeg/decoder.c3
-rw-r--r--src/codec/ffmpeg/encoder.c9
-rw-r--r--src/codec/ffmpeg/encoder.h2
-rw-r--r--src/fruits/cmc/cmc.c49
-rw-r--r--src/liana/list.c41
-rw-r--r--src/liana/list.h2
-rw-r--r--src/liana/vcr.c7
-rw-r--r--src/libclient/client.c20
-rw-r--r--src/libclient/client.h5
-rw-r--r--src/libsink/sink.c7
-rw-r--r--src/libsink/sink.h1
-rw-r--r--src/mixer/audio_miniaudio.c4
-rw-r--r--src/mixer/mixer.c36
-rw-r--r--src/mixer/mixer.h3
-rw-r--r--src/screen/screen.c18
-rw-r--r--src/screen/screen.h1
-rw-r--r--src/server/server.c106
-rw-r--r--src/sink/desktop.c5
22 files changed, 306 insertions, 133 deletions
diff --git a/src/buffer/audio.c b/src/buffer/audio.c
index 8d33f79..3abae42 100644
--- a/src/buffer/audio.c
+++ b/src/buffer/audio.c
@@ -9,12 +9,12 @@
#include "common_internal.h"
#include "volume.h"
-#define BUFFER_SIZE (6 * 1000000L)
-#define BUFFER_MARK_MIN (2.6 * 1000000L) // Must be a most half of the buffer size.
-#define BUFFER_MARK_BUFFERED (2.5 * 1000000L)
+#define BUFFER_SIZE (9 * 1000000L)
+#define BUFFER_MARK_MIN (4.3 * 1000000L) // Must be a most half of the buffer size.
+#define BUFFER_MARK_BUFFERED (4.0 * 1000000L)
#ifdef CAMU_AUDIO_BUFFER_FADE
-#define FADE_STEP(fmt) (1.1f / (fmt)->sample_rate)
+#define FADE_STEP(fmt) (1.f / (fmt)->sample_rate)
#endif
enum {
@@ -29,10 +29,11 @@ static void reset_buffer_state(struct camu_audio_buffer *buf)
{
buf->pts = -1.0;
buf->pause = PAUSE_PAUSED;
- al_atomic_store(u8)(&buf->unpause, 0, AL_ATOMIC_RELAXED);
- buf->volume = 1.f;
+ al_atomic_store(s32)(&buf->unpause, 0, AL_ATOMIC_RELAXED);
+ al_atomic_store(s32)(&buf->volume.set, 0, AL_ATOMIC_RELAXED);
#ifdef CAMU_AUDIO_BUFFER_FADE
- buf->fade_offset = 0;
+ buf->fade.offset = 0;
+ buf->fade.volume = 1.f;
#endif
buf->buffered = false;
al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED);
@@ -88,6 +89,12 @@ bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_code
return true;
}
+void camu_audio_buffer_set_volume(struct camu_audio_buffer *buf, f32 volume)
+{
+ al_atomic_store(f32)(&buf->volume.queued, volume, AL_ATOMIC_RELAXED);
+ al_atomic_add(s32)(&buf->volume.set, 1, AL_ATOMIC_RELAXED);
+}
+
void camu_audio_buffer_set_latency(struct camu_audio_buffer *buf, f64 latency)
{
buf->latency = latency;
@@ -191,7 +198,7 @@ void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_codec_fra
void camu_audio_buffer_unpause(struct camu_audio_buffer *buf)
{
- al_atomic_store(u8)(&buf->unpause, 1, AL_ATOMIC_RELAXED);
+ al_atomic_add(s32)(&buf->unpause, 1, AL_ATOMIC_RELAXED);
}
// Not thread-safe, must be called while the buffer is not being read from or written to.
@@ -238,12 +245,23 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
return 0;
}
+ if (al_atomic_load(s32)(&buf->volume.set, AL_ATOMIC_ACQUIRE) > 0) {
+ buf->volume.user = al_atomic_load(f32)(&buf->volume.queued, AL_ATOMIC_RELAXED);
+#ifdef CAMU_AUDIO_BUFFER_FADE
+ if (buf->fade.volume == -1.f) {
+ buf->fade.volume = buf->volume.user;
+ }
+#endif
+ al_atomic_sub(s32)(&buf->volume.set, 1, AL_ATOMIC_RELEASE);
+ }
+
if (camu_clock_is_paused(buf->clock)) {
#ifdef CAMU_AUDIO_BUFFER_FADE
if (buf->pause == PAUSE_PLAYING) {
buf->pause = PAUSE_FADING;
- buf->fade_offset = 0;
- } else if (buf->volume == 0.f || buf->pause == PAUSE_PAUSED) {
+ buf->fade.offset = 0;
+ } else if (buf->fade.volume == 0.f || buf->pause == PAUSE_PAUSED) {
+ buf->fade.offset = 0;
al_memset(data, 0, req);
if (NOT_PAUSED(buf->pause)) {
buf->pause = PAUSE_PAUSED;
@@ -263,10 +281,10 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
#endif
}
- if (al_atomic_load(u8)(&buf->unpause, AL_ATOMIC_ACQUIRE)) {
+ if (al_atomic_load(s32)(&buf->unpause, AL_ATOMIC_ACQUIRE) > 0) {
// If pause != PLAYING here, this will cause unexpected behavior.
buf->pause = PAUSE_PAUSED;
- al_atomic_store(u8)(&buf->unpause, 0, AL_ATOMIC_RELEASE);
+ al_atomic_sub(s32)(&buf->unpause, 1, AL_ATOMIC_RELEASE);
}
f64 pts = camu_clock_get_pts(buf->clock, buf->latency);
@@ -274,8 +292,8 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
size_t have = al_ring_buffer_occupied(&buf->rb);
#ifdef CAMU_AUDIO_BUFFER_FADE
// Cut off fade if it's reaching too far.
- if (have < buf->fade_offset) have = 0;
- else have -= buf->fade_offset;
+ if (have < buf->fade.offset) have = 0;
+ else have -= buf->fade.offset;
if (buf->pause != PAUSE_FADING && buf->pause != PAUSE_PLAYING) {
#endif
pts -= buf->pts;
@@ -350,8 +368,8 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
if (buf->pause == PAUSE_FADING) {
// Peek into the ring buffer. This won't consume the data, so when
// we resume it will fade in from the same position the fade out started.
- ret = al_ring_buffer_peek(&buf->rb, data, buf->fade_offset, req);
- buf->fade_offset += ret;
+ ret = al_ring_buffer_peek(&buf->rb, data, buf->fade.offset, req);
+ buf->fade.offset += ret;
} else {
#endif
// Read as much as we determined we can.
@@ -361,19 +379,30 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
#ifdef CAMU_AUDIO_BUFFER_FADE
}
f32 step = 0.f;
- if (buf->pause == PAUSE_FADING) {
+ if (buf->pause == PAUSE_FADING || buf->fade.volume > buf->volume.user) {
step = -FADE_STEP(&buf->fmt.req);
- } else if (buf->volume < 1.f) {
+ } else if (buf->fade.volume < buf->volume.user) {
step = FADE_STEP(&buf->fmt.req);
}
- if (step != 0.f || buf->volume != 1.f) {
- buf->volume = apply_volume(data, req, &buf->fmt.req, buf->volume, step);
+ if (step != 0.f || buf->fade.volume != 1.f) {
+ if (buf->volume.user > 1.f) {
+ step *= buf->volume.user;
+ }
+ buf->fade.volume = apply_volume(data, req, &buf->fmt.req, buf->fade.volume, buf->volume.user, step);
+ }
+#else
+ if (buf->volume.user != 1.f) {
+ apply_volume(data, req, &buf->fmt.req, buf->volume.user, 0.f, 0.f);
}
#endif
}
// Check if we should request to uncork.
+#ifdef CAMU_AUDIO_BUFFER_FADE
+ if (flow == FLOWING && buf->pause != PAUSE_FADING) {
+#else
if (flow == FLOWING) {
+#endif
ret = al_atomic_load(size_t)(&buf->uncork_at, AL_ATOMIC_RELAXED);
if (ret && have - req <= ret) {
buf->callback(buf->userdata, CAMU_BUFFER_UNCORK);
diff --git a/src/buffer/audio.h b/src/buffer/audio.h
index 0f84d5a..1d49448 100644
--- a/src/buffer/audio.h
+++ b/src/buffer/audio.h
@@ -17,7 +17,7 @@ struct camu_audio_buffer {
f64 pts;
u8 pause;
- atomic(u8) unpause;
+ atomic(s32) unpause;
struct camu_clock *clock;
bool ignore_desync;
f64 latency;
@@ -36,9 +36,17 @@ struct camu_audio_buffer {
struct camu_peak_buffer peak;
atomic(size_t) uncork_at;
- f32 volume;
+ struct {
+ f32 user;
+ atomic(f32) queued;
+ atomic(s32) set;
+ } volume;
+
#ifdef CAMU_AUDIO_BUFFER_FADE
- size_t fade_offset;
+ struct {
+ f32 volume;
+ size_t offset;
+ } fade;
#endif
#ifdef CAMU_MIXER_THREADED
@@ -51,6 +59,7 @@ struct camu_audio_buffer {
bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, struct camu_mixer *mixer);
bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream);
+void camu_audio_buffer_set_volume(struct camu_audio_buffer *buf, f32 volume);
void camu_audio_buffer_set_latency(struct camu_audio_buffer *buf, f64 latency);
void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_codec_frame *frame);
void camu_audio_buffer_unpause(struct camu_audio_buffer *buf);
diff --git a/src/buffer/video.c b/src/buffer/video.c
index 91c3687..a8b35c6 100644
--- a/src/buffer/video.c
+++ b/src/buffer/video.c
@@ -10,9 +10,9 @@
#endif
#endif
-#define BUFFER_MARK_LOW 0.199998
-#define BUFFER_MARK_BUFFERED 0.133332
-#define BUFFER_MARK_HIGH 0.33333
+#define BUFFER_MARK_LOW ((1.0 / 30.0) * 6)
+#define BUFFER_MARK_BUFFERED ((1.0 / 30.0) * 8)
+#define BUFFER_MARK_HIGH ((1.0 / 30.0) * 12)
#define BUFFER_MARK_RESET (BUFFER_MARK_HIGH * 2.0)
bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer)
diff --git a/src/buffer/volume.h b/src/buffer/volume.h
index 0cad8d3..d39affc 100644
--- a/src/buffer/volume.h
+++ b/src/buffer/volume.h
@@ -2,26 +2,24 @@
AL_UNUSED_FUNCTION_PUSH
-// https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1AB9U8lJL6yAngGVG6AMKpaAVxYMQ0hwBk8DTAA5dwAjTGIvAFZSAAdUBUJbBmc3Dy9Y%2BMSBPwDgljCIyWjLTGskoQImYgIU909pErKBCqqCHKDQ8KiLSuratIbetv8O/K6igEoLVFdiZHYOAFIAJgBmf2Q3LABqRdXHBQJ8QQA6BD3sRY0AQRX1hk3XHb2Do6wqM4ur27WNrcxdvsqLRUEwCJ9Vpcbt87u8Rttrj5jABZa4ADQgTFI2xCE22%2BIJu0ijm%2BEEWAHYAEKEmm0ukExbE76E0wEACeMUwqCopkxeOMTEBABFtkw9tSiSSbizjOzOdzebjtsYQsKceL8YypdcWYKLsrVXsAGLKwUgA0awlamHkoUTGFrOEBBFI5EASUCmOxStp1puZKp9KD9L9OoJrI5XJ5xj5prVYtWEtDMrlUcV/MNqxFIUtyfDev2BsBJoF23NKstDKZ0Nt9uhjswNGdiOMjh812RyggDGxIOxCDweJb7s9A%2Bxw/RED72wYEzrtxuqfe238BAAHLLtgo1%2BLvkvG9tXKuNwRDzvE3vI8vV1wAGybhR33eLq8Ho%2BCO%2Bb1xPi8vznXwRVmWB8gOfa59yoQ9VyAr9QN/cDX0g1db0kB8ULAiCoMEFCv3Q%2BDL3/A9gVBU8qDgykCMwZd0BmEJ6G2Kg8Io6EbkOME8GQBigOVAUCAIYg8BCVwCEwUxtigBhUH8WgRjnUUYhiWg2WMAA3Fx3FEsjlmobiACp0DBLEtzwAAvUTTwUVhFNEtBXEEbEFG45AEEMAJaGMWz7K45ZtjU1JMGxLStxEmJ5wpZiw3xfhiHE1xuLwNUNA1BKXi3Kz6A8mZBGSlYKOWSk8XC5k6TwSCIBS5ZIm2ZzXMwdzPNPPYhSa7YNEKqliuDPyNLVFs2w7LturYXZ8uCzAYmxDQTiobEuGm%2B14JDW1OppRjJDSlhrLVagUImAzKkZSk8EZZrFrpSzNvo3SWqGzAwPpfaxUiI6Tu2rSJj69tOwgC7rOxI0fAAFRRD1/qBlF0QWiKVopZrpQJYhMAIWYGF89S2DA2GYRuVdthYJh/AgNS8HQMKOvhqLuMew6uGWSRlhO%2B6CSC261TmpKzvxVmWqYBSlNU9HNKAiBHtmunlmxCW0f8yaTg0LguCoKGVsR5HiFRjnodtDgploThIl4TxuF4VBOEcLcZjmAE7h4UgCE0HWpgAaxASJVhOLgAE5PbXenPciOnPdWLhoj1jhJENh3SFNjheAUEANDth2pjgWAkDQTa6HCchKAzmIs4iZBNkMYAuFWeW%2BDoETiHjiAQijkJ/CqNlOFtxvmGINkAHkQm0Up7eN0gM7YQQu4YJSo6wITgEcMRaHjwesHxoxxA4LRSHwRGyhUzAF/XzBVFKYSFlt1c6qjmSQmIZvnCwKP%2BLwFhW94HfiBCeJMCFTBl%2BAGSjGTvgBhgAKAAGp4EwAAdy7pyI2tt%2BCCBEGIdgUgZCCEUCodQa8dAgAlgYf%2BpgPL6EEvHSAUxUAxBsAIBeABaQ46AmrIG9tsahXcfLUPxnMc4WYTIMFfmwlgyMAhNR4Xwk2r8BJYBIRAKYjRKGeAgA4fonguCkF8CMPIBQ9BxASHIpRWjMhyPaBoroKjZHlCGHo0xdV%2B7mNaEYzoERTEWPRsonodj1EOIkDIy28wvH6H1pHLB0dODbFUGuW81CULVTwcAbYZc5Ye3ErgQgJARrBwmLwAeWg5ykBdpIW8Jw1waHlpEckGhJDkkiBoW8a5yT%2BPDoE9eMc44JyTlgnJYdliNJNpwTJycpivwSHYSQQA
-
-static inline f32 apply_volume_f32(f32 *data, size_t sample_count, s32 channel_count, f32 volume, f32 step)
+static inline f32 apply_volume_f32(f32 *data, size_t sample_count, s32 channel_count, f32 volume, f32 user, f32 step)
{
for (u32 i = 0; i < sample_count; i++) {
- if (i % channel_count == 0) {
- volume = AL_CLAMP(volume + step, 0.f, 1.f);
+ if (step != 0.f && i % channel_count == 0) {
+ volume = AL_CLAMP(volume + step, 0.f, user);
}
f64 sample = (f64)data[i];
sample *= volume;
- data[i] = (f32)AL_CLAMP(sample, FLT_MIN, FLT_MAX);
+ data[i] = (f32)AL_CLAMP(sample, -1.0, 1.0);
}
return volume;
}
-static inline f32 apply_volume_s32(s32 *data, size_t sample_count, s32 channel_count, f32 volume, f32 step)
+static inline f32 apply_volume_s32(s32 *data, size_t sample_count, s32 channel_count, f32 volume, f32 user, f32 step)
{
for (u32 i = 0; i < sample_count; i++) {
- if (i % channel_count == 0) {
- volume = AL_CLAMP(volume + step, 0.f, 1.f);
+ if (step != 0.f && i % channel_count == 0) {
+ volume = AL_CLAMP(volume + step, 0.f, user);
}
f64 sample = (f64)data[i];
sample *= volume;
@@ -30,11 +28,11 @@ static inline f32 apply_volume_s32(s32 *data, size_t sample_count, s32 channel_c
return volume;
}
-static inline f32 apply_volume_s16(s16 *data, size_t sample_count, s32 channel_count, f32 volume, f32 step)
+static inline f32 apply_volume_s16(s16 *data, size_t sample_count, s32 channel_count, f32 volume, f32 user, f32 step)
{
for (u32 i = 0; i < sample_count; i++) {
- if (i % channel_count == 0) {
- volume = AL_CLAMP(volume + step, 0.f, 1.f);
+ if (step != 0.f && i % channel_count == 0) {
+ volume = AL_CLAMP(volume + step, 0.f, user);
}
f64 sample = (f64)data[i];
sample *= volume;
@@ -43,22 +41,22 @@ static inline f32 apply_volume_s16(s16 *data, size_t sample_count, s32 channel_c
return volume;
}
-static inline f32 apply_volume(u8 *data, size_t size, struct camu_audio_format *fmt, f32 volume, f32 step)
+static inline f32 apply_volume(u8 *data, size_t size, struct camu_audio_format *fmt, f32 volume, f32 user, f32 step)
{
size_t sample_count;
switch (camu_audio_format_bytes_per_sample(fmt)) {
case 4:
sample_count = size / 4;
if (fmt->format == CAMU_SAMPLE_FORMAT_S32) {
- return apply_volume_s32((s32 *)data, sample_count, fmt->channel_count, volume, step);
+ return apply_volume_s32((s32 *)data, sample_count, fmt->channel_count, volume, user, step);
} else if (fmt->format == CAMU_SAMPLE_FORMAT_FLT) {
- return apply_volume_f32((f32 *)data, sample_count, fmt->channel_count, volume, step);
+ return apply_volume_f32((f32 *)data, sample_count, fmt->channel_count, volume, user, step);
}
break;
case 2:
sample_count = size / 2;
if (fmt->format == CAMU_SAMPLE_FORMAT_S16) {
- return apply_volume_s16((s16 *)data, sample_count, fmt->channel_count, volume, step);
+ return apply_volume_s16((s16 *)data, sample_count, fmt->channel_count, volume, user, step);
}
break;
}
diff --git a/src/codec/ffmpeg/decoder.c b/src/codec/ffmpeg/decoder.c
index 03940a0..d3ab3b2 100644
--- a/src/codec/ffmpeg/decoder.c
+++ b/src/codec/ffmpeg/decoder.c
@@ -43,8 +43,7 @@ static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *rend
av->codec_context->thread_type = FF_THREAD_SLICE;
al_log_debug("ff_decoder", "Using %i threads for decoder.", cpus);
-//#ifndef CAMU_SINK_NO_VIDEO
-#if 0
+#if !defined CAMU_SINK_NO_VIDEO && defined CAMU_RENDERER_VULKAN
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO && renderer && renderer->get_buffer2) {
av->codec_context->get_buffer2 = renderer->get_buffer2;
av->codec_context->opaque = renderer->opaque;
diff --git a/src/codec/ffmpeg/encoder.c b/src/codec/ffmpeg/encoder.c
index 83a8b55..11aaaff 100644
--- a/src/codec/ffmpeg/encoder.c
+++ b/src/codec/ffmpeg/encoder.c
@@ -11,7 +11,6 @@
#include "encoder.h"
-#define BITRATE 256000
#define FRAME_DURATION 20
#define SAMPLES_PER_FRAME (48000 / 1000.0 * FRAME_DURATION)
#define CHANNELS 2
@@ -21,7 +20,7 @@ static void free_frame_data(struct camu_ff_encoder *enc)
if (enc->frame && enc->frame->nb_samples > 0) av_freep(&enc->frame->data[0]);
}
-bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext)
+bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext, u32 bitrate)
{
al_memset(enc, 0, sizeof(struct camu_ff_encoder));
@@ -42,7 +41,7 @@ bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char
enc->codec_context->sample_rate = 48000;
av_channel_layout_default(&enc->codec_context->ch_layout, CHANNELS);
enc->codec_context->sample_fmt = AV_SAMPLE_FMT_FLT;
- enc->codec_context->bit_rate = BITRATE;
+ enc->codec_context->bit_rate = bitrate;
enc->codec_context->frame_size = SAMPLES_PER_FRAME;
enc->codec_context->time_base = (AVRational){ 1, enc->codec_context->sample_rate };
enc->codec_context->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
@@ -53,7 +52,9 @@ bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char
}
AVDictionary *opts = NULL;
- av_dict_set(&opts, "b", "256k", 0);
+ char kbps[16];
+ al_snprintf(kbps, sizeof(kbps), "%uk", bitrate / 1000);
+ av_dict_set(&opts, "b", kbps, 0);
av_dict_set(&opts, "vbr", "off", 0);
av_dict_set(&opts, "compression_level", "10", 0);
av_dict_set(&opts, "frame_duration", "20", 0);
diff --git a/src/codec/ffmpeg/encoder.h b/src/codec/ffmpeg/encoder.h
index c0d953c..238403f 100644
--- a/src/codec/ffmpeg/encoder.h
+++ b/src/codec/ffmpeg/encoder.h
@@ -18,7 +18,7 @@ struct camu_ff_encoder {
void *userdata;
};
-bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext);
+bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext, u32 bitrate);
void camu_ff_encoder_push(struct camu_ff_encoder *enc, u8 **data, s32 sample_count);
void camu_ff_encoder_reset(struct camu_ff_encoder *enc);
void camu_ff_encoder_close(struct camu_ff_encoder *enc);
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
index ad7c75e..d39d639 100644
--- a/src/fruits/cmc/cmc.c
+++ b/src/fruits/cmc/cmc.c
@@ -8,8 +8,14 @@
#include "cmc.h"
+enum {
+ CLI_ADD = 0
+};
+
struct cmc {
struct aki_event_loop loop;
+ u8 command;
+ array(str) args;
struct camu_client client;
struct camu_post_cache cache;
array(struct cmc_search *) searches;
@@ -29,7 +35,16 @@ static void client_callback(void *userdata, u8 op, void *opaque)
struct cmc *c = (struct cmc *)userdata;
switch (op) {
case CAMU_CLIENT_LOGIN: {
- camu_client_create_search(&c->client, al_str_c("youtube"), al_str_c(""));
+ switch (c->command) {
+ case CLI_ADD: {
+ str *arg;
+ al_array_foreach_ptr(c->args, i, arg) {
+ camu_client_add_from_path(&c->client, al_str_c("default"), arg);
+ }
+ camu_client_disconnect(&c->client);
+ break;
+ }
+ }
break;
}
case CAMU_CLIENT_SEARCH_CREATED: {
@@ -62,7 +77,7 @@ static void client_callback(void *userdata, u8 op, void *opaque)
al_array_push(page->list, s);
}
al_array_push(search->pages, page);
- camu_client_add(&c->client, al_str_c("default"), &al_array_at(page->list, 0), 0);
+ camu_client_add_from_post(&c->client, al_str_c("default"), &al_array_at(page->list, 0), 0);
break;
}
}
@@ -71,21 +86,45 @@ static void client_callback(void *userdata, u8 op, void *opaque)
static struct cmc c = { 0 };
#ifndef _WIN32
+static bool parse_cmd(s32 argc, char *argv[])
+#else
+static bool parse_cmd(s32 argc, wchar_t **argv)
+#endif
+{
+ if (argc < 2) return false;
+ if (al_str_eq(al_str_cr(argv[1]), al_str_c("add"))) {
+ if (argc < 3) return false;
+ c.command = CLI_ADD;
+ char path[PATH_MAX];
+ str arg;
+ for (s32 i = 2; i < argc; i++) {
+ if (realpath(argv[i], path)) {
+ al_str_clone(&arg, al_str_cr(path));
+ al_array_push(c.args, arg);
+ }
+ }
+ }
+ return true;
+}
+
+
+#ifndef _WIN32
s32 main(s32 argc, char *argv[])
#else
s32 wmain(s32 argc, wchar_t **argv)
#endif
{
- (void)argc;
- (void)argv;
if (!aki_common_init()) return EXIT_FAILURE;
aki_event_loop_init(&c.loop);
- camu_post_cache_init(&c.cache);
+ al_array_init(c.args);
+ camu_post_cache_init(&c.cache);
al_array_init(c.searches);
+ if (!parse_cmd(argc, argv)) return EXIT_FAILURE;
+
c.client.callback = client_callback;
c.client.userdata = &c;
if (!camu_client_login(&c.client, &c.loop, CAMU_LOCAL_TYPE, CAMU_LOCAL_ADDR, CAMU_PORT, al_str_c("andrew"))) {
diff --git a/src/liana/list.c b/src/liana/list.c
index df91140..65efc9d 100644
--- a/src/liana/list.c
+++ b/src/liana/list.c
@@ -52,6 +52,8 @@ void lia_list_init(struct lia_list *list, str *name)
list->cmd = NULL;
}
+static void pump_queue(struct lia_list *list);
+
static bool assume_ended(struct lia_list_entry *entry, u64 at)
{
if (entry->duration == 0) return true;
@@ -177,18 +179,32 @@ static bool handle_add(struct lia_list *list, struct lia_list_entry *entry)
return true;
}
-static void unset_all(struct lia_list *list)
+
+static void unset_all(struct lia_list *list, struct lia_list_entry *previous)
{
struct lia_list_sink *sink;
al_array_foreach(list->sinks, i, sink) {
sink->set = -1;
sink->queued = -1;
}
+ if (previous) {
+ list->cmd->op = SKIPTO;
+ struct lia_list_entry *entry;
+ al_array_foreach(list->entries, i, entry) {
+ if (entry->opaque == previous->opaque) {
+ list->cmd->i = list->current;
+ list->current = i;
+ list->cmd->sequence = i;
+ break;
+ }
+ }
+ pump_queue(list);
+ }
}
static void handle_unset(struct lia_list *list)
{
- unset_all(list);
+ unset_all(list, NULL);
list->current = list->entries.size - 1;
list->previous = -1;
list->queued = -1;
@@ -405,30 +421,33 @@ static void handle_end(struct lia_list *list, s32 sequence)
static void handle_reverse(struct lia_list *list)
{
+ struct lia_list_entry *previous = al_array_at(list->entries, list->current);
u32 size = list->entries.size;
for (u32 i = 0; i < size; i++) {
u32 tail = size - (i + 1);
if (tail <= i) break;
AL_SWAP(al_array_at(list->entries, i), al_array_at(list->entries, tail), struct lia_list_entry *);
}
- unset_all(list);
+ unset_all(list, previous);
}
static void handle_sort(struct lia_list *list)
{
+ struct lia_list_entry *previous = al_array_at(list->entries, list->current);
al_array_sort(list->entries, struct lia_list_entry *, camu_db_compare);
- unset_all(list);
+ unset_all(list, previous);
}
static void handle_shuffle(struct lia_list *list)
{
+ struct lia_list_entry *previous = al_array_at(list->entries, list->current);
u32 size = list->entries.size;
if (size == 0) return;
for (u32 i = 0; i < size - 1; i++) {
u32 j = i + al_rand() / (AL_RAND_MAX / (size - i) + 1);
AL_SWAP(al_array_at(list->entries, i), al_array_at(list->entries, j), struct lia_list_entry *);
}
- unset_all(list);
+ unset_all(list, previous);
}
static void handle_clear(struct lia_list *list)
@@ -439,13 +458,13 @@ static void handle_clear(struct lia_list *list)
al_free(entry);
}
list->entries.size = 0;
- unset_all(list);
+ unset_all(list, NULL);
list->current = -1;
list->previous = -1;
list->idle = true;
}
-static void pump_queue(struct lia_list *list)
+void pump_queue(struct lia_list *list)
{
if (!list->cmd) {
if (list->queue.size == 0) return;
@@ -490,16 +509,16 @@ static void pump_queue(struct lia_list *list)
break;
case REVERSE:
handle_reverse(list);
- break;
+ return;
case SORT:
handle_sort(list);
- break;
+ return;
case SHUFFLE:
handle_shuffle(list);
- break;
+ return;
case CLEAR:
handle_clear(list);
- break;
+ return;
}
al_free(cmd);
list->cmd = NULL;
diff --git a/src/liana/list.h b/src/liana/list.h
index 4f56641..7a24104 100644
--- a/src/liana/list.h
+++ b/src/liana/list.h
@@ -7,7 +7,7 @@
#define LIANA_SEQUENCE_ANY -1
#define LIANA_TIMESTAMP_INVALID UINT64_MAX
-#define LIANA_BASE_DELAY 475000Lu // 475ms
+#define LIANA_BASE_DELAY 575000Lu // 575ms
#define LIANA_BASE_PING 150000Lu // 150ms
#define LIANA_PAUSE_DELAY LIANA_BASE_PING
#define LIANA_DELAY_IGNORE 0Lu
diff --git a/src/liana/vcr.c b/src/liana/vcr.c
index 0f3eac4..213d5cb 100644
--- a/src/liana/vcr.c
+++ b/src/liana/vcr.c
@@ -109,9 +109,9 @@ static void cork_if_buffered(struct lia_vcr *vcr)
{
u8 buffered = 1;
struct lia_vcr_track *track;
- //al_array_foreach(vcr->tracks, i, track) {
- // buffered &= al_atomic_load(u8)(&track->buffered, AL_ATOMIC_RELAXED);
- //}
+ al_array_foreach(vcr->tracks, i, track) {
+ buffered &= al_atomic_load(u8)(&track->buffered, AL_ATOMIC_RELAXED);
+ }
if (buffered) {
aki_packet_stream_cork(vcr->data, true);
al_array_foreach(vcr->tracks, i, track) {
@@ -140,6 +140,7 @@ bool lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet)
if (!aki_packet_cache_send_packet(&track->cache, packet)) {
aki_packet_free(packet);
} else if ((count = al_atomic_add(s32)(&vcr->count, 1, AL_ATOMIC_RELAXED)) > vcr->mark.buffered) {
+ vcr->mark.buffered = count;
cork_if_buffered(vcr);
}
break;
diff --git a/src/libclient/client.c b/src/libclient/client.c
index 0776842..d7a2325 100644
--- a/src/libclient/client.c
+++ b/src/libclient/client.c
@@ -50,7 +50,7 @@ static void connection_callback(void *userdata, struct aki_rpc_connection *conn)
static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn)
{
struct camu_client *client = (struct camu_client *)userdata;
- al_assert(client->conn == conn);
+ al_assert(client->conn == NULL || client->conn == conn);
client->conn = NULL;
}
@@ -59,6 +59,7 @@ bool camu_client_login(struct camu_client *client, struct aki_event_loop *loop,
{
client->loop = loop;
al_str_clone(&client->username, username);
+ client->conn = NULL;
aki_rpc_init(&client->client, client->loop, connection_callback, connection_closed_callback, client);
for (u32 i = 0; i < AL_ARRAY_SIZE(commands); i++) {
commands[i].userdata = client;
@@ -109,7 +110,17 @@ void camu_client_get_page(struct camu_client *client, s32 id, u32 num)
aki_rpc_connection_command(client->conn, packet, NULL, NULL);
}
-void camu_client_add(struct camu_client *client, str *list, str *unique_id, u32 index)
+void camu_client_add_from_path(struct camu_client *client, str *list, str *path)
+{
+ struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SERVER_LIST_ACTION);
+ aki_packet_write_str(packet, list);
+ aki_packet_write_u8(packet, CAMU_LIST_ADD);
+ aki_packet_write_u8(packet, CAMU_RESOURCE_FILE);
+ aki_packet_write_str(packet, path);
+ aki_rpc_connection_command(client->conn, packet, NULL, NULL);
+}
+
+void camu_client_add_from_post(struct camu_client *client, str *list, str *unique_id, u32 index)
{
struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SERVER_LIST_ACTION);
aki_packet_write_str(packet, list);
@@ -119,3 +130,8 @@ void camu_client_add(struct camu_client *client, str *list, str *unique_id, u32
aki_packet_write_u32(packet, index);
aki_rpc_connection_command(client->conn, packet, NULL, NULL);
}
+
+void camu_client_disconnect(struct camu_client *client)
+{
+ aki_rpc_conn_flush(client->conn);
+}
diff --git a/src/libclient/client.h b/src/libclient/client.h
index 02a6639..c073c9e 100644
--- a/src/libclient/client.h
+++ b/src/libclient/client.h
@@ -28,4 +28,7 @@ void camu_client_toggle_sink(struct camu_client *client, str *sink, str *list, b
void camu_client_create_search(struct camu_client *client, str *module, str *query);
void camu_client_get_page(struct camu_client *client, s32 id, u32 num);
-void camu_client_add(struct camu_client *client, str *list, str *unique_id, u32 index);
+void camu_client_add_from_path(struct camu_client *client, str *list, str *path);
+void camu_client_add_from_post(struct camu_client *client, str *list, str *unique_id, u32 index);
+
+void camu_client_disconnect(struct camu_client *client);
diff --git a/src/libsink/sink.c b/src/libsink/sink.c
index ee2b216..894dc40 100644
--- a/src/libsink/sink.c
+++ b/src/libsink/sink.c
@@ -1051,6 +1051,7 @@ static void connection_callback(void *userdata, struct aki_rpc_connection *conn)
{
struct camu_sink *sink = (struct camu_sink *)userdata;
sink->conn = conn;
+ aki_timer_stop(&sink->reconnect_timer);
struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_IDENTIFY);
aki_packet_write_u8(packet, CAMU_SINK);
aki_packet_write_str(packet, &sink->name);
@@ -1062,7 +1063,6 @@ static void reconnect_timer_callback(void *userdata, struct aki_timer *timer)
struct camu_sink *sink = (struct camu_sink *)userdata;
(void)timer;
aki_rpc_reconnect(&sink->client, &sink->addr, sink->port);
- aki_timer_stop(&sink->reconnect_timer);
}
static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn)
@@ -1165,6 +1165,11 @@ void camu_sink_reseek(struct camu_sink *sink)
});
}
+void camu_sink_offset_volume(struct camu_sink *sink, f64 amount)
+{
+ camu_mixer_offset_volume(sink->audio.mixer, amount);
+}
+
void camu_sink_stop(struct camu_sink *sink)
{
aki_mutex_lock(&sink->mutex);
diff --git a/src/libsink/sink.h b/src/libsink/sink.h
index 0b55ad6..5feadf5 100644
--- a/src/libsink/sink.h
+++ b/src/libsink/sink.h
@@ -112,6 +112,7 @@ void camu_sink_shuffle(struct camu_sink *sink);
void camu_sink_toggle_pause(struct camu_sink *sink);
void camu_sink_seek(struct camu_sink *sink, f64 pos);
void camu_sink_reseek(struct camu_sink *sink);
+void camu_sink_offset_volume(struct camu_sink *sink, f64 amount);
void camu_sink_stop(struct camu_sink *sink);
void camu_sink_close(struct camu_sink *sink);
void camu_sink_free(struct camu_sink *sink);
diff --git a/src/mixer/audio_miniaudio.c b/src/mixer/audio_miniaudio.c
index 2a93491..7db1b5a 100644
--- a/src/mixer/audio_miniaudio.c
+++ b/src/mixer/audio_miniaudio.c
@@ -158,9 +158,9 @@ static bool audio_miniaudio_configure_stream(struct camu_audio *audio, void *opa
ma->config.performanceProfile = ma_performance_profile_conservative;
ma->config.periods = DEFAULT_PERIODS;
ma->config.periodSizeInMilliseconds = DEFAULT_PERIOD_SIZE_IN_MILLISECONDS;
- ma->config.playback.format = ma_format_s32;
+ ma->config.playback.format = ma_format_unknown;
ma->config.playback.channels = 2;
- ma->config.sampleRate = 44100;
+ ma->config.sampleRate = 0;
ma->config.dataCallback = data_callback;
ma->config.pUserData = ma;
ma->config.noPreSilencedOutputBuffer = MA_TRUE;
diff --git a/src/mixer/mixer.c b/src/mixer/mixer.c
index 5af63b7..7608efc 100644
--- a/src/mixer/mixer.c
+++ b/src/mixer/mixer.c
@@ -68,6 +68,7 @@ bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio)
mixer->audio->data_callback = data_callback;
mixer->audio->userdata = mixer;
mixer->audio->init(mixer->audio, al_str_c("cmv"));
+ mixer->volume = 1.f;
mixer->paused = true;
mixer->empty_after = 0;
al_array_init(mixer->buffers);
@@ -85,6 +86,40 @@ void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_resampler_form
mixer->audio->pick_format(mixer->audio, fmt);
}
+void camu_mixer_set_volume(struct camu_mixer *mixer, f32 volume)
+{
+#ifdef CAMU_MIXER_THREADED
+ aki_mutex_lock(&mixer->mutex);
+#endif
+ mixer->volume = volume;
+ struct camu_audio_buffer *buf;
+ al_array_foreach(mixer->buffers, i, buf) {
+ camu_audio_buffer_set_volume(buf, mixer->volume);
+ }
+#ifdef CAMU_MIXER_THREADED
+ aki_mutex_unlock(&mixer->mutex);
+#endif
+}
+
+void camu_mixer_offset_volume(struct camu_mixer *mixer, f32 amount)
+{
+#ifdef CAMU_MIXER_THREADED
+ aki_mutex_lock(&mixer->mutex);
+#endif
+ if (FLT_MAX - mixer->volume < amount) {
+ mixer->volume = FLT_MAX;
+ } else {
+ mixer->volume = AL_MAX(0.f, mixer->volume + amount);
+ }
+ struct camu_audio_buffer *buf;
+ al_array_foreach(mixer->buffers, i, buf) {
+ camu_audio_buffer_set_volume(buf, mixer->volume);
+ }
+#ifdef CAMU_MIXER_THREADED
+ aki_mutex_unlock(&mixer->mutex);
+#endif
+}
+
f64 camu_mixer_get_latency(struct camu_mixer *mixer)
{
return mixer->audio->get_latency(mixer->audio) / 1000000.0;
@@ -92,6 +127,7 @@ f64 camu_mixer_get_latency(struct camu_mixer *mixer)
static void add_buffer_internal(struct camu_mixer *mixer, struct camu_audio_buffer *buf)
{
+ camu_audio_buffer_set_volume(buf, mixer->volume);
#ifdef CAMU_MIXER_THREADED
al_atomic_store(u8)(&buf->ref, 1, AL_ATOMIC_RELAXED);
#endif
diff --git a/src/mixer/mixer.h b/src/mixer/mixer.h
index 9a15751..356e2b1 100644
--- a/src/mixer/mixer.h
+++ b/src/mixer/mixer.h
@@ -15,6 +15,7 @@ enum {
struct camu_mixer {
struct camu_audio *audio;
+ f32 volume;
struct camu_resampler_format fmt;
bool paused;
u8 empty_after;
@@ -31,6 +32,8 @@ struct camu_mixer {
bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio);
void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_resampler_format *fmt);
+void camu_mixer_set_volume(struct camu_mixer *mixer, f32 volume);
+void camu_mixer_offset_volume(struct camu_mixer *mixer, f32 amount);
f64 camu_mixer_get_latency(struct camu_mixer *mixer);
void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf);
void camu_mixer_remove_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf);
diff --git a/src/screen/screen.c b/src/screen/screen.c
index f949c1d..aa61ba6 100644
--- a/src/screen/screen.c
+++ b/src/screen/screen.c
@@ -133,15 +133,19 @@ static bool mouse_button_callback(void *userdata, u8 state, u8 button)
static bool scroll_callback(void *userdata, f64 y)
{
struct camu_screen *scr = (struct camu_screen *)userdata;
- struct camu_view *view = get_view_from_mouse_pos(scr);
- if (view) {
- if (scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) {
- y = -y / SCROLL_MULTIPLIER;
- if (camu_view_zoom_simple(view, scr->width, scr->height, scr->last_mouse_x, scr->last_mouse_y, y)) {
- view->mode = CAMU_VIEW_DETACHED;
- return true;
+ y = -y / SCROLL_MULTIPLIER;
+ if (!(scr->flags & CAMU_SCREEN_MODIFIER)) {
+ struct camu_view *view = get_view_from_mouse_pos(scr);
+ if (view) {
+ if (scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) {
+ if (camu_view_zoom_simple(view, scr->width, scr->height, scr->last_mouse_x, scr->last_mouse_y, y)) {
+ view->mode = CAMU_VIEW_DETACHED;
+ return true;
+ }
}
}
+ } else {
+ scr->callback(scr->userdata, CAMU_SCREEN_VOLUME, &y);
}
return false;
}
diff --git a/src/screen/screen.h b/src/screen/screen.h
index a10796a..0c56cf9 100644
--- a/src/screen/screen.h
+++ b/src/screen/screen.h
@@ -34,6 +34,7 @@ enum {
CAMU_SCREEN_TOGGLE_PAUSE,
CAMU_SCREEN_SEEK,
CAMU_SCREEN_RESEEK,
+ CAMU_SCREEN_VOLUME,
CAMU_SCREEN_CLOSE
};
diff --git a/src/server/server.c b/src/server/server.c
index a77046b..6fc2892 100644
--- a/src/server/server.c
+++ b/src/server/server.c
@@ -188,56 +188,6 @@ void handle_toggle_sink(struct camu_server *server, str *name, struct camu_serve
}
}
-static bool client_command_callback(void *userdata, struct aki_rpc_connection *conn,
- struct aki_packet *packet, struct aki_packet *rpacket)
-{
- struct camu_server *server = (struct camu_server *)userdata;
- (void)rpacket;
-
- struct camu_server_client *client = get_client_by_connection(server, conn);
- if (!client) goto out;
-
- u8 op = aki_packet_read_u8(packet);
- switch (op) {
- case CAMU_CLIENT_CREATE_LIST: {
- str name;
- aki_packet_read_str(packet, &name);
- struct lia_list *list = al_alloc_object(struct lia_list);
- lia_list_init(list, &name);
- al_array_push(server->lists, list);
- break;
- }
- case CAMU_CLIENT_TOGGLE_SINK: {
- str name;
- aki_packet_read_str(packet, &name);
- struct camu_server_sink *sink = get_sink_from_name(server, &name);
- if (!sink) goto out;
- aki_packet_read_str(packet, &name); // list name.
- bool enable = aki_packet_read_bool(packet);
- handle_toggle_sink(server, &name, sink, enable);
- break;
- }
- case CAMU_CLIENT_CREATE_SEARCH: {
- str module;
- aki_packet_read_str(packet, &module);
- str query;
- aki_packet_read_str(packet, &query);
- camu_portal_create_search(&server->bridge, &module, &query, client_portal_callback, conn);
- break;
- }
- case CAMU_CLIENT_GET_PAGE: {
- s32 id = aki_packet_read_s32(packet);
- u32 num = aki_packet_read_u32(packet);
- camu_portal_get_page(&server->bridge, id, num, client_portal_callback, conn);
- break;
- }
- }
-
-out:
- aki_packet_free(packet);
- return true;
-}
-
static void node_callback(void *userdata, u8 op, u64 duration)
{
struct camu_resource *resource = (struct camu_resource *)userdata;
@@ -293,6 +243,58 @@ static void list_callback(void *userdata, u8 op, struct lia_list_entry *entry, v
}
}
+static bool client_command_callback(void *userdata, struct aki_rpc_connection *conn,
+ struct aki_packet *packet, struct aki_packet *rpacket)
+{
+ struct camu_server *server = (struct camu_server *)userdata;
+ (void)rpacket;
+
+ struct camu_server_client *client = get_client_by_connection(server, conn);
+ if (!client) goto out;
+
+ u8 op = aki_packet_read_u8(packet);
+ switch (op) {
+ case CAMU_CLIENT_CREATE_LIST: {
+ str name;
+ aki_packet_read_str(packet, &name);
+ struct lia_list *list = al_alloc_object(struct lia_list);
+ lia_list_init(list, &name);
+ list->callback = list_callback;
+ list->userdata = server;
+ al_array_push(server->lists, list);
+ break;
+ }
+ case CAMU_CLIENT_TOGGLE_SINK: {
+ str name;
+ aki_packet_read_str(packet, &name);
+ struct camu_server_sink *sink = get_sink_from_name(server, &name);
+ if (!sink) goto out;
+ aki_packet_read_str(packet, &name); // list name.
+ bool enable = aki_packet_read_bool(packet);
+ handle_toggle_sink(server, &name, sink, enable);
+ break;
+ }
+ case CAMU_CLIENT_CREATE_SEARCH: {
+ str module;
+ aki_packet_read_str(packet, &module);
+ str query;
+ aki_packet_read_str(packet, &query);
+ camu_portal_create_search(&server->bridge, &module, &query, client_portal_callback, conn);
+ break;
+ }
+ case CAMU_CLIENT_GET_PAGE: {
+ s32 id = aki_packet_read_s32(packet);
+ u32 num = aki_packet_read_u32(packet);
+ camu_portal_get_page(&server->bridge, id, num, client_portal_callback, conn);
+ break;
+ }
+ }
+
+out:
+ aki_packet_free(packet);
+ return true;
+}
+
static void handle_add_command(struct camu_server *server, struct lia_list *list, struct aki_packet *packet)
{
u8 op = aki_packet_read_u8(packet);
@@ -300,11 +302,13 @@ static void handle_add_command(struct camu_server *server, struct lia_list *list
case CAMU_RESOURCE_FILE: {
str path;
aki_packet_read_str(packet, &path);
+ struct cch_entry *entry = cch_handler_file_create(&path);
+ if (!entry) return;
struct camu_resource_file *resource = al_alloc_object(struct camu_resource_file);
resource->r.type = CAMU_RESOURCE_FILE;
resource->r.load = CAMU_RESOURCE_NOT_LOADED;
al_str_clone(&resource->path, &path);
- resource->r.entry = cch_handler_file_create(&path);
+ resource->r.entry = entry;
resource->r.node = lia_server_create_node(&server->data.server, resource->r.entry);
resource->r.node->callback = node_callback;
resource->r.node->userdata = (struct camu_resource *)resource;
diff --git a/src/sink/desktop.c b/src/sink/desktop.c
index ddde86b..2e2863f 100644
--- a/src/sink/desktop.c
+++ b/src/sink/desktop.c
@@ -116,6 +116,11 @@ static void screen_callback(void *userdata, u8 op, void *opaque)
case CAMU_SCREEN_RESEEK:
camu_sink_reseek(&c->sink);
break;
+ case CAMU_SCREEN_VOLUME: {
+ f64 amount = *(f64 *)opaque;
+ camu_sink_offset_volume(&c->sink, (f32)amount);
+ break;
+ }
case CAMU_SCREEN_CLOSE:
c->should_quit = 1;
break;