mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-26 02:00:33 +00:00
7729f4ce81
Otherwise the CAPS event will be dropped and we never configure any caps at all, leading to weird behaviour in many situations. Especially header rewriting is not going to work if a capsfilter is after wavenc. https://bugzilla.gnome.org/show_bug.cgi?id=737735
1073 lines
30 KiB
C
1073 lines
30 KiB
C
/* -*- mOde: C; tab-width: 2; indent-tabs-mode: t; c-basic-offset: 2 -*- */
|
|
/* GStreamer .wav encoder
|
|
* Copyright (C) <2002> Iain Holmes <iain@prettypeople.org>
|
|
* Copyright (C) <2006> Tim-Philipp Müller <tim centricular net>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
/**
|
|
* SECTION:element-wavenc
|
|
*
|
|
* Format an audio stream into the wav format.
|
|
*
|
|
* <refsect2>
|
|
* <title>Example launch line</title>
|
|
* |[
|
|
* gst-launch-1.0 cdparanoiasrc mode=continuous ! queue ! audioconvert ! wavenc ! filesink location=cd.wav
|
|
* ]| Rip a whole audio CD into a single wav file, with the track table written into a CUE sheet inside the file
|
|
* |[
|
|
* gst-launch-1.0 cdparanoiasrc track=5 ! queue ! audioconvert ! wavenc ! filesink location=track5.wav
|
|
* ]| Rip track 5 of an audio CD into a single wav file containing unencoded raw audio samples.
|
|
* </refsect2>
|
|
*
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <string.h>
|
|
#include "gstwavenc.h"
|
|
|
|
#include <gst/audio/audio.h>
|
|
#include <gst/riff/riff-media.h>
|
|
#include <gst/base/gstbytewriter.h>
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (wavenc_debug);
|
|
#define GST_CAT_DEFAULT wavenc_debug
|
|
|
|
typedef struct
|
|
{
|
|
/* Offset Size Description Value
|
|
* 0x00 4 ID unique identification value
|
|
* 0x04 4 Position play order position
|
|
* 0x08 4 Data Chunk ID RIFF ID of corresponding data chunk
|
|
* 0x0c 4 Chunk Start Byte Offset of Data Chunk *
|
|
* 0x10 4 Block Start Byte Offset to sample of First Channel
|
|
* 0x14 4 Sample Offset Byte Offset to sample byte of First Channel
|
|
*/
|
|
guint32 id;
|
|
guint32 position;
|
|
guint8 data_chunk_id[4];
|
|
guint32 chunk_start;
|
|
guint32 block_start;
|
|
guint32 sample_offset;
|
|
} GstWavEncCue;
|
|
|
|
typedef struct
|
|
{
|
|
/* Offset Size Description Value
|
|
* 0x00 4 Chunk ID "labl" (0x6C61626C) or "note" (0x6E6F7465)
|
|
* 0x04 4 Chunk Data Size depends on contained text
|
|
* 0x08 4 Cue Point ID 0 - 0xFFFFFFFF
|
|
* 0x0c Text
|
|
*/
|
|
guint8 chunk_id[4];
|
|
guint32 chunk_data_size;
|
|
guint32 cue_point_id;
|
|
gchar *text;
|
|
} GstWavEncLabl, GstWavEncNote;
|
|
|
|
/* FIXME: mono doesn't produce correct files it seems, at least mplayer xruns */
|
|
#define SINK_CAPS \
|
|
"audio/x-raw, " \
|
|
"rate = (int) [ 1, MAX ], " \
|
|
"channels = (int) [ 1, 65535 ], " \
|
|
"format = (string) { S32LE, S24LE, S16LE, U8, F32LE, F64LE }, " \
|
|
"layout = (string) interleaved" \
|
|
"; " \
|
|
"audio/x-alaw, " \
|
|
"rate = (int) [ 8000, 192000 ], " \
|
|
"channels = (int) [ 1, 2 ]; " \
|
|
"audio/x-mulaw, " \
|
|
"rate = (int) [ 8000, 192000 ], " \
|
|
"channels = (int) [ 1, 2 ]"
|
|
|
|
|
|
static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
|
|
GST_PAD_SINK,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS (SINK_CAPS)
|
|
);
|
|
|
|
static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
|
|
GST_PAD_SRC,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS ("audio/x-wav")
|
|
);
|
|
|
|
#define gst_wavenc_parent_class parent_class
|
|
G_DEFINE_TYPE_WITH_CODE (GstWavEnc, gst_wavenc, GST_TYPE_ELEMENT,
|
|
G_IMPLEMENT_INTERFACE (GST_TYPE_TAG_SETTER, NULL)
|
|
G_IMPLEMENT_INTERFACE (GST_TYPE_TOC_SETTER, NULL)
|
|
);
|
|
|
|
static GstFlowReturn gst_wavenc_chain (GstPad * pad, GstObject * parent,
|
|
GstBuffer * buf);
|
|
static gboolean gst_wavenc_event (GstPad * pad, GstObject * parent,
|
|
GstEvent * event);
|
|
static GstStateChangeReturn gst_wavenc_change_state (GstElement * element,
|
|
GstStateChange transition);
|
|
static gboolean gst_wavenc_sink_setcaps (GstPad * pad, GstCaps * caps);
|
|
|
|
static void
|
|
gst_wavenc_class_init (GstWavEncClass * klass)
|
|
{
|
|
GstElementClass *element_class;
|
|
|
|
element_class = (GstElementClass *) klass;
|
|
|
|
element_class->change_state = GST_DEBUG_FUNCPTR (gst_wavenc_change_state);
|
|
|
|
gst_element_class_set_static_metadata (element_class, "WAV audio muxer",
|
|
"Codec/Muxer/Audio",
|
|
"Encode raw audio into WAV", "Iain Holmes <iain@prettypeople.org>");
|
|
|
|
gst_element_class_add_pad_template (element_class,
|
|
gst_static_pad_template_get (&src_factory));
|
|
gst_element_class_add_pad_template (element_class,
|
|
gst_static_pad_template_get (&sink_factory));
|
|
|
|
GST_DEBUG_CATEGORY_INIT (wavenc_debug, "wavenc", 0, "WAV encoder element");
|
|
}
|
|
|
|
static void
|
|
gst_wavenc_init (GstWavEnc * wavenc)
|
|
{
|
|
wavenc->sinkpad = gst_pad_new_from_static_template (&sink_factory, "sink");
|
|
gst_pad_set_chain_function (wavenc->sinkpad,
|
|
GST_DEBUG_FUNCPTR (gst_wavenc_chain));
|
|
gst_pad_set_event_function (wavenc->sinkpad,
|
|
GST_DEBUG_FUNCPTR (gst_wavenc_event));
|
|
gst_pad_use_fixed_caps (wavenc->sinkpad);
|
|
gst_element_add_pad (GST_ELEMENT (wavenc), wavenc->sinkpad);
|
|
|
|
wavenc->srcpad = gst_pad_new_from_static_template (&src_factory, "src");
|
|
gst_pad_use_fixed_caps (wavenc->srcpad);
|
|
gst_element_add_pad (GST_ELEMENT (wavenc), wavenc->srcpad);
|
|
}
|
|
|
|
#define RIFF_CHUNK_LEN 12
|
|
#define FMT_WAV_CHUNK_LEN 24
|
|
#define FMT_EXT_CHUNK_LEN 48
|
|
#define FACT_CHUNK_LEN 12
|
|
#define DATA_HEADER_LEN 8
|
|
|
|
static gboolean
|
|
use_format_ext (GstWavEnc * wavenc)
|
|
{
|
|
return wavenc->channels > 2;
|
|
}
|
|
|
|
static int
|
|
get_header_len (GstWavEnc * wavenc)
|
|
{
|
|
int len = RIFF_CHUNK_LEN;
|
|
|
|
if (use_format_ext (wavenc))
|
|
len += FMT_EXT_CHUNK_LEN + FACT_CHUNK_LEN;
|
|
else
|
|
len += FMT_WAV_CHUNK_LEN;
|
|
|
|
return len + DATA_HEADER_LEN;
|
|
}
|
|
|
|
static guint64
|
|
gstmask_to_wavmask (guint64 gstmask, GstAudioChannelPosition * pos)
|
|
{
|
|
const GstAudioChannelPosition valid_pos =
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT |
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT |
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_LFE1 |
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_LEFT |
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT |
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT |
|
|
GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER |
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT;
|
|
|
|
const GstAudioChannelPosition wav_pos[] = {
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_LFE1,
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_LEFT,
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_REAR_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT,
|
|
GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER,
|
|
GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT,
|
|
};
|
|
int k;
|
|
int chan = 0;
|
|
guint64 ret = 0;
|
|
guint64 mask = 1;
|
|
|
|
if (gstmask == 0 || ((gstmask & ~valid_pos) != 0))
|
|
return 0;
|
|
|
|
for (k = 0; k < G_N_ELEMENTS (wav_pos); ++k) {
|
|
if (gstmask & wav_pos[k]) {
|
|
ret |= mask;
|
|
pos[chan++] = wav_pos[k];
|
|
}
|
|
mask <<= 1;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static guint8 *
|
|
write_fmt_chunk (GstWavEnc * wavenc, guint8 * header)
|
|
{
|
|
guint16 wBlockAlign;
|
|
|
|
wBlockAlign = (wavenc->width / 8) * wavenc->channels;
|
|
|
|
memcpy (header, "fmt ", 4);
|
|
/* wChannels */
|
|
GST_WRITE_UINT16_LE (header + 10, wavenc->channels);
|
|
/* dwSamplesPerSec */
|
|
GST_WRITE_UINT32_LE (header + 12, wavenc->rate);
|
|
/* dwAvgBytesPerSec */
|
|
GST_WRITE_UINT32_LE (header + 16, wBlockAlign * wavenc->rate);
|
|
/* wBlockAlign */
|
|
GST_WRITE_UINT16_LE (header + 20, wBlockAlign);
|
|
/* wBitsPerSample */
|
|
GST_WRITE_UINT16_LE (header + 22, wavenc->width);
|
|
|
|
if (use_format_ext (wavenc)) {
|
|
GST_DEBUG_OBJECT (wavenc, "Using WAVE_FORMAT_EXTENSIBLE");
|
|
|
|
GST_WRITE_UINT32_LE (header + 4, FMT_EXT_CHUNK_LEN - 8);
|
|
|
|
/* wFormatTag */
|
|
GST_WRITE_UINT16_LE (header + 8, 0xFFFE);
|
|
/* cbSize */
|
|
GST_WRITE_UINT16_LE (header + 24, 22);
|
|
/* wValidBitsPerSample */
|
|
GST_WRITE_UINT16_LE (header + 26, wavenc->width);
|
|
/* dwChannelMask */
|
|
GST_WRITE_UINT32_LE (header + 28, (guint32) wavenc->channel_mask);
|
|
|
|
GST_WRITE_UINT16_LE (header + 32, wavenc->format);
|
|
|
|
memcpy (header + 34,
|
|
"\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71", 14);
|
|
|
|
header += FMT_EXT_CHUNK_LEN;
|
|
|
|
} else {
|
|
GST_WRITE_UINT32_LE (header + 4, FMT_WAV_CHUNK_LEN - 8);
|
|
|
|
/* wFormatTag */
|
|
GST_WRITE_UINT16_LE (header + 8, wavenc->format);
|
|
header += FMT_WAV_CHUNK_LEN;
|
|
}
|
|
|
|
|
|
return header;
|
|
}
|
|
|
|
static guint8 *
|
|
write_fact_chunk (GstWavEnc * wavenc, guint8 * header)
|
|
{
|
|
memcpy (header, "fact", 4);
|
|
GST_WRITE_UINT32_LE (header + 4, FACT_CHUNK_LEN - 8);
|
|
/* compressed files are only supported up to 2 channels,
|
|
* that means we never write a fact chunk for them */
|
|
GST_WRITE_UINT32_LE (header + 8,
|
|
wavenc->audio_length / (wavenc->width / 8) / wavenc->channels);
|
|
return header + FACT_CHUNK_LEN;
|
|
}
|
|
|
|
static GstBuffer *
|
|
gst_wavenc_create_header_buf (GstWavEnc * wavenc)
|
|
{
|
|
GstBuffer *buf;
|
|
GstMapInfo map;
|
|
guint8 *header;
|
|
guint32 riffLen;
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "Header size: %d", get_header_len (wavenc));
|
|
buf = gst_buffer_new_and_alloc (get_header_len (wavenc));
|
|
gst_buffer_map (buf, &map, GST_MAP_WRITE);
|
|
header = map.data;
|
|
memset (header, 0, get_header_len (wavenc));
|
|
|
|
riffLen = wavenc->meta_length + wavenc->audio_length
|
|
+ get_header_len (wavenc) - 8;
|
|
|
|
/* RIFF chunk */
|
|
memcpy (header, "RIFF", 4);
|
|
GST_WRITE_UINT32_LE (header + 4, riffLen);
|
|
memcpy (header + 8, "WAVE", 4);
|
|
header += RIFF_CHUNK_LEN;
|
|
|
|
header = write_fmt_chunk (wavenc, header);
|
|
if (use_format_ext (wavenc))
|
|
header = write_fact_chunk (wavenc, header);
|
|
|
|
/* data chunk */
|
|
memcpy (header, "data ", 4);
|
|
GST_WRITE_UINT32_LE (header + 4, wavenc->audio_length);
|
|
|
|
gst_buffer_unmap (buf, &map);
|
|
|
|
return buf;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_wavenc_push_header (GstWavEnc * wavenc)
|
|
{
|
|
GstFlowReturn ret;
|
|
GstBuffer *outbuf;
|
|
GstSegment segment;
|
|
|
|
/* seek to beginning of file */
|
|
gst_segment_init (&segment, GST_FORMAT_BYTES);
|
|
if (!gst_pad_push_event (wavenc->srcpad, gst_event_new_segment (&segment))) {
|
|
GST_WARNING_OBJECT (wavenc, "Seek to the beginning failed");
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "writing header, meta_size=%u, audio_size=%u",
|
|
wavenc->meta_length, wavenc->audio_length);
|
|
|
|
outbuf = gst_wavenc_create_header_buf (wavenc);
|
|
GST_BUFFER_OFFSET (outbuf) = 0;
|
|
|
|
ret = gst_pad_push (wavenc->srcpad, outbuf);
|
|
|
|
if (ret != GST_FLOW_OK) {
|
|
GST_WARNING_OBJECT (wavenc, "push header failed: flow = %s",
|
|
gst_flow_get_name (ret));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_sink_setcaps (GstPad * pad, GstCaps * caps)
|
|
{
|
|
GstWavEnc *wavenc;
|
|
GstStructure *structure;
|
|
const gchar *name;
|
|
gint chans, rate;
|
|
GstCaps *ccaps;
|
|
|
|
wavenc = GST_WAVENC (gst_pad_get_parent (pad));
|
|
|
|
ccaps = gst_pad_get_current_caps (pad);
|
|
if (wavenc->sent_header && ccaps && !gst_caps_can_intersect (caps, ccaps)) {
|
|
gst_caps_unref (ccaps);
|
|
GST_WARNING_OBJECT (wavenc, "cannot change format in middle of stream");
|
|
goto fail;
|
|
}
|
|
if (ccaps)
|
|
gst_caps_unref (ccaps);
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "got caps: %" GST_PTR_FORMAT, caps);
|
|
|
|
structure = gst_caps_get_structure (caps, 0);
|
|
name = gst_structure_get_name (structure);
|
|
|
|
if (!gst_structure_get_int (structure, "channels", &chans) ||
|
|
!gst_structure_get_int (structure, "rate", &rate)) {
|
|
GST_WARNING_OBJECT (wavenc, "caps incomplete");
|
|
goto fail;
|
|
}
|
|
|
|
wavenc->channels = chans;
|
|
wavenc->rate = rate;
|
|
wavenc->channel_mask = 0;
|
|
|
|
if (strcmp (name, "audio/x-raw") == 0) {
|
|
GstAudioInfo info;
|
|
guint64 gstmask;
|
|
|
|
if (!gst_audio_info_from_caps (&info, caps)) {
|
|
GST_WARNING_OBJECT (wavenc, "Could not retrieve audio info from caps");
|
|
goto fail;
|
|
}
|
|
if (gst_audio_channel_positions_to_mask (info.position, wavenc->channels,
|
|
FALSE, &gstmask)) {
|
|
wavenc->channel_mask = gstmask_to_wavmask (gstmask, wavenc->destPos);
|
|
memcpy (wavenc->srcPos, info.position, sizeof (info.position));
|
|
GST_DEBUG_OBJECT (wavenc, "Channel mask input: 0x%" G_GINT64_MODIFIER "x"
|
|
" output: 0x%" G_GINT64_MODIFIER "x", gstmask, wavenc->channel_mask);
|
|
}
|
|
wavenc->audio_format = GST_AUDIO_INFO_FORMAT (&info);
|
|
|
|
if (GST_AUDIO_INFO_IS_INTEGER (&info))
|
|
wavenc->format = GST_RIFF_WAVE_FORMAT_PCM;
|
|
else if (GST_AUDIO_INFO_IS_FLOAT (&info))
|
|
wavenc->format = GST_RIFF_WAVE_FORMAT_IEEE_FLOAT;
|
|
else
|
|
goto fail;
|
|
|
|
wavenc->width = GST_AUDIO_INFO_WIDTH (&info);
|
|
} else if (strcmp (name, "audio/x-alaw") == 0) {
|
|
wavenc->format = GST_RIFF_WAVE_FORMAT_ALAW;
|
|
wavenc->width = 8;
|
|
} else if (strcmp (name, "audio/x-mulaw") == 0) {
|
|
wavenc->format = GST_RIFF_WAVE_FORMAT_MULAW;
|
|
wavenc->width = 8;
|
|
} else {
|
|
GST_WARNING_OBJECT (wavenc, "Unsupported format %s", name);
|
|
goto fail;
|
|
}
|
|
|
|
GST_LOG_OBJECT (wavenc,
|
|
"accepted caps: format=0x%04x chans=%u width=%u rate=%u",
|
|
wavenc->format, wavenc->channels, wavenc->width, wavenc->rate);
|
|
|
|
gst_object_unref (wavenc);
|
|
return TRUE;
|
|
|
|
fail:
|
|
gst_object_unref (wavenc);
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
gst_wavparse_tags_foreach (const GstTagList * tags, const gchar * tag,
|
|
gpointer data)
|
|
{
|
|
const struct
|
|
{
|
|
guint32 fcc;
|
|
const gchar *tag;
|
|
} rifftags[] = {
|
|
{
|
|
GST_RIFF_INFO_IARL, GST_TAG_LOCATION}, {
|
|
GST_RIFF_INFO_IART, GST_TAG_ARTIST}, {
|
|
GST_RIFF_INFO_ICMT, GST_TAG_COMMENT}, {
|
|
GST_RIFF_INFO_ICOP, GST_TAG_COPYRIGHT}, {
|
|
GST_RIFF_INFO_ICRD, GST_TAG_DATE}, {
|
|
GST_RIFF_INFO_IGNR, GST_TAG_GENRE}, {
|
|
GST_RIFF_INFO_IKEY, GST_TAG_KEYWORDS}, {
|
|
GST_RIFF_INFO_INAM, GST_TAG_TITLE}, {
|
|
GST_RIFF_INFO_IPRD, GST_TAG_ALBUM}, {
|
|
GST_RIFF_INFO_ISBJ, GST_TAG_ALBUM_ARTIST}, {
|
|
GST_RIFF_INFO_ISFT, GST_TAG_ENCODER}, {
|
|
GST_RIFF_INFO_ISRC, GST_TAG_ISRC}, {
|
|
0, NULL}
|
|
};
|
|
gint n;
|
|
gchar *str = NULL;
|
|
GstByteWriter *bw = data;
|
|
for (n = 0; rifftags[n].fcc != 0; n++) {
|
|
if (!strcmp (rifftags[n].tag, tag)) {
|
|
if (rifftags[n].fcc == GST_RIFF_INFO_ICRD) {
|
|
GDate *date;
|
|
/* special case for the date tag */
|
|
if (gst_tag_list_get_date (tags, tag, &date)) {
|
|
str =
|
|
g_strdup_printf ("%04d:%02d:%02d", g_date_get_year (date),
|
|
g_date_get_month (date), g_date_get_day (date));
|
|
g_date_free (date);
|
|
}
|
|
} else {
|
|
gst_tag_list_get_string (tags, tag, &str);
|
|
}
|
|
if (str) {
|
|
gst_byte_writer_put_uint32_le (bw, rifftags[n].fcc);
|
|
gst_byte_writer_put_uint32_le (bw, GST_ROUND_UP_2 (strlen (str)));
|
|
gst_byte_writer_put_string (bw, str);
|
|
g_free (str);
|
|
str = NULL;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_wavenc_write_tags (GstWavEnc * wavenc)
|
|
{
|
|
const GstTagList *user_tags;
|
|
GstTagList *tags;
|
|
guint size;
|
|
GstBuffer *buf;
|
|
GstByteWriter bw;
|
|
|
|
g_return_val_if_fail (wavenc != NULL, GST_FLOW_OK);
|
|
|
|
user_tags = gst_tag_setter_get_tag_list (GST_TAG_SETTER (wavenc));
|
|
if ((!wavenc->tags) && (!user_tags)) {
|
|
GST_DEBUG_OBJECT (wavenc, "have no tags");
|
|
return GST_FLOW_OK;
|
|
}
|
|
tags =
|
|
gst_tag_list_merge (user_tags, wavenc->tags,
|
|
gst_tag_setter_get_tag_merge_mode (GST_TAG_SETTER (wavenc)));
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "writing tags");
|
|
|
|
gst_byte_writer_init_with_size (&bw, 1024, FALSE);
|
|
|
|
/* add LIST INFO chunk */
|
|
gst_byte_writer_put_data (&bw, (const guint8 *) "LIST", 4);
|
|
gst_byte_writer_put_uint32_le (&bw, 0);
|
|
gst_byte_writer_put_data (&bw, (const guint8 *) "INFO", 4);
|
|
|
|
/* add tags */
|
|
gst_tag_list_foreach (tags, gst_wavparse_tags_foreach, &bw);
|
|
|
|
/* sets real size of LIST INFO chunk */
|
|
size = gst_byte_writer_get_pos (&bw);
|
|
gst_byte_writer_set_pos (&bw, 4);
|
|
gst_byte_writer_put_uint32_le (&bw, size - 8);
|
|
|
|
gst_tag_list_unref (tags);
|
|
|
|
buf = gst_byte_writer_reset_and_get_buffer (&bw);
|
|
wavenc->meta_length += gst_buffer_get_size (buf);
|
|
return gst_pad_push (wavenc->srcpad, buf);
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_is_cue_id_unique (guint32 id, GList * list)
|
|
{
|
|
GstWavEncCue *cue;
|
|
|
|
while (list) {
|
|
cue = list->data;
|
|
if (cue->id == id)
|
|
return FALSE;
|
|
list = g_list_next (list);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_parse_cue (GstWavEnc * wavenc, guint32 id, GstTocEntry * entry)
|
|
{
|
|
gint64 start;
|
|
GstWavEncCue *cue;
|
|
|
|
g_return_val_if_fail (entry != NULL, FALSE);
|
|
|
|
gst_toc_entry_get_start_stop_times (entry, &start, NULL);
|
|
|
|
cue = g_new (GstWavEncCue, 1);
|
|
cue->id = id;
|
|
cue->position = gst_util_uint64_scale_round (start, wavenc->rate, GST_SECOND);
|
|
memcpy (cue->data_chunk_id, "data", 4);
|
|
cue->chunk_start = 0;
|
|
cue->block_start = 0;
|
|
cue->sample_offset = cue->position;
|
|
wavenc->cues = g_list_append (wavenc->cues, cue);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_parse_labl (GstWavEnc * wavenc, guint32 id, GstTocEntry * entry)
|
|
{
|
|
gchar *tag;
|
|
GstTagList *tags;
|
|
GstWavEncLabl *labl;
|
|
|
|
g_return_val_if_fail (entry != NULL, FALSE);
|
|
|
|
tags = gst_toc_entry_get_tags (entry);
|
|
if (!tags) {
|
|
GST_INFO_OBJECT (wavenc, "no tags for entry: %d", id);
|
|
return FALSE;
|
|
}
|
|
if (!gst_tag_list_get_string (tags, GST_TAG_TITLE, &tag)) {
|
|
GST_INFO_OBJECT (wavenc, "no title tag for entry: %d", id);
|
|
return FALSE;
|
|
}
|
|
|
|
labl = g_new (GstWavEncLabl, 1);
|
|
memcpy (labl->chunk_id, "labl", 4);
|
|
labl->chunk_data_size = 4 + strlen (tag) + 1;
|
|
labl->cue_point_id = id;
|
|
labl->text = tag;
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "got labl: '%s'", tag);
|
|
|
|
wavenc->labls = g_list_append (wavenc->labls, labl);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_parse_note (GstWavEnc * wavenc, guint32 id, GstTocEntry * entry)
|
|
{
|
|
gchar *tag;
|
|
GstTagList *tags;
|
|
GstWavEncNote *note;
|
|
|
|
g_return_val_if_fail (entry != NULL, FALSE);
|
|
tags = gst_toc_entry_get_tags (entry);
|
|
if (!tags) {
|
|
GST_INFO_OBJECT (wavenc, "no tags for entry: %d", id);
|
|
return FALSE;
|
|
}
|
|
if (!gst_tag_list_get_string (tags, GST_TAG_COMMENT, &tag)) {
|
|
GST_INFO_OBJECT (wavenc, "no comment tag for entry: %d", id);
|
|
return FALSE;
|
|
}
|
|
|
|
note = g_new (GstWavEncNote, 1);
|
|
memcpy (note->chunk_id, "note", 4);
|
|
note->chunk_data_size = 4 + strlen (tag) + 1;
|
|
note->cue_point_id = id;
|
|
note->text = tag;
|
|
|
|
GST_DEBUG_OBJECT (wavenc, "got note: '%s'", tag);
|
|
|
|
wavenc->notes = g_list_append (wavenc->notes, note);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_write_cues (guint8 ** data, GList * list)
|
|
{
|
|
GstWavEncCue *cue;
|
|
|
|
while (list) {
|
|
cue = list->data;
|
|
GST_WRITE_UINT32_LE (*data, cue->id);
|
|
GST_WRITE_UINT32_LE (*data + 4, cue->position);
|
|
memcpy (*data + 8, (gchar *) cue->data_chunk_id, 4);
|
|
GST_WRITE_UINT32_LE (*data + 12, cue->chunk_start);
|
|
GST_WRITE_UINT32_LE (*data + 16, cue->block_start);
|
|
GST_WRITE_UINT32_LE (*data + 20, cue->sample_offset);
|
|
*data += 24;
|
|
list = g_list_next (list);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_write_labls (guint8 ** data, GList * list)
|
|
{
|
|
GstWavEncLabl *labl;
|
|
|
|
while (list) {
|
|
labl = list->data;
|
|
memcpy (*data, (gchar *) labl->chunk_id, 4);
|
|
GST_WRITE_UINT32_LE (*data + 4, labl->chunk_data_size);
|
|
GST_WRITE_UINT32_LE (*data + 8, labl->cue_point_id);
|
|
memcpy (*data + 12, (gchar *) labl->text, strlen (labl->text));
|
|
*data += 8 + GST_ROUND_UP_2 (labl->chunk_data_size);
|
|
list = g_list_next (list);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_write_notes (guint8 ** data, GList * list)
|
|
{
|
|
GstWavEncNote *note;
|
|
|
|
while (list) {
|
|
note = list->data;
|
|
memcpy (*data, (gchar *) note->chunk_id, 4);
|
|
GST_WRITE_UINT32_LE (*data + 4, note->chunk_data_size);
|
|
GST_WRITE_UINT32_LE (*data + 8, note->cue_point_id);
|
|
memcpy (*data + 12, (gchar *) note->text, strlen (note->text));
|
|
*data += 8 + GST_ROUND_UP_2 (note->chunk_data_size);
|
|
list = g_list_next (list);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_wavenc_write_toc (GstWavEnc * wavenc)
|
|
{
|
|
GList *list;
|
|
GstToc *toc;
|
|
GstTocEntry *entry, *subentry;
|
|
GstBuffer *buf;
|
|
GstMapInfo map;
|
|
guint8 *data;
|
|
guint32 ncues, size, cues_size, labls_size, notes_size;
|
|
|
|
if (!wavenc->toc) {
|
|
GST_DEBUG_OBJECT (wavenc, "have no toc, checking toc_setter");
|
|
wavenc->toc = gst_toc_setter_get_toc (GST_TOC_SETTER (wavenc));
|
|
}
|
|
if (!wavenc->toc) {
|
|
GST_WARNING_OBJECT (wavenc, "have no toc");
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
toc = gst_toc_ref (wavenc->toc);
|
|
size = 0;
|
|
cues_size = 0;
|
|
labls_size = 0;
|
|
notes_size = 0;
|
|
|
|
/* check if the TOC entries is valid */
|
|
list = gst_toc_get_entries (toc);
|
|
entry = list->data;
|
|
if (gst_toc_entry_is_alternative (entry)) {
|
|
list = gst_toc_entry_get_sub_entries (entry);
|
|
while (list) {
|
|
subentry = list->data;
|
|
if (!gst_toc_entry_is_sequence (subentry))
|
|
return FALSE;
|
|
list = g_list_next (list);
|
|
}
|
|
list = gst_toc_entry_get_sub_entries (entry);
|
|
}
|
|
if (gst_toc_entry_is_sequence (entry)) {
|
|
while (list) {
|
|
entry = list->data;
|
|
if (!gst_toc_entry_is_sequence (entry))
|
|
return FALSE;
|
|
list = g_list_next (list);
|
|
}
|
|
list = gst_toc_get_entries (toc);
|
|
}
|
|
|
|
ncues = g_list_length (list);
|
|
GST_DEBUG_OBJECT (wavenc, "number of cue entries: %d", ncues);
|
|
|
|
while (list) {
|
|
guint32 id = 0;
|
|
gint64 id64;
|
|
const gchar *uid;
|
|
|
|
entry = list->data;
|
|
uid = gst_toc_entry_get_uid (entry);
|
|
id64 = g_ascii_strtoll (uid, NULL, 0);
|
|
/* check if id unique compatible with guint32 else generate random */
|
|
if (id64 >= 0 && gst_wavenc_is_cue_id_unique (id64, wavenc->cues)) {
|
|
id = (guint32) id64;
|
|
} else {
|
|
do {
|
|
id = g_random_int ();
|
|
} while (!gst_wavenc_is_cue_id_unique (id, wavenc->cues));
|
|
}
|
|
gst_wavenc_parse_cue (wavenc, id, entry);
|
|
gst_wavenc_parse_labl (wavenc, id, entry);
|
|
gst_wavenc_parse_note (wavenc, id, entry);
|
|
list = g_list_next (list);
|
|
}
|
|
|
|
/* count cues size */
|
|
if (wavenc->cues) {
|
|
cues_size = 24 * g_list_length (wavenc->cues);
|
|
size += 12 + cues_size;
|
|
} else {
|
|
GST_WARNING_OBJECT (wavenc, "cue's not found");
|
|
return FALSE;
|
|
}
|
|
/* count labls size */
|
|
if (wavenc->labls) {
|
|
list = wavenc->labls;
|
|
while (list) {
|
|
GstWavEncLabl *labl;
|
|
labl = list->data;
|
|
labls_size += 8 + GST_ROUND_UP_2 (labl->chunk_data_size);
|
|
list = g_list_next (list);
|
|
}
|
|
size += labls_size;
|
|
}
|
|
/* count notes size */
|
|
if (wavenc->notes) {
|
|
list = wavenc->notes;
|
|
while (list) {
|
|
GstWavEncNote *note;
|
|
note = list->data;
|
|
notes_size += 8 + GST_ROUND_UP_2 (note->chunk_data_size);
|
|
list = g_list_next (list);
|
|
}
|
|
size += notes_size;
|
|
}
|
|
if (wavenc->labls || wavenc->notes) {
|
|
size += 12;
|
|
}
|
|
|
|
buf = gst_buffer_new_and_alloc (size);
|
|
gst_buffer_map (buf, &map, GST_MAP_WRITE);
|
|
data = map.data;
|
|
memset (data, 0, size);
|
|
|
|
/* write Cue Chunk */
|
|
if (wavenc->cues) {
|
|
memcpy (data, (gchar *) "cue ", 4);
|
|
GST_WRITE_UINT32_LE (data + 4, 4 + cues_size);
|
|
GST_WRITE_UINT32_LE (data + 8, ncues);
|
|
data += 12;
|
|
gst_wavenc_write_cues (&data, wavenc->cues);
|
|
|
|
/* write Associated Data List Chunk */
|
|
if (wavenc->labls || wavenc->notes) {
|
|
memcpy (data, (gchar *) "LIST", 4);
|
|
GST_WRITE_UINT32_LE (data + 4, 4 + labls_size + notes_size);
|
|
memcpy (data + 8, (gchar *) "adtl", 4);
|
|
data += 12;
|
|
if (wavenc->labls)
|
|
gst_wavenc_write_labls (&data, wavenc->labls);
|
|
if (wavenc->notes)
|
|
gst_wavenc_write_notes (&data, wavenc->notes);
|
|
}
|
|
}
|
|
|
|
/* free resources */
|
|
if (toc)
|
|
gst_toc_unref (toc);
|
|
if (wavenc->cues)
|
|
g_list_free_full (wavenc->cues, g_free);
|
|
if (wavenc->labls)
|
|
g_list_free_full (wavenc->labls, g_free);
|
|
if (wavenc->notes)
|
|
g_list_free_full (wavenc->notes, g_free);
|
|
|
|
gst_buffer_unmap (buf, &map);
|
|
wavenc->meta_length += gst_buffer_get_size (buf);
|
|
|
|
return gst_pad_push (wavenc->srcpad, buf);
|
|
}
|
|
|
|
static gboolean
|
|
gst_wavenc_event (GstPad * pad, GstObject * parent, GstEvent * event)
|
|
{
|
|
gboolean res = TRUE;
|
|
GstWavEnc *wavenc;
|
|
GstTagList *tags;
|
|
GstToc *toc;
|
|
|
|
wavenc = GST_WAVENC (parent);
|
|
|
|
switch (GST_EVENT_TYPE (event)) {
|
|
case GST_EVENT_CAPS:
|
|
{
|
|
GstCaps *caps;
|
|
|
|
gst_event_parse_caps (event, &caps);
|
|
gst_wavenc_sink_setcaps (pad, caps);
|
|
|
|
/* have our own src caps */
|
|
gst_event_unref (event);
|
|
break;
|
|
}
|
|
case GST_EVENT_EOS:
|
|
{
|
|
GstFlowReturn flow;
|
|
GST_DEBUG_OBJECT (wavenc, "got EOS");
|
|
|
|
flow = gst_wavenc_write_toc (wavenc);
|
|
if (flow != GST_FLOW_OK) {
|
|
GST_WARNING_OBJECT (wavenc, "error pushing toc: %s",
|
|
gst_flow_get_name (flow));
|
|
}
|
|
flow = gst_wavenc_write_tags (wavenc);
|
|
if (flow != GST_FLOW_OK) {
|
|
GST_WARNING_OBJECT (wavenc, "error pushing tags: %s",
|
|
gst_flow_get_name (flow));
|
|
}
|
|
|
|
/* write header with correct length values */
|
|
gst_wavenc_push_header (wavenc);
|
|
|
|
/* we're done with this file */
|
|
wavenc->finished_properly = TRUE;
|
|
|
|
/* and forward the EOS event */
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
}
|
|
case GST_EVENT_SEGMENT:
|
|
/* Just drop it, it's probably in TIME format
|
|
* anyway. We'll send our own newsegment event */
|
|
gst_event_unref (event);
|
|
break;
|
|
case GST_EVENT_TOC:
|
|
gst_event_parse_toc (event, &toc, NULL);
|
|
if (toc) {
|
|
if (wavenc->toc != toc) {
|
|
if (wavenc->toc)
|
|
gst_toc_unref (wavenc->toc);
|
|
wavenc->toc = toc;
|
|
} else {
|
|
gst_toc_unref (toc);
|
|
}
|
|
}
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
case GST_EVENT_TAG:
|
|
gst_event_parse_tag (event, &tags);
|
|
if (tags) {
|
|
if (wavenc->tags != tags) {
|
|
if (wavenc->tags)
|
|
gst_tag_list_unref (wavenc->tags);
|
|
wavenc->tags = gst_tag_list_ref (tags);
|
|
}
|
|
}
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
default:
|
|
res = gst_pad_event_default (pad, parent, event);
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_wavenc_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)
|
|
{
|
|
GstWavEnc *wavenc = GST_WAVENC (parent);
|
|
GstFlowReturn flow = GST_FLOW_OK;
|
|
|
|
if (wavenc->channels <= 0) {
|
|
GST_ERROR_OBJECT (wavenc, "Got data without caps");
|
|
return GST_FLOW_NOT_NEGOTIATED;
|
|
}
|
|
|
|
if (G_UNLIKELY (!wavenc->sent_header)) {
|
|
gst_pad_set_caps (wavenc->srcpad,
|
|
gst_static_pad_template_get_caps (&src_factory));
|
|
|
|
/* starting a file, means we have to finish it properly */
|
|
wavenc->finished_properly = FALSE;
|
|
|
|
/* push initial bogus header, it will be updated on EOS */
|
|
flow = gst_wavenc_push_header (wavenc);
|
|
if (flow != GST_FLOW_OK) {
|
|
GST_WARNING_OBJECT (wavenc, "error pushing header: %s",
|
|
gst_flow_get_name (flow));
|
|
return flow;
|
|
}
|
|
GST_DEBUG_OBJECT (wavenc, "wrote dummy header");
|
|
wavenc->audio_length = 0;
|
|
wavenc->sent_header = TRUE;
|
|
}
|
|
|
|
GST_LOG_OBJECT (wavenc,
|
|
"pushing %" G_GSIZE_FORMAT " bytes raw audio, ts=%" GST_TIME_FORMAT,
|
|
gst_buffer_get_size (buf), GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buf)));
|
|
|
|
buf = gst_buffer_make_writable (buf);
|
|
|
|
GST_BUFFER_OFFSET (buf) = get_header_len (wavenc) + wavenc->audio_length;
|
|
GST_BUFFER_OFFSET_END (buf) = GST_BUFFER_OFFSET_NONE;
|
|
|
|
wavenc->audio_length += gst_buffer_get_size (buf);
|
|
|
|
if (wavenc->channel_mask != 0 &&
|
|
!gst_audio_buffer_reorder_channels (buf, wavenc->audio_format,
|
|
wavenc->channels, wavenc->srcPos, wavenc->destPos)) {
|
|
GST_WARNING_OBJECT (wavenc, "Could not reorder channels");
|
|
}
|
|
|
|
flow = gst_pad_push (wavenc->srcpad, buf);
|
|
|
|
return flow;
|
|
}
|
|
|
|
static GstStateChangeReturn
|
|
gst_wavenc_change_state (GstElement * element, GstStateChange transition)
|
|
{
|
|
GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
|
|
GstWavEnc *wavenc = GST_WAVENC (element);
|
|
|
|
switch (transition) {
|
|
case GST_STATE_CHANGE_NULL_TO_READY:
|
|
wavenc->format = 0;
|
|
wavenc->channels = 0;
|
|
wavenc->width = 0;
|
|
wavenc->rate = 0;
|
|
/* use bogus size initially, we'll write the real
|
|
* header when we get EOS and know the exact length */
|
|
wavenc->audio_length = 0x7FFF0000;
|
|
wavenc->meta_length = 0;
|
|
wavenc->sent_header = FALSE;
|
|
/* its true because we haven't writen anything */
|
|
wavenc->finished_properly = TRUE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
|
if (ret != GST_STATE_CHANGE_SUCCESS)
|
|
return ret;
|
|
|
|
switch (transition) {
|
|
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
|
if (!wavenc->finished_properly) {
|
|
GST_ELEMENT_WARNING (wavenc, STREAM, MUX,
|
|
("Wav stream not finished properly"),
|
|
("Wav stream not finished properly, no EOS received "
|
|
"before shutdown"));
|
|
}
|
|
break;
|
|
case GST_STATE_CHANGE_READY_TO_NULL:
|
|
GST_DEBUG_OBJECT (wavenc, "tags: %p", wavenc->tags);
|
|
if (wavenc->tags) {
|
|
gst_tag_list_unref (wavenc->tags);
|
|
wavenc->tags = NULL;
|
|
}
|
|
GST_DEBUG_OBJECT (wavenc, "toc: %p", wavenc->toc);
|
|
if (wavenc->toc) {
|
|
gst_toc_unref (wavenc->toc);
|
|
wavenc->toc = NULL;
|
|
}
|
|
gst_tag_setter_reset_tags (GST_TAG_SETTER (wavenc));
|
|
gst_toc_setter_reset (GST_TOC_SETTER (wavenc));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
plugin_init (GstPlugin * plugin)
|
|
{
|
|
return gst_element_register (plugin, "wavenc", GST_RANK_PRIMARY,
|
|
GST_TYPE_WAVENC);
|
|
}
|
|
|
|
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
|
|
GST_VERSION_MINOR,
|
|
wavenc,
|
|
"Encode raw audio into WAV",
|
|
plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
|