Files
ustreamer/janus/src/audio.c
Maxim Devaev d4a9862a18 webrtc audio
2022-06-01 07:57:34 +03:00

250 lines
7.8 KiB
C

/*****************************************************************************
# #
# uStreamer - Lightweight and fast MJPEG-HTTP streamer. #
# #
# This source file is partially based on this code: #
# - https://github.com/catid/kvm/blob/master/kvm_pipeline/src #
# #
# Copyright (C) 2018-2022 Maxim Devaev <mdevaev@gmail.com> #
# #
# 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 3 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, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
#include "audio.h"
#define JLOG_PERROR_ALSA(_err, _prefix, _msg, ...) JLOG_ERROR(_prefix, _msg ": %s", ##__VA_ARGS__, snd_strerror(_err))
#define JLOG_PERROR_OPUS(_err, _prefix, _msg, ...) JLOG_ERROR(_prefix, _msg ": %s", ##__VA_ARGS__, opus_strerror(_err))
// https://github.com/xiph/opus/blob/7b05f44/src/opus_demo.c#L368
#define PCM_BITRATE 48000
#define PCM_FRAMES (6 * PCM_BITRATE / 50) // 120ms
#define PCM_DATA_SIZE (PCM_FRAMES * 2)
#define RAW_DATA_SIZE (PCM_DATA_SIZE * sizeof(opus_int16))
typedef struct {
opus_int16 data[PCM_DATA_SIZE];
} _pcm_buf_s;
typedef struct {
uint8_t data[RAW_DATA_SIZE]; // Worst
size_t used;
uint64_t pts;
} _enc_buf_s;
static void *_pcm_thread(void *v_audio);
static void *_encoder_thread(void *v_audio);
audio_s *audio_init(const char *name) {
audio_s *audio;
A_CALLOC(audio, 1);
audio->pcm_queue = queue_init(8);
audio->enc_queue = queue_init(8);
atomic_init(&audio->run, true);
{
int err;
if ((err = snd_pcm_open(&audio->pcm, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
audio->pcm = NULL;
JLOG_PERROR_ALSA(err, "audio", "Can't open PCM capture");
goto error;
}
assert(!snd_pcm_hw_params_malloc(&audio->pcm_params));
# define SET_PARAM(_msg, _func, ...) { \
if ((err = _func(audio->pcm, audio->pcm_params, ##__VA_ARGS__)) < 0) { \
JLOG_PERROR_ALSA(err, "audio", _msg); \
goto error; \
} \
}
SET_PARAM("Can't initialize PCM params", snd_pcm_hw_params_any);
SET_PARAM("Can't set PCM access type", snd_pcm_hw_params_set_access, SND_PCM_ACCESS_RW_INTERLEAVED);
SET_PARAM("Can't set PCM channels numbre", snd_pcm_hw_params_set_channels, 2);
SET_PARAM("Can't set PCM sampling format", snd_pcm_hw_params_set_format, SND_PCM_FORMAT_S16_LE);
unsigned pcm_bitrate = PCM_BITRATE;
SET_PARAM("Can't set PCM sampling rate", snd_pcm_hw_params_set_rate_near, &pcm_bitrate, 0);
if (pcm_bitrate != PCM_BITRATE) {
JLOG_ERROR("audio", "PCM bitrate mismatch: %u, should be %u", pcm_bitrate, PCM_BITRATE);
goto error;
}
SET_PARAM("Can't apply PCM params", snd_pcm_hw_params);
# undef SET_PARAM
}
{
int err;
// OPUS_APPLICATION_VOIP
// OPUS_APPLICATION_RESTRICTED_LOWDELAY
audio->enc = opus_encoder_create(PCM_BITRATE, 2, OPUS_APPLICATION_AUDIO, &err);
if (err < 0) {
audio->enc = NULL;
JLOG_PERROR_OPUS(err, "audio", "Can't create OPUS encoder");
goto error;
}
# define SET_PARAM(_msg, _ctl) { \
if ((err = opus_encoder_ctl(audio->enc, _ctl)) < 0) { \
JLOG_PERROR_OPUS(err, "audio", _msg); \
goto error; \
} \
}
SET_PARAM("Can't set OPUS bitrate", OPUS_SET_BITRATE(48000));
SET_PARAM("Can't set OPUS max bandwidth", OPUS_SET_MAX_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND));
SET_PARAM("Can't set OPUS signal type", OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC));
// Also see rtpa.c
// SET_PARAM("Can't set OPUS FEC", OPUS_SET_INBAND_FEC(1));
// SET_PARAM("Can't set OPUS exploss", OPUS_SET_PACKET_LOSS_PERC(10));
# undef SET_PARAM
}
JLOG_INFO("audio", "PCM & OPUS prepared; capturing ...");
audio->tids_created = true;
A_THREAD_CREATE(&audio->enc_tid, _encoder_thread, audio);
A_THREAD_CREATE(&audio->pcm_tid, _pcm_thread, audio);
return audio;
error:
audio_destroy(audio);
return NULL;
}
void audio_destroy(audio_s *audio) {
if (audio->tids_created) {
atomic_store(&audio->run, false);
A_THREAD_JOIN(audio->pcm_tid);
A_THREAD_JOIN(audio->enc_tid);
}
if (audio->enc) {
opus_encoder_destroy(audio->enc);
}
if (audio->pcm) {
snd_pcm_close(audio->pcm);
}
if (audio->pcm_params) {
snd_pcm_hw_params_free(audio->pcm_params);
}
# define FREE_QUEUE(_suffix) { \
while (!queue_get_free(audio->_suffix##_queue)) { \
_##_suffix##_buf_s *ptr; \
assert(!queue_get(audio->_suffix##_queue, (void **)&ptr, 1)); \
free(ptr); \
} \
queue_destroy(audio->_suffix##_queue); \
}
FREE_QUEUE(enc);
FREE_QUEUE(pcm);
# undef FREE_QUEUE
if (audio->tids_created) {
JLOG_INFO("audio", "Pipeline closed");
}
free(audio);
}
int audio_copy_encoded(audio_s *audio, uint8_t *data, size_t *size, uint64_t *pts) {
if (!atomic_load(&audio->run)) {
return -1;
}
_enc_buf_s *in;
if (!queue_get(audio->enc_queue, (void **)&in, 1)) {
if (*size < in->used) {
free(in);
return -3;
}
memcpy(data, in->data, in->used);
*size = in->used;
*pts = in->pts;
free(in);
return 0;
}
return -2;
}
static void *_pcm_thread(void *v_audio) {
A_THREAD_RENAME("us_a_pcm");
audio_s *audio = (audio_s *)v_audio;
while (atomic_load(&audio->run)) {
uint8_t in[RAW_DATA_SIZE];
int frames = snd_pcm_readi(audio->pcm, in, PCM_FRAMES);
if (frames < 0) {
JLOG_PERROR_ALSA(frames, "audio", "Can't capture PCM frames; breaking audio ...");
break;
} else if (frames < PCM_FRAMES) {
JLOG_ERROR("audio", "Too few PCM frames captured; breaking audio ...");
break;
}
if (queue_get_free(audio->pcm_queue)) {
_pcm_buf_s *out;
A_CALLOC(out, 1);
/*for (unsigned index = 0; index < RAW_DATA_SIZE; ++index) {
out->data[index] = (opus_int16)in[index * 2 + 1] << 8 | in[index * 2];
}*/
memcpy(out->data, in, RAW_DATA_SIZE);
assert(!queue_put(audio->pcm_queue, out, 1));
} else {
JLOG_ERROR("audio", "PCM queue is full");
}
}
atomic_store(&audio->run, false);
return NULL;
}
static void *_encoder_thread(void *v_audio) {
A_THREAD_RENAME("us_a_enc");
audio_s *audio = (audio_s *)v_audio;
while (atomic_load(&audio->run)) {
_pcm_buf_s *in;
if (!queue_get(audio->pcm_queue, (void **)&in, 1)) {
_enc_buf_s *out;
A_CALLOC(out, 1);
int size = opus_encode(audio->enc, in->data, PCM_FRAMES, out->data, RAW_DATA_SIZE);
free(in);
if (size < 0) {
JLOG_PERROR_OPUS(size, "audio", "Can't encode PCM frame to OPUS; breaking audio ...");
free(out);
break;
}
out->used = size;
out->pts = audio->pts;
audio->pts += PCM_FRAMES;
if (queue_get_free(audio->enc_queue)) {
assert(!queue_put(audio->enc_queue, out, 1));
} else {
JLOG_ERROR("audio", "OPUS encoder queue is full");
free(out);
}
}
}
atomic_store(&audio->run, false);
return NULL;
}