/*
This file is part of GNUnet.
Copyright (C) 2013 GNUnet e.V.
GNUnet is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
GNUnet 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
Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
/**
* @file conversation/gnunet-helper-audio-record.c
* @brief program to record audio data from the microphone
* @author Siomon Dieterle
* @author Andreas Fuchs
* @author Christian Grothoff
*/
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_protocols.h"
#include "conversation.h"
#include "gnunet_constants.h"
#include "gnunet_core_service.h"
#include
#include
#include
#include
#include
#include
#include
#define DEBUG_RECORD_PURE_OGG 1
/**
* Sampling rate
*/
#define SAMPLING_RATE 48000
/**
* How many ms of audio to buffer before encoding them.
* Possible values:
* 60, 40, 20, 10, 5, 2.5
*/
#define FRAME_SIZE_MS 40
/**
* How many samples to buffer before encoding them.
*/
#define FRAME_SIZE (SAMPLING_RATE / 1000 * FRAME_SIZE_MS)
/**
* Pages are commited when their size goes over this value.
* Note that in practice we flush pages VERY often (every frame),
* which means that pages NEVER really get to be this big.
* With one-packet-per-page, pages are roughly 100-300 bytes each.
*
* This value is chosen to make MAX_PAYLOAD_BYTES=1024 fit
* into a single page.
*/
#define PAGE_WATERLINE 800
/**
* Maximum length of opus payload
*/
#define MAX_PAYLOAD_BYTES 1024
/**
* Number of channels
*/
#define CHANNELS 1
/**
* Configures the encoder's expected packet loss percentage.
*
* Higher values will trigger progressively more loss resistant behavior
* in the encoder at the expense of quality at a given bitrate
* in the lossless case, but greater quality under loss.
*/
#define CONV_OPUS_PACKET_LOSS_PERCENTAGE 1
/**
* Configures the encoder's computational complexity.
*
* The supported range is 0-10 inclusive with 10 representing
* the highest complexity.
*/
#define CONV_OPUS_ENCODING_COMPLEXITY 10
/**
* Configures the encoder's use of inband forward error correction (FEC).
*
* Note: This is only applicable to the LPC layer.
*/
#define CONV_OPUS_INBAND_FEC 1
/**
* Configures the type of signal being encoded.
*
* This is a hint which helps the encoder's mode selection.
*
* Possible values:
* OPUS_AUTO - (default) Encoder detects the type automatically.
* OPUS_SIGNAL_VOICE - Bias thresholds towards choosing LPC or Hybrid modes.
* OPUS_SIGNAL_MUSIC - Bias thresholds towards choosing MDCT modes.
*/
#define CONV_OPUS_SIGNAL OPUS_SIGNAL_VOICE
/**
* Coding mode.
*
* Possible values:
* OPUS_APPLICATION_VOIP - gives best quality at a given bitrate for voice
* signals. It enhances the input signal by high-pass filtering and
* emphasizing formants and harmonics. Optionally it includes in-band forward
* error correction to protect against packet loss. Use this mode for typical
* VoIP applications. Because of the enhancement, even at high bitrates
* the output may sound different from the input.
* OPUS_APPLICATION_AUDIO - gives best quality at a given bitrate for most
* non-voice signals like music. Use this mode for music and mixed
* (music/voice) content, broadcast, and applications requiring less than
* 15 ms of coding delay.
* OPUS_APPLICATION_RESTRICTED_LOWDELAY - configures low-delay mode that
* disables the speech-optimized mode in exchange for slightly reduced delay.
* This mode can only be set on an newly initialized or freshly reset encoder
* because it changes the codec delay.
*/
#define CONV_OPUS_APP_TYPE OPUS_APPLICATION_VOIP
/**
* Specification for recording. May change in the future to spec negotiation.
*/
static pa_sample_spec sample_spec = {
.format = PA_SAMPLE_FLOAT32LE,
.rate = SAMPLING_RATE,
.channels = CHANNELS
};
GNUNET_NETWORK_STRUCT_BEGIN
/* OggOpus spec says the numbers must be in little-endian order */
struct OpusHeadPacket
{
uint8_t magic[8];
uint8_t version;
uint8_t channels;
uint16_t preskip GNUNET_PACKED;
uint32_t sampling_rate GNUNET_PACKED;
uint16_t gain GNUNET_PACKED;
uint8_t channel_mapping;
};
struct OpusCommentsPacket
{
uint8_t magic[8];
uint32_t vendor_length;
/* followed by:
char vendor[vendor_length];
uint32_t string_count;
followed by @a string_count pairs of:
uint32_t string_length;
char string[string_length];
*/
};
GNUNET_NETWORK_STRUCT_END
/**
* Pulseaudio mainloop api
*/
static pa_mainloop_api *mainloop_api;
/**
* Pulseaudio mainloop
*/
static pa_mainloop *m;
/**
* Pulseaudio context
*/
static pa_context *context;
/**
* Pulseaudio recording stream
*/
static pa_stream *stream_in;
/**
* Pulseaudio io events
*/
static pa_io_event *stdio_event;
/**
* OPUS encoder
*/
static OpusEncoder *enc;
/**
* Buffer for encoded data
*/
static unsigned char *opus_data;
/**
* PCM data buffer for one OPUS frame
*/
static float *pcm_buffer;
/**
* Length of the pcm data needed for one OPUS frame
*/
static int pcm_length;
/**
* Audio buffer
*/
static char *transmit_buffer;
/**
* Length of audio buffer
*/
static size_t transmit_buffer_length;
/**
* Read index for transmit buffer
*/
static size_t transmit_buffer_index;
/**
* Audio message skeleton
*/
static struct AudioMessage *audio_message;
/**
* Ogg muxer state
*/
static ogg_stream_state os;
/**
* Ogg packet id
*/
static int32_t packet_id;
/**
* Ogg granule for current packet
*/
static int64_t enc_granulepos;
#ifdef DEBUG_RECORD_PURE_OGG
/**
* 1 to not to write GNUnet message headers,
* producing pure playable ogg output
*/
static int dump_pure_ogg;
#endif
/**
* Pulseaudio shutdown task
*/
static void
quit (int ret)
{
mainloop_api->quit (mainloop_api,
ret);
exit (ret);
}
static void
write_data (const char *ptr,
size_t msg_size)
{
ssize_t ret;
size_t off;
off = 0;
while (off < msg_size)
{
ret = write (STDOUT_FILENO,
&ptr[off],
msg_size - off);
if (0 >= ret)
{
if (-1 == ret)
GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
"write");
quit (2);
}
off += ret;
}
}
static void
write_page (ogg_page *og)
{
static unsigned long long toff;
size_t msg_size;
msg_size = sizeof (struct AudioMessage) + og->header_len + og->body_len;
audio_message->header.size = htons ((uint16_t) msg_size);
GNUNET_memcpy (&audio_message[1], og->header, og->header_len);
GNUNET_memcpy (((char *) &audio_message[1]) + og->header_len, og->body, og->body_len);
toff += msg_size;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Sending %u bytes of audio data (total: %llu)\n",
(unsigned int) msg_size,
toff);
#ifdef DEBUG_RECORD_PURE_OGG
if (dump_pure_ogg)
write_data ((const char *) &audio_message[1],
og->header_len + og->body_len);
else
#endif
write_data ((const char *) audio_message,
msg_size);
}
/**
* Creates OPUS packets from PCM data
*/
static void
packetizer ()
{
char *nbuf;
size_t new_size;
int32_t len;
ogg_packet op;
ogg_page og;
while (transmit_buffer_length >= transmit_buffer_index + pcm_length)
{
GNUNET_memcpy (pcm_buffer,
&transmit_buffer[transmit_buffer_index],
pcm_length);
transmit_buffer_index += pcm_length;
len =
opus_encode_float (enc, pcm_buffer, FRAME_SIZE, opus_data,
MAX_PAYLOAD_BYTES);
if (len < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("opus_encode_float() failed: %s. Aborting\n"),
opus_strerror (len));
quit (5);
}
if (((uint32_t)len) > UINT16_MAX - sizeof (struct AudioMessage))
{
GNUNET_break (0);
continue;
}
/* As per OggOpus spec, granule is calculated as if the audio
had 48kHz sampling rate. */
enc_granulepos += FRAME_SIZE * 48000 / SAMPLING_RATE;
op.packet = (unsigned char *) opus_data;
op.bytes = len;
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = enc_granulepos;
op.packetno = packet_id++;
ogg_stream_packetin (&os, &op);
while (ogg_stream_flush_fill (&os, &og, PAGE_WATERLINE))
{
if ( ((unsigned long long) og.header_len) +
((unsigned long long) og.body_len) >
UINT16_MAX - sizeof (struct AudioMessage))
{
GNUNET_assert (0);
continue;
}
write_page (&og);
}
}
new_size = transmit_buffer_length - transmit_buffer_index;
if (0 != new_size)
{
nbuf = pa_xmalloc (new_size);
memmove (nbuf,
&transmit_buffer[transmit_buffer_index],
new_size);
pa_xfree (transmit_buffer);
transmit_buffer = nbuf;
}
else
{
pa_xfree (transmit_buffer);
transmit_buffer = NULL;
}
transmit_buffer_index = 0;
transmit_buffer_length = new_size;
}
/**
* Pulseaudio callback when new data is available.
*/
static void
stream_read_callback (pa_stream * s,
size_t length,
void *userdata)
{
const void *data;
(void) userdata;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Got %u/%d bytes of PCM data\n",
(unsigned int) length,
pcm_length);
GNUNET_assert (NULL != s);
GNUNET_assert (length > 0);
if (stdio_event)
mainloop_api->io_enable (stdio_event, PA_IO_EVENT_OUTPUT);
if (pa_stream_peek (s, (const void **) &data, &length) < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_stream_peek() failed: %s\n"),
pa_strerror (pa_context_errno (context)));
quit (1);
return;
}
GNUNET_assert (NULL != data);
GNUNET_assert (length > 0);
if (NULL != transmit_buffer)
{
transmit_buffer = pa_xrealloc (transmit_buffer,
transmit_buffer_length + length);
GNUNET_memcpy (&transmit_buffer[transmit_buffer_length],
data,
length);
transmit_buffer_length += length;
}
else
{
transmit_buffer = pa_xmalloc (length);
GNUNET_memcpy (transmit_buffer, data, length);
transmit_buffer_length = length;
transmit_buffer_index = 0;
}
pa_stream_drop (s);
packetizer ();
}
/**
* Exit callback for SIGTERM and SIGINT
*/
static void
exit_signal_callback (pa_mainloop_api * m,
pa_signal_event * e,
int sig,
void *userdata)
{
(void) m;
(void) e;
(void) sig;
(void) userdata;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Got signal, exiting.\n"));
quit (1);
}
/**
* Pulseaudio stream state callback
*/
static void
stream_state_callback (pa_stream * s,
void *userdata)
{
(void) userdata;
GNUNET_assert (NULL != s);
switch (pa_stream_get_state (s))
{
case PA_STREAM_CREATING:
case PA_STREAM_TERMINATED:
break;
case PA_STREAM_READY:
{
const pa_buffer_attr *a;
char cmt[PA_CHANNEL_MAP_SNPRINT_MAX];
char sst[PA_SAMPLE_SPEC_SNPRINT_MAX];
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Stream successfully created.\n"));
if (!(a = pa_stream_get_buffer_attr (s)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_stream_get_buffer_attr() failed: %s\n"),
pa_strerror (pa_context_errno
(pa_stream_get_context (s))));
}
else
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Buffer metrics: maxlength=%u, fragsize=%u\n"),
a->maxlength, a->fragsize);
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Using sample spec '%s', channel map '%s'.\n"),
pa_sample_spec_snprint (sst, sizeof (sst),
pa_stream_get_sample_spec (s)),
pa_channel_map_snprint (cmt, sizeof (cmt),
pa_stream_get_channel_map (s)));
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Connected to device %s (%u, %ssuspended).\n"),
pa_stream_get_device_name (s),
pa_stream_get_device_index (s),
pa_stream_is_suspended (s) ? "" : "not ");
}
break;
case PA_STREAM_FAILED:
default:
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Stream error: %s\n"),
pa_strerror (pa_context_errno (pa_stream_get_context (s))));
quit (1);
}
}
/**
* Pulseaudio context state callback
*/
static void
context_state_callback (pa_context * c,
void *userdata)
{
(void) userdata;
GNUNET_assert (c);
switch (pa_context_get_state (c))
{
case PA_CONTEXT_CONNECTING:
case PA_CONTEXT_AUTHORIZING:
case PA_CONTEXT_SETTING_NAME:
break;
case PA_CONTEXT_READY:
{
int r;
pa_buffer_attr na;
GNUNET_assert (!stream_in);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
_("Connection established.\n"));
if (! (stream_in =
pa_stream_new (c, "GNUNET_VoIP recorder", &sample_spec, NULL)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_stream_new() failed: %s\n"),
pa_strerror (pa_context_errno (c)));
goto fail;
}
pa_stream_set_state_callback (stream_in, &stream_state_callback, NULL);
pa_stream_set_read_callback (stream_in, &stream_read_callback, NULL);
memset (&na, 0, sizeof (na));
na.maxlength = UINT32_MAX;
na.fragsize = pcm_length;
if ((r = pa_stream_connect_record (stream_in, NULL, &na,
PA_STREAM_ADJUST_LATENCY)) < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_stream_connect_record() failed: %s\n"),
pa_strerror (pa_context_errno (c)));
goto fail;
}
break;
}
case PA_CONTEXT_TERMINATED:
quit (0);
break;
case PA_CONTEXT_FAILED:
default:
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Connection failure: %s\n"),
pa_strerror (pa_context_errno (c)));
goto fail;
}
return;
fail:
quit (1);
}
/**
* Pulsaudio init
*/
static void
pa_init ()
{
int r;
int i;
if (!pa_sample_spec_valid (&sample_spec))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Wrong Spec\n"));
}
/* set up main record loop */
if (!(m = pa_mainloop_new ()))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_mainloop_new() failed.\n"));
}
mainloop_api = pa_mainloop_get_api (m);
/* listen to signals */
r = pa_signal_init (mainloop_api);
GNUNET_assert (r == 0);
pa_signal_new (SIGINT, &exit_signal_callback, NULL);
pa_signal_new (SIGTERM, &exit_signal_callback, NULL);
/* connect to the main pulseaudio context */
if (!(context = pa_context_new (mainloop_api, "GNUNET VoIP")))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_context_new() failed.\n"));
}
pa_context_set_state_callback (context, &context_state_callback, NULL);
if (pa_context_connect (context, NULL, 0, NULL) < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_context_connect() failed: %s\n"),
pa_strerror (pa_context_errno (context)));
}
if (pa_mainloop_run (m, &i) < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("pa_mainloop_run() failed.\n"));
}
}
/**
* OPUS init
*/
static void
opus_init ()
{
int err;
pcm_length = FRAME_SIZE * CHANNELS * sizeof (float);
pcm_buffer = pa_xmalloc (pcm_length);
opus_data = GNUNET_malloc (MAX_PAYLOAD_BYTES);
enc = opus_encoder_create (SAMPLING_RATE,
CHANNELS,
CONV_OPUS_APP_TYPE,
&err);
opus_encoder_ctl (enc,
OPUS_SET_PACKET_LOSS_PERC (CONV_OPUS_PACKET_LOSS_PERCENTAGE));
opus_encoder_ctl (enc,
OPUS_SET_COMPLEXITY (CONV_OPUS_ENCODING_COMPLEXITY));
opus_encoder_ctl (enc,
OPUS_SET_INBAND_FEC (CONV_OPUS_INBAND_FEC));
opus_encoder_ctl (enc,
OPUS_SET_SIGNAL (CONV_OPUS_SIGNAL));
}
static void
ogg_init ()
{
int serialno;
struct OpusHeadPacket headpacket;
struct OpusCommentsPacket *commentspacket;
size_t commentspacket_len;
serialno = GNUNET_CRYPTO_random_u32 (GNUNET_CRYPTO_QUALITY_STRONG,
0x7FFFFFFF);
/*Initialize Ogg stream struct*/
if (-1 == ogg_stream_init (&os, serialno))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("ogg_stream_init() failed.\n"));
exit (3);
}
packet_id = 0;
/*Write header*/
{
ogg_packet op;
ogg_page og;
const char *opusver;
int vendor_length;
GNUNET_memcpy (headpacket.magic, "OpusHead", 8);
headpacket.version = 1;
headpacket.channels = CHANNELS;
headpacket.preskip = GNUNET_htole16 (0);
headpacket.sampling_rate = GNUNET_htole32 (SAMPLING_RATE);
headpacket.gain = GNUNET_htole16 (0);
headpacket.channel_mapping = 0; /* Mono or stereo */
op.packet = (unsigned char *) &headpacket;
op.bytes = sizeof (headpacket);
op.b_o_s = 1;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = packet_id++;
ogg_stream_packetin (&os, &op);
/* Head packet must be alone on its page */
while (ogg_stream_flush (&os, &og))
{
write_page (&og);
}
commentspacket_len = sizeof (*commentspacket);
opusver = opus_get_version_string ();
vendor_length = strlen (opusver);
commentspacket_len += vendor_length;
commentspacket_len += sizeof (uint32_t);
commentspacket = (struct OpusCommentsPacket *) malloc (commentspacket_len);
if (NULL == commentspacket)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
_("Failed to allocate %u bytes for second packet\n"),
(unsigned int) commentspacket_len);
exit (5);
}
GNUNET_memcpy (commentspacket->magic, "OpusTags", 8);
commentspacket->vendor_length = GNUNET_htole32 (vendor_length);
GNUNET_memcpy (&commentspacket[1], opusver, vendor_length);
*(uint32_t *) &((char *) &commentspacket[1])[vendor_length] = \
GNUNET_htole32 (0); /* no tags */
op.packet = (unsigned char *) commentspacket;
op.bytes = commentspacket_len;
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = packet_id++;
ogg_stream_packetin (&os, &op);
/* Comment packets must not be mixed with audio packets on their pages */
while (ogg_stream_flush (&os, &og))
{
write_page (&og);
}
free (commentspacket);
}
}
/**
* The main function for the record helper.
*
* @param argc number of arguments from the command line
* @param argv command line arguments
* @return 0 ok, 1 on error
*/
int
main (int argc,
char *argv[])
{
(void) argc;
(void) argv;
GNUNET_assert (GNUNET_OK ==
GNUNET_log_setup ("gnunet-helper-audio-record",
"WARNING",
NULL));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Audio source starts\n");
audio_message = GNUNET_malloc (UINT16_MAX);
audio_message->header.type = htons (GNUNET_MESSAGE_TYPE_CONVERSATION_AUDIO);
#ifdef DEBUG_RECORD_PURE_OGG
dump_pure_ogg = getenv ("GNUNET_RECORD_PURE_OGG") ? 1 : 0;
#endif
ogg_init ();
opus_init ();
pa_init ();
return 0;
}