/* * Copyright (C) 2024 Mikhail Burakov. This file is part of receiver. * * receiver 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 3 of the License, or * (at your option) any later version. * * receiver 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 receiver. If not, see . */ #include "audio.h" #include #include #include #include #include #include #include #include "atomic_queue.h" #include "toolbox/utils.h" struct AudioContext { struct AtomicQueue queue; struct pw_thread_loop* pw_thread_loop; struct pw_stream* pw_stream; size_t queue_samples_sum; size_t queue_samples_count; }; static void OnStreamProcess(void* data) { struct AudioContext* audio_context = data; struct pw_buffer* pw_buffer = pw_stream_dequeue_buffer(audio_context->pw_stream); if (!pw_buffer) { LOG("Failed to dequeue stream buffer"); return; } static const size_t stride = sizeof(int16_t) * 2; struct spa_data* spa_data = &pw_buffer->buffer->datas[0]; size_t requested = MIN(pw_buffer->requested, spa_data->maxsize / stride) * stride; size_t available = AtomicQueueRead(&audio_context->queue, spa_data->data, requested); if (available < requested) { // LOG("Audio queue underflow (%zu < %zu)!", available, requested); memset((uint8_t*)spa_data->data + available, 0, requested - available); } spa_data->chunk->offset = 0; spa_data->chunk->stride = stride; spa_data->chunk->size = (uint32_t)requested; pw_stream_queue_buffer(audio_context->pw_stream, pw_buffer); return; } struct AudioContext* AudioContextCreate(size_t queue_size) { pw_init(0, NULL); struct AudioContext* audio_context = malloc(sizeof(struct AudioContext)); if (!audio_context) { LOG("Failed to allocate audio context (%s)", strerror(errno)); return NULL; } if (!AtomicQueueCreate(&audio_context->queue, queue_size * sizeof(int16_t) * 2)) { LOG("Failed to create buffer queue (%s)", strerror(errno)); goto rollback_audio_context; } audio_context->pw_thread_loop = pw_thread_loop_new("audio-playback", NULL); if (!audio_context->pw_thread_loop) { LOG("Failed to create pipewire thread loop"); goto rollback_queue; } pw_thread_loop_lock(audio_context->pw_thread_loop); int err = pw_thread_loop_start(audio_context->pw_thread_loop); if (err) { LOG("Failed to start pipewire thread loop (%s)", spa_strerror(err)); pw_thread_loop_unlock(audio_context->pw_thread_loop); goto rollback_thread_loop; } // TOOD(mburakov): Read these from the commandline? struct pw_properties* pw_properties = pw_properties_new( #define _(...) __VA_ARGS__ _(PW_KEY_MEDIA_TYPE, "Audio"), _(PW_KEY_MEDIA_CATEGORY, "Playback"), _(PW_KEY_MEDIA_ROLE, "Game"), _(PW_KEY_NODE_LATENCY, "128/48000"), NULL #undef _ ); if (!pw_properties) { LOG("Failed to create pipewire properties"); pw_thread_loop_unlock(audio_context->pw_thread_loop); goto rollback_thread_loop; } static const struct pw_stream_events kPwStreamEvents = { .version = PW_VERSION_STREAM_EVENTS, .process = OnStreamProcess, }; audio_context->pw_stream = pw_stream_new_simple( pw_thread_loop_get_loop(audio_context->pw_thread_loop), "audio-playback", pw_properties, &kPwStreamEvents, audio_context); if (!audio_context->pw_stream) { LOG("Failed to create pipewire stream"); pw_thread_loop_unlock(audio_context->pw_thread_loop); goto rollback_thread_loop; } uint8_t buffer[1024]; struct spa_pod_builder spa_pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod* params[] = { spa_format_audio_raw_build( &spa_pod_builder, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_S16_LE, .rate = 48000, .channels = 2, .position = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR})), }; static const enum pw_stream_flags kPwStreamFlags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS; if (pw_stream_connect(audio_context->pw_stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, kPwStreamFlags, params, LENGTH(params))) { LOG("Failed to connect pipewire stream"); pw_stream_destroy(audio_context->pw_stream); pw_thread_loop_unlock(audio_context->pw_thread_loop); goto rollback_thread_loop; } audio_context->queue_samples_sum = 0; audio_context->queue_samples_count = 0; pw_thread_loop_unlock(audio_context->pw_thread_loop); return audio_context; rollback_thread_loop: pw_thread_loop_destroy(audio_context->pw_thread_loop); rollback_queue: AtomicQueueDestroy(&audio_context->queue); rollback_audio_context: free(audio_context); pw_deinit(); return NULL; } bool AudioContextDecode(struct AudioContext* audio_context, const void* buffer, size_t size) { if (AtomicQueueWrite(&audio_context->queue, buffer, size) < size) LOG("Audio queue overflow!"); size_t queue_size = atomic_load_explicit(&audio_context->queue.size, memory_order_relaxed); static const size_t stride = sizeof(int16_t) * 2; audio_context->queue_samples_sum += queue_size / stride; audio_context->queue_samples_count++; return true; } uint64_t AudioContextGetLatency(struct AudioContext* audio_context) { size_t queue_latency = 0; if (audio_context->queue_samples_count) { queue_latency = audio_context->queue_samples_sum / audio_context->queue_samples_count; audio_context->queue_samples_sum = 0; audio_context->queue_samples_count = 0; } // TODO(mburakov): This number is extremely optimistic, i.e. Bluetooth delays // are not accounted for. Is it anyhow possible to get this information? return (128 + queue_latency) * 1000000 / 48000; } void AudioContextDestroy(struct AudioContext* audio_context) { pw_thread_loop_lock(audio_context->pw_thread_loop); pw_stream_destroy(audio_context->pw_stream); pw_thread_loop_unlock(audio_context->pw_thread_loop); pw_thread_loop_destroy(audio_context->pw_thread_loop); AtomicQueueDestroy(&audio_context->queue); free(audio_context); pw_deinit(); }