Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TargetBridge-Receiver/TBReceiverC/src/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ struct tb_display *tb_disp_create(int fullscreen) {
* anisotropic where supported. Must be set BEFORE renderer creation. */
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");

if (SDL_Init(SDL_INIT_VIDEO) < 0) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
fprintf(stderr, "[disp] SDL_Init: %s\n", SDL_GetError());
return NULL;
}
Expand Down
102 changes: 98 additions & 4 deletions TargetBridge-Receiver/TBReceiverC/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include <time.h>
#include <unistd.h>

#define AUDIO_BUF_CAP (192000) // 1 second buffer of 48000Hz stereo 16-bit PCM

struct app {
struct tb_display *disp;
struct tb_decoder *dec;
Expand All @@ -53,6 +55,13 @@ struct app {

DNSServiceRef bonjour_ref;
char bonjour_name[128];

SDL_AudioDeviceID audio_device;

uint8_t audio_buf[AUDIO_BUF_CAP];
int audio_buf_head;
int audio_buf_tail;
int audio_buf_size;
};

static volatile sig_atomic_t g_term = 0;
Expand Down Expand Up @@ -221,6 +230,29 @@ static void on_frame(const uint8_t *y, int y_stride,
a->frames++;
}

static void ring_read(struct app *a, Uint8 *dst, int len) {
int first = AUDIO_BUF_CAP - a->audio_buf_tail;
if (first >= len) {
memcpy(dst, a->audio_buf + a->audio_buf_tail, len);
} else {
memcpy(dst, a->audio_buf + a->audio_buf_tail, first);
memcpy(dst + first, a->audio_buf, len - first);
}
a->audio_buf_tail = (a->audio_buf_tail + len) % AUDIO_BUF_CAP;
a->audio_buf_size -= len;
}

static void audio_callback(void *userdata, Uint8 *stream, int len) {
struct app *a = (struct app *)userdata;
if (a->audio_buf_size >= len) {
ring_read(a, stream, len);
} else {
int available = a->audio_buf_size;
if (available > 0) ring_read(a, stream, available);
memset(stream + available, 0, len - available);
}
}

/* ---- Callbacks: parser → decoder ------------------------------------- */

static void on_packet(uint8_t type, const uint8_t *payload, size_t len, void *ud) {
Expand Down Expand Up @@ -290,6 +322,35 @@ static void on_packet(uint8_t type, const uint8_t *payload, size_t len, void *ud
tb_disp_set_cursor(a->disp, x, y, w, h, visible, type);
}
break;
case TB_PKT_AUDIO_FRAME:
if (a->audio_device != 0) {
SDL_LockAudioDevice(a->audio_device);

// Limit audio backlog to 150ms (150 * 192 = 28800 bytes) to cushion against network/scheduling jitter.
// If the buffer would exceed this, smoothly discard the oldest excess bytes.
const int cap_bytes = 28800;
if (a->audio_buf_size + len > cap_bytes) {
int excess = (a->audio_buf_size + len) - cap_bytes;
a->audio_buf_tail = (a->audio_buf_tail + excess) % AUDIO_BUF_CAP;
a->audio_buf_size -= excess;
}

// Write payload to circular buffer
if (a->audio_buf_size + (int)len <= AUDIO_BUF_CAP) {
int first = AUDIO_BUF_CAP - a->audio_buf_head;
if (first >= (int)len) {
memcpy(a->audio_buf + a->audio_buf_head, payload, len);
} else {
memcpy(a->audio_buf + a->audio_buf_head, payload, first);
memcpy(a->audio_buf, payload + first, len - first);
}
a->audio_buf_head = (a->audio_buf_head + (int)len) % AUDIO_BUF_CAP;
a->audio_buf_size += (int)len;
}

SDL_UnlockAudioDevice(a->audio_device);
}
break;
case TB_PKT_HEARTBEAT:
break;
case TB_PKT_TEST_DATA:
Expand All @@ -308,11 +369,13 @@ static void on_packet(uint8_t type, const uint8_t *payload, size_t len, void *ud

/* ---- Networking helpers ---------------------------------------------- */

static int drain_socket(struct app *a) {
static int drain_socket(struct app *a, int *bytes_read) {
uint8_t buf[1024 * 1024];
if (bytes_read) *bytes_read = 0;
for (;;) {
ssize_t n = read(a->client_fd, buf, sizeof(buf));
if (n > 0) {
if (bytes_read) *bytes_read += n;
if (tb_parser_feed(&a->parser, buf, (size_t)n) < 0) return -1;
} else if (n == 0) {
return -1; /* peer closed */
Expand Down Expand Up @@ -422,6 +485,14 @@ static void close_client(struct app *a) {
tb_parser_free(&a->parser);
tb_parser_init(&a->parser, on_packet, a);
tb_dec_reset(a->dec); /* fresh decoder for next session */
if (a->audio_device != 0) {
SDL_LockAudioDevice(a->audio_device);
a->audio_buf_head = 0;
a->audio_buf_tail = 0;
a->audio_buf_size = 0;
SDL_UnlockAudioDevice(a->audio_device);
}

fprintf(stderr, "[main] client disconnected\n");
}

Expand Down Expand Up @@ -464,6 +535,24 @@ int main(int argc, char **argv) {
a.disp = tb_disp_create(fullscreen);
if (!a.disp) { fprintf(stderr, "tb_disp_create failed\n"); return 1; }

/* Open SDL Audio Device */
SDL_AudioSpec spec;
SDL_zero(spec);
spec.freq = 48000;
spec.format = AUDIO_S16LSB; // 16-bit signed, little-endian PCM
spec.channels = 2; // Stereo
spec.samples = 1024; // Buffer size (approx 21.3ms)
spec.callback = audio_callback;
spec.userdata = &a;
SDL_AudioSpec obtained;
a.audio_device = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained, 0);
if (a.audio_device != 0) {
SDL_PauseAudioDevice(a.audio_device, 0); // Start playing (unpaused)
fprintf(stderr, "[main] SDL audio device opened: 48000Hz stereo 16-bit PCM (obtained %d samples)\n", obtained.samples);
} else {
fprintf(stderr, "[main] warning: SDL_OpenAudioDevice failed: %s\n", SDL_GetError());
}

struct tb_display_info boot_info;
if (tb_disp_get_info(a.disp, &boot_info) == 0) {
snprintf(a.panel_text, sizeof(a.panel_text), "%u x %u px (%s)",
Expand All @@ -486,6 +575,7 @@ int main(int argc, char **argv) {

while (!g_term && !tb_disp_poll_quit(a.disp)) {
uint64_t t = now_ms();
int bytes_read = 0;

if (t - a.last_ip_check_ms >= 1000) {
char refreshed_ip[64] = {0};
Expand All @@ -511,7 +601,7 @@ int main(int argc, char **argv) {
send_receiver_info(&a);
}
} else {
if (drain_socket(&a) < 0) close_client(&a);
if (drain_socket(&a, &bytes_read) < 0) close_client(&a);
else if (a.close_requested) close_client(&a);
}

Expand All @@ -528,8 +618,9 @@ int main(int argc, char **argv) {
}

/* Yield only while idle. During active video, keep draining and
* rendering without injecting an extra millisecond of latency. */
if (a.client_fd < 0 || !a.have_video_frame) {
* rendering without injecting an extra millisecond of latency.
* If we didn't read any bytes from the socket, we can safely yield 1ms. */
if (a.client_fd < 0 || !a.have_video_frame || bytes_read == 0) {
SDL_Delay(1);
}
}
Expand All @@ -539,6 +630,9 @@ int main(int argc, char **argv) {
bonjour_deinit(&a);
tb_parser_free(&a.parser);
tb_dec_destroy(a.dec);
if (a.audio_device != 0) {
SDL_CloseAudioDevice(a.audio_device);
}
tb_disp_destroy(a.disp);
fprintf(stderr, "[main] bye\n");
return 0;
Expand Down
1 change: 1 addition & 0 deletions TargetBridge-Receiver/TBReceiverC/src/proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#define TB_PKT_CREATE_SESSION_ACK 0x12
#define TB_PKT_PARAM_SETS 0x20
#define TB_PKT_FRAME 0x21
#define TB_PKT_AUDIO_FRAME 0x23
#define TB_PKT_HEARTBEAT 0x30
#define TB_PKT_TEARDOWN 0x31
#define TB_PKT_CURSOR 0x32
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ struct TBDisplaySenderContentView: View {

Toggle(TBDisplaySenderL10n.largeCursor(service.language), isOn: $service.largeCursor)
.disabled(service.anyConnected)

Toggle(TBDisplaySenderL10n.streamAudio(service.language), isOn: $service.audioEnabled)
.disabled(service.anyConnected)
}
}
}
Expand Down Expand Up @@ -266,6 +269,11 @@ private struct TBDisplaySenderSessionCard: View {
.disabled(session.isConnected || session.isStreaming)
}

controlRow(TBDisplaySenderL10n.streamAudio(service.language)) {
Toggle("", isOn: $session.audioEnabled)
.labelsHidden()
}

VStack(alignment: .leading, spacing: 4) {
Text(TBDisplaySenderL10n.streamHint1(service.language))
Text(TBDisplaySenderL10n.streamHint2(service.language))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,14 @@ enum TBDisplaySenderL10n {
}
}

static func streamAudio(_ language: TBDisplaySenderLanguage) -> String {
switch language {
case .italian: return "Trasmetti audio del Mac"
case .english: return "Stream Mac audio"
case .german: return "Mac-Audio übertragen"
}
}

static func showMainWindow(_ language: TBDisplaySenderLanguage) -> String {
switch language {
case .italian: return "Mostra finestra principale"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ final class TBDisplaySenderService: ObservableObject {
objectWillChange.send()
}
}
@Published var audioEnabled: Bool = UserDefaults.standard.object(forKey: "fd.tbdisplaysender.audioEnabled") as? Bool ?? true {
didSet {
UserDefaults.standard.set(audioEnabled, forKey: "fd.tbdisplaysender.audioEnabled")
objectWillChange.send()
}
}

private var sessionCancellables: [UUID: AnyCancellable] = [:]
private let receiverDiscovery = TBReceiverDiscovery()
Expand Down Expand Up @@ -70,7 +76,7 @@ final class TBDisplaySenderService: ObservableObject {
}

func addSession() {
let session = TBDisplaySenderSession(language: language, largeCursor: largeCursor)
let session = TBDisplaySenderSession(language: language, largeCursor: largeCursor, audioEnabled: audioEnabled)
if let previous = sessions.last {
session.capturePreset = previous.capturePreset
session.captureSource = previous.captureSource
Expand Down
Loading