Add support for MixRamp tags

Adds mixrampdb and mixrampdelay commands.  Reads MIXRAP_START and
MIXRAMP_END tags from FLAC files and overlaps instead of crossfading.
This commit is contained in:
Tim Phipps 2010-03-21 18:21:47 +01:00 committed by Max Kellermann
parent e9b75d462c
commit e7a515c8b1
22 changed files with 478 additions and 19 deletions

1
NEWS
View File

@ -88,6 +88,7 @@ ver 0.16 (20??/??/??)
* pcm_volume, pcm_mix: implemented 32 bit support
* support packed 24 bit samples
* CUE sheet support
* support for MixRamp tags
* obey $(sysconfdir) for default mpd.conf location
* build with large file support by default
* added test suite ("make check")

View File

@ -338,6 +338,18 @@
<returnvalue>crossfade in seconds</returnvalue>
</para>
</listitem>
<listitem>
<para>
<varname>mixrampdb</varname>:
<returnvalue>mixramp threshold in dB</returnvalue>
</para>
</listitem>
<listitem>
<para>
<varname>mixrampdelay</varname>:
<returnvalue>mixrampdelay in seconds</returnvalue>
</para>
</listitem>
<listitem>
<para>
<varname>audio</varname>:
@ -442,6 +454,32 @@
</para>
</listitem>
</varlistentry>
<varlistentry id="command_mixrampdb">
<term>
<cmdsynopsis>
<command>mixrampdb</command>
<arg choice="req"><replaceable>deciBels</replaceable></arg>
</cmdsynopsis>
</term>
<listitem>
<para>
Sets the threshold at which songs will be overlapped. Like crossfading but doesn't fade the track volume, just overlaps. The songs need to have MixRamp tags added by an external tool. 0dB is the normalized maximum volume so use negative values, I prefer -17dB. In the absence of mixramp tags crossfading will be used. See http://sourceforge.net/projects/mixramp
</para>
</listitem>
</varlistentry>
<varlistentry id="command_mixrampdelay">
<term>
<cmdsynopsis>
<command>mixrampdelay</command>
<arg choice="req"><replaceable>SECONDS</replaceable></arg>
</cmdsynopsis>
</term>
<listitem>
<para>
Additional time subtracted from the overlap calculated by mixrampdb. A value of "nan" disables MixRamp overlapping and falls back to crossfading.
</para>
</listitem>
</varlistentry>
<varlistentry id="command_random">
<term>
<cmdsynopsis>

View File

@ -76,6 +76,8 @@
#define COMMAND_STATUS_BITRATE "bitrate"
#define COMMAND_STATUS_ERROR "error"
#define COMMAND_STATUS_CROSSFADE "xfade"
#define COMMAND_STATUS_MIXRAMPDB "mixrampdb"
#define COMMAND_STATUS_MIXRAMPDELAY "mixrampdelay"
#define COMMAND_STATUS_AUDIO "audio"
#define COMMAND_STATUS_UPDATING_DB "updating_db"
@ -294,6 +296,23 @@ check_bool(struct client *client, bool *value_r, const char *s)
return true;
}
static bool
check_float(struct client *client, float *value_r, const char *s)
{
float value;
char *endptr;
value = strtof(s, &endptr);
if (*endptr != 0 && endptr == s) {
command_error(client, ACK_ERROR_ARG,
"Float expected: %s", s);
return false;
}
*value_r = value;
return true;
}
static enum command_return
print_playlist_result(struct client *client,
enum playlist_result result)
@ -495,6 +514,8 @@ handle_status(struct client *client,
COMMAND_STATUS_PLAYLIST ": %li\n"
COMMAND_STATUS_PLAYLIST_LENGTH ": %i\n"
COMMAND_STATUS_CROSSFADE ": %i\n"
COMMAND_STATUS_MIXRAMPDB ": %f\n"
COMMAND_STATUS_MIXRAMPDELAY ": %f\n"
COMMAND_STATUS_STATE ": %s\n",
volume_level_get(),
playlist_get_repeat(&g_playlist),
@ -504,6 +525,8 @@ handle_status(struct client *client,
playlist_get_version(&g_playlist),
playlist_get_length(&g_playlist),
(int)(pc_get_cross_fade() + 0.5),
pc_get_mixramp_db(),
pc_get_mixramp_delay(),
state);
song = playlist_get_current_song(&g_playlist);
@ -1450,6 +1473,30 @@ handle_crossfade(struct client *client, G_GNUC_UNUSED int argc, char *argv[])
return COMMAND_RETURN_OK;
}
static enum command_return
handle_mixrampdb(struct client *client, G_GNUC_UNUSED int argc, char *argv[])
{
float db;
if (!check_float(client, &db, argv[1]))
return COMMAND_RETURN_ERROR;
pc_set_mixramp_db(db);
return COMMAND_RETURN_OK;
}
static enum command_return
handle_mixrampdelay(struct client *client, G_GNUC_UNUSED int argc, char *argv[])
{
float delay_secs;
if (!check_float(client, &delay_secs, argv[1]))
return COMMAND_RETURN_ERROR;
pc_set_mixramp_delay(delay_secs);
return COMMAND_RETURN_OK;
}
static enum command_return
handle_enableoutput(struct client *client, G_GNUC_UNUSED int argc, char *argv[])
{
@ -1805,6 +1852,8 @@ static const struct command commands[] = {
{ "listplaylists", PERMISSION_READ, 0, 0, handle_listplaylists },
{ "load", PERMISSION_ADD, 1, 1, handle_load },
{ "lsinfo", PERMISSION_READ, 0, 1, handle_lsinfo },
{ "mixrampdb", PERMISSION_CONTROL, 1, 1, handle_mixrampdb },
{ "mixrampdelay", PERMISSION_CONTROL, 1, 1, handle_mixrampdelay },
{ "move", PERMISSION_CONTROL, 2, 2, handle_move },
{ "moveid", PERMISSION_CONTROL, 2, 2, handle_moveid },
{ "next", PERMISSION_CONTROL, 0, 0, handle_next },

View File

@ -26,34 +26,111 @@
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <glib.h>
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "crossfade"
static float mixramp_interpolate(char *ramp_list, float required_db)
{
float db, secs, last_db = nan(""), last_secs = 0;
char *ramp_str, *save_str = NULL;
/* ramp_list is a string of pairs of dBs and seconds that describe the
* volume profile. Delimiters are semi-colons between pairs and spaces
* between the dB and seconds of a pair.
* The dB values must be monotonically increasing for this to work. */
while (1) {
/* Parse the dB tokens out of the input string. */
ramp_str = strtok_r(ramp_list, " ", &save_str);
/* Tell strtok to continue next time round. */
ramp_list = NULL;
/* Parse the dB value. */
if (NULL == ramp_str) {
return nan("");
}
db = (float)atof(ramp_str);
/* Parse the time. */
ramp_str = strtok_r(NULL, ";", &save_str);
if (NULL == ramp_str) {
return nan("");
}
secs = (float)atof(ramp_str);
/* Check for exact match. */
if (db == required_db) {
return secs;
}
/* Save if too quiet. */
if (db < required_db) {
last_db = db;
last_secs = secs;
continue;
}
/* If required db < any stored value, use the least. */
if (isnan(last_db)) {
return secs;
}
/* Finally, interpolate linearly. */
secs = last_secs + (required_db - last_db) * (secs - last_secs) / (db - last_db);
return secs;
}
}
unsigned cross_fade_calc(float duration, float total_time,
float mixramp_db, float mixramp_delay,
char *mixramp_start, char *mixramp_prev_end,
const struct audio_format *af,
const struct audio_format *old_format,
unsigned max_chunks)
{
unsigned int chunks;
unsigned int chunks = 0;
float chunks_f;
float mixramp_overlap;
if (duration <= 0 || duration >= total_time ||
if (duration < 0 || duration >= total_time ||
/* we can't crossfade when the audio formats are different */
!audio_format_equals(af, old_format))
return 0;
assert(duration > 0);
assert(duration >= 0);
assert(audio_format_valid(af));
chunks = audio_format_time_to_size(af) / CHUNK_SIZE;
chunks = (chunks * duration + 0.5);
chunks_f = (float)audio_format_time_to_size(af) / (float)CHUNK_SIZE;
if (chunks > max_chunks)
if (isnan(mixramp_delay) || !(mixramp_start) || !(mixramp_prev_end)) {
chunks = (chunks_f * duration + 0.5);
} else {
/* Calculate mixramp overlap.
* FIXME factor in ReplayGain for both songs. */
mixramp_overlap = mixramp_interpolate(mixramp_start, mixramp_db)
+ mixramp_interpolate(mixramp_prev_end, mixramp_db);
if (!isnan(mixramp_overlap) && (mixramp_delay <= mixramp_overlap)) {
chunks = (chunks_f * (mixramp_overlap - mixramp_delay));
g_debug("will overlap %d chunks, %fs", chunks,
mixramp_overlap - mixramp_delay);
}
}
if (chunks > max_chunks) {
chunks = max_chunks;
g_warning("audio_buffer_size too small for computed MixRamp overlap");
}
return chunks;
}
void cross_fade_apply(struct music_chunk *a, const struct music_chunk *b,
const struct audio_format *format,
unsigned int current_chunk, unsigned int num_chunks)
float mix_ratio)
{
size_t size;
@ -61,7 +138,6 @@ void cross_fade_apply(struct music_chunk *a, const struct music_chunk *b,
assert(b != NULL);
assert(a->length == 0 || b->length == 0 ||
audio_format_equals(&a->audio_format, &b->audio_format));
assert(current_chunk <= num_chunks);
if (a->tag == NULL && b->tag != NULL)
/* merge the tag into the destination chunk */
@ -75,7 +151,7 @@ void cross_fade_apply(struct music_chunk *a, const struct music_chunk *b,
b->data,
size,
format,
((float)current_chunk) / num_chunks);
mix_ratio);
if (b->length > a->length) {
/* the second buffer is larger than the first one:

View File

@ -28,6 +28,10 @@ struct music_chunk;
*
* @param duration the requested crossfade duration
* @param total_time total_time the duration of the new song
* @param mixramp_db the current mixramp_db setting
* @param mixramp_delay the current mixramp_delay setting
* @param mixramp_start the next songs mixramp_start tag
* @param mixramp_prev_end the last songs mixramp_end setting
* @param af the audio format of the new song
* @param old_format the audio format of the current song
* @param max_chunks the maximum number of chunks
@ -35,6 +39,8 @@ struct music_chunk;
* should be disabled for this song change
*/
unsigned cross_fade_calc(float duration, float total_time,
float mixramp_db, float mixramp_delay,
char *mixramp_start, char *mixramp_prev_end,
const struct audio_format *af,
const struct audio_format *old_format,
unsigned max_chunks);
@ -51,6 +57,6 @@ unsigned cross_fade_calc(float duration, float total_time,
*/
void cross_fade_apply(struct music_chunk *a, const struct music_chunk *b,
const struct audio_format *format,
unsigned int current_chunk, unsigned int num_chunks);
float mix_ratio);
#endif

View File

@ -102,11 +102,6 @@ flac_got_stream_info(struct flac_data *data,
if (data->total_frames == 0)
data->total_frames = stream_info->total_samples;
decoder_initialized(data->decoder, &data->audio_format,
data->input_stream->seekable,
(float)data->total_frames /
(float)data->audio_format.sample_rate);
data->initialized = true;
}
@ -117,6 +112,8 @@ void flac_metadata_common_cb(const FLAC__StreamMetadata * block,
return;
struct replay_gain_info rgi;
char *mixramp_start;
char *mixramp_end;
switch (block->type) {
case FLAC__METADATA_TYPE_STREAMINFO:
@ -126,6 +123,10 @@ void flac_metadata_common_cb(const FLAC__StreamMetadata * block,
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (flac_parse_replay_gain(&rgi, block))
decoder_replay_gain(data->decoder, &rgi);
if (flac_parse_mixramp(&mixramp_start, &mixramp_end, block)) {
g_debug("setting mixramp_tags");
decoder_mixramp(data->decoder, mixramp_start, mixramp_end);
}
if (data->tag != NULL)
flac_vorbis_comments_to_tag(data->tag, NULL,

View File

@ -247,9 +247,14 @@ flac_decoder_initialize(struct flac_data *data, FLAC__StreamDecoder *sd,
return false;
}
if (data->initialized)
if (data->initialized) {
/* done */
decoder_initialized(data->decoder, &data->audio_format,
data->input_stream->seekable,
(float)data->total_frames /
(float)data->audio_format.sample_rate);
return true;
}
if (data->input_stream->seekable)
/* allow the workaround below only for nonseekable

View File

@ -80,6 +80,49 @@ flac_parse_replay_gain(struct replay_gain_info *rgi,
return found;
}
static bool
flac_find_string_comment(const FLAC__StreamMetadata *block,
const char *cmnt, char **str)
{
int offset;
size_t pos;
int len;
unsigned char tmp, *p;
*str = NULL;
offset = FLAC__metadata_object_vorbiscomment_find_entry_from(block, 0,
cmnt);
if (offset < 0)
return false;
pos = strlen(cmnt) + 1; /* 1 is for '=' */
len = block->data.vorbis_comment.comments[offset].length - pos;
if (len <= 0)
return false;
p = &block->data.vorbis_comment.comments[offset].entry[pos];
tmp = p[len];
p[len] = '\0';
*str = strdup((char *)p);
p[len] = tmp;
return true;
}
bool
flac_parse_mixramp(char **mixramp_start, char **mixramp_end,
const FLAC__StreamMetadata *block)
{
bool found = false;
if (flac_find_string_comment(block, "mixramp_start", mixramp_start))
found = true;
if (flac_find_string_comment(block, "mixramp_end", mixramp_end))
found = true;
return found;
}
/**
* Checks if the specified name matches the entry's name, and if yes,
* returns the comment value (not null-temrinated).

View File

@ -37,6 +37,10 @@ bool
flac_parse_replay_gain(struct replay_gain_info *rgi,
const FLAC__StreamMetadata *block);
bool
flac_parse_mixramp(char **mixramp_start, char **mixramp_end,
const FLAC__StreamMetadata *block);
void
flac_vorbis_comments_to_tag(struct tag *tag, const char *char_tnum,
const FLAC__StreamMetadata_VorbisComment *comment);

View File

@ -347,6 +347,47 @@ parse_id3_replay_gain_info(struct replay_gain_info *replay_gain_info,
}
#endif
#ifdef HAVE_ID3TAG
static bool
parse_id3_mixramp(char **mixramp_start, char **mixramp_end,
struct id3_tag *tag)
{
int i;
char *key;
char *value;
struct id3_frame *frame;
bool found = false;
*mixramp_start = NULL;
*mixramp_end = NULL;
for (i = 0; (frame = id3_tag_findframe(tag, "TXXX", i)); i++) {
if (frame->nfields < 3)
continue;
key = (char *)
id3_ucs4_latin1duplicate(id3_field_getstring
(&frame->fields[1]));
value = (char *)
id3_ucs4_latin1duplicate(id3_field_getstring
(&frame->fields[2]));
if (g_ascii_strcasecmp(key, "mixramp_start") == 0) {
*mixramp_start = strdup(value);
found = true;
} else if (g_ascii_strcasecmp(key, "mixramp_end") == 0) {
*mixramp_end = strdup(value);
found = true;
}
free(key);
free(value);
}
return found;
}
#endif
static void mp3_parse_id3(struct mp3_data *data, size_t tagsize,
struct tag **mpd_tag)
{
@ -403,10 +444,16 @@ static void mp3_parse_id3(struct mp3_data *data, size_t tagsize,
if (data->decoder != NULL) {
struct replay_gain_info rgi;
char *mixramp_start;
char *mixramp_end;
if (parse_id3_replay_gain_info(&rgi, id3_tag)) {
decoder_replay_gain(data->decoder, &rgi);
data->found_replay_gain = true;
}
if (parse_id3_mixramp(&mixramp_start, &mixramp_end, id3_tag)) {
g_debug("setting mixramp_tags");
decoder_mixramp(data->decoder, mixramp_start, mixramp_end);
}
}
id3_tag_delete(id3_tag);

View File

@ -427,3 +427,15 @@ decoder_replay_gain(struct decoder *decoder,
} else
decoder->replay_gain_serial = 0;
}
void
decoder_mixramp(struct decoder *decoder,
char *mixramp_start, char *mixramp_end)
{
assert(decoder != NULL);
struct decoder_control *dc = decoder->dc;
assert(dc != NULL);
dc_mixramp_start(dc, mixramp_start);
dc_mixramp_end(dc, mixramp_end);
}

View File

@ -157,4 +157,15 @@ void
decoder_replay_gain(struct decoder *decoder,
const struct replay_gain_info *replay_gain_info);
/**
* Store MixRamp tags.
*
* @param decoder the decoder object
* @param mixramp_start the mixramp_start tag; may be NULL to invalidate
* @param mixramp_end the mixramp_end tag; may be NULL to invalidate
*/
void
decoder_mixramp(struct decoder *decoder,
char *mixramp_start, char *mixramp_end);
#endif

View File

@ -22,6 +22,10 @@
#include "player_control.h"
#include <assert.h>
#include <malloc.h>
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "decoder_control"
void
dc_init(struct decoder_control *dc)
@ -33,6 +37,10 @@ dc_init(struct decoder_control *dc)
dc->state = DECODE_STATE_STOP;
dc->command = DECODE_COMMAND_NONE;
dc->mixramp_start = NULL;
dc->mixramp_end = NULL;
dc->mixramp_prev_end = NULL;
}
void
@ -40,6 +48,15 @@ dc_deinit(struct decoder_control *dc)
{
g_cond_free(dc->cond);
g_mutex_free(dc->mutex);
if (dc->mixramp_start)
free(dc->mixramp_start);
if (dc->mixramp_end)
free(dc->mixramp_end);
if (dc->mixramp_prev_end)
free(dc->mixramp_prev_end);
dc->mixramp_start = NULL;
dc->mixramp_end = NULL;
dc->mixramp_prev_end = NULL;
}
static void
@ -147,3 +164,36 @@ dc_quit(struct decoder_control *dc)
g_thread_join(dc->thread);
dc->thread = NULL;
}
void
dc_mixramp_start(struct decoder_control *dc, char *mixramp_start)
{
assert(dc != NULL);
if (dc->mixramp_start)
free(dc->mixramp_start);
dc->mixramp_start = mixramp_start;
g_debug("mixramp_start = %s", mixramp_start ? mixramp_start : "NULL");
}
void
dc_mixramp_end(struct decoder_control *dc, char *mixramp_end)
{
assert(dc != NULL);
if (dc->mixramp_end)
free(dc->mixramp_end);
dc->mixramp_end = mixramp_end;
g_debug("mixramp_end = %s", mixramp_end ? mixramp_end : "NULL");
}
void
dc_mixramp_prev_end(struct decoder_control *dc, char *mixramp_prev_end)
{
assert(dc != NULL);
if (dc->mixramp_prev_end)
free(dc->mixramp_prev_end);
dc->mixramp_prev_end = mixramp_prev_end;
g_debug("mixramp_prev_end = %s", mixramp_prev_end ? mixramp_prev_end : "NULL");
}

View File

@ -89,6 +89,10 @@ struct decoder_control {
* owns this object, and is responsible for freeing it.
*/
struct music_pipe *pipe;
char *mixramp_start;
char *mixramp_end;
char *mixramp_prev_end;
};
void
@ -235,4 +239,13 @@ dc_seek(struct decoder_control *dc, double where);
void
dc_quit(struct decoder_control *dc);
void
dc_mixramp_start(struct decoder_control *dc, char *mixramp_start);
void
dc_mixramp_end(struct decoder_control *dc, char *mixramp_end);
void
dc_mixramp_prev_end(struct decoder_control *dc, char *mixramp_prev_end);
#endif

View File

@ -23,6 +23,7 @@
#include "decoder_internal.h"
#include "decoder_list.h"
#include "decoder_plugin.h"
#include "decoder_api.h"
#include "input_stream.h"
#include "player_control.h"
#include "pipe.h"
@ -36,6 +37,9 @@
#include <unistd.h>
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "decoder_thread"
static enum decoder_command
decoder_lock_get_command(struct decoder_control *dc)
{
@ -430,6 +434,13 @@ decoder_task(gpointer arg)
switch (dc->command) {
case DECODE_COMMAND_START:
g_debug("clearing mixramp tags");
dc_mixramp_start(dc, NULL);
dc_mixramp_prev_end(dc, dc->mixramp_end);
dc->mixramp_end = NULL; /* Don't free, it's copied above. */
/* fall through */
case DECODE_COMMAND_SEEK:
decoder_run(dc);

View File

@ -137,7 +137,16 @@ pcm_mix(void *buffer1, const void *buffer2, size_t size,
const struct audio_format *format, float portion1)
{
int vol1;
float s = sin(M_PI_2 * portion1);
float s;
/* portion1 is between 0.0 and 1.0 for crossfading, MixRamp uses NaN
* to signal mixing rather than fading */
if (isnan(portion1)) {
pcm_add(buffer1, buffer2, size, PCM_VOLUME_1, PCM_VOLUME_1, format);
return;
}
s = sin(M_PI_2 * portion1);
s *= s;
vol1 = s * PCM_VOLUME_1 + 0.5;

View File

@ -30,6 +30,7 @@
#include <assert.h>
#include <stdio.h>
#include <math.h>
struct player_control pc;
@ -45,6 +46,8 @@ void pc_init(unsigned buffer_chunks, unsigned int buffered_before_play)
pc.error = PLAYER_ERROR_NOERROR;
pc.state = PLAYER_STATE_STOP;
pc.cross_fade_seconds = 0;
pc.mixramp_db = 0;
pc.mixramp_delay_seconds = nanf("");
}
void pc_deinit(void)
@ -305,6 +308,34 @@ pc_set_cross_fade(float cross_fade_seconds)
idle_add(IDLE_OPTIONS);
}
float
pc_get_mixramp_db(void)
{
return pc.mixramp_db;
}
void
pc_set_mixramp_db(float mixramp_db)
{
pc.mixramp_db = mixramp_db;
idle_add(IDLE_OPTIONS);
}
float
pc_get_mixramp_delay(void)
{
return pc.mixramp_delay_seconds;
}
void
pc_set_mixramp_delay(float mixramp_delay_seconds)
{
pc.mixramp_delay_seconds = mixramp_delay_seconds;
idle_add(IDLE_OPTIONS);
}
double
pc_get_total_play_time(void)
{

View File

@ -111,6 +111,8 @@ struct player_control {
const struct song *errored_song;
double seek_where;
float cross_fade_seconds;
float mixramp_db;
float mixramp_delay_seconds;
double total_play_time;
};
@ -250,6 +252,18 @@ pc_set_cross_fade(float cross_fade_seconds);
float
pc_get_cross_fade(void);
void
pc_set_mixramp_db(float mixramp_db);
float
pc_get_mixramp_db(void);
void
pc_set_mixramp_delay(float mixramp_delay_seconds);
float
pc_get_mixramp_delay(void);
double
pc_get_total_play_time(void);

View File

@ -644,13 +644,21 @@ play_next_chunk(struct player *player)
}
if (other_chunk != NULL) {
float mix_ratio;
chunk = music_pipe_shift(player->pipe);
assert(chunk != NULL);
if (isnan(pc.mixramp_delay_seconds)) {
mix_ratio = ((float)cross_fade_position)
/ player->cross_fade_chunks;
} else {
mix_ratio = nan("");
}
cross_fade_apply(chunk, other_chunk,
&dc->out_audio_format,
cross_fade_position,
player->cross_fade_chunks);
mix_ratio);
music_buffer_return(player_buffer, other_chunk);
} else {
/* there are not enough decoded chunks yet */
@ -865,6 +873,10 @@ static void do_play(struct decoder_control *dc)
for it */
player.cross_fade_chunks =
cross_fade_calc(pc.cross_fade_seconds, dc->total_time,
pc.mixramp_db,
pc.mixramp_delay_seconds,
dc->mixramp_start,
dc->mixramp_prev_end,
&dc->out_audio_format,
&player.play_audio_format,
music_buffer_size(player_buffer) -

View File

@ -40,6 +40,8 @@
#define PLAYLIST_STATE_FILE_CURRENT "current: "
#define PLAYLIST_STATE_FILE_TIME "time: "
#define PLAYLIST_STATE_FILE_CROSSFADE "crossfade: "
#define PLAYLIST_STATE_FILE_MIXRAMPDB "mixrampdb: "
#define PLAYLIST_STATE_FILE_MIXRAMPDELAY "mixrampdelay: "
#define PLAYLIST_STATE_FILE_PLAYLIST_BEGIN "playlist_begin"
#define PLAYLIST_STATE_FILE_PLAYLIST_END "playlist_end"
@ -90,6 +92,10 @@ playlist_state_save(FILE *fp, const struct playlist *playlist)
playlist->queue.consume);
fprintf(fp, "%s%i\n", PLAYLIST_STATE_FILE_CROSSFADE,
(int)(pc_get_cross_fade()));
fprintf(fp, "%s%f\n", PLAYLIST_STATE_FILE_MIXRAMPDB,
pc_get_mixramp_db());
fprintf(fp, "%s%f\n", PLAYLIST_STATE_FILE_MIXRAMPDELAY,
pc_get_mixramp_delay());
fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_PLAYLIST_BEGIN);
queue_save(fp, &playlist->queue);
fprintf(fp, "%s\n", PLAYLIST_STATE_FILE_PLAYLIST_END);
@ -168,6 +174,10 @@ playlist_state_restore(const char *line, FILE *fp, struct playlist *playlist)
playlist_set_consume(playlist, false);
} else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_CROSSFADE)) {
pc_set_cross_fade(atoi(buffer + strlen(PLAYLIST_STATE_FILE_CROSSFADE)));
} else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_MIXRAMPDB)) {
pc_set_mixramp_db(atof(buffer + strlen(PLAYLIST_STATE_FILE_MIXRAMPDB)));
} else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_MIXRAMPDELAY)) {
pc_set_mixramp_delay(atof(buffer + strlen(PLAYLIST_STATE_FILE_MIXRAMPDELAY)));
} else if (g_str_has_prefix(buffer, PLAYLIST_STATE_FILE_RANDOM)) {
random_mode =
strcmp(buffer + strlen(PLAYLIST_STATE_FILE_RANDOM),

View File

@ -121,6 +121,14 @@ decoder_replay_gain(G_GNUC_UNUSED struct decoder *decoder,
{
}
void
decoder_mixramp(G_GNUC_UNUSED struct decoder *decoder,
char *mixramp_start, char *mixramp_end)
{
g_free(mixramp_start);
g_free(mixramp_end);
}
static void
print_tag(const struct tag *tag)
{

View File

@ -142,6 +142,14 @@ decoder_replay_gain(G_GNUC_UNUSED struct decoder *decoder,
{
}
void
decoder_mixramp(G_GNUC_UNUSED struct decoder *decoder,
char *mixramp_start, char *mixramp_end)
{
g_free(mixramp_start);
g_free(mixramp_end);
}
int main(int argc, char **argv)
{
GError *error = NULL;