output: use the software mixer plugin
Do all the software volume stuff inside each output thread, not in the player thread. This allows one software mixer per output device, and also allows the user to configure the mixer type (hardware or software) for each audio output. This moves the global "mixer_type" setting into the "audio_output" section, deprecating the "mixer_enabled" flag.
This commit is contained in:
parent
da8095db54
commit
0275690b5c
@ -740,6 +740,7 @@ test_run_output_SOURCES = test/run_output.c \
|
|||||||
$(ENCODER_SRC) \
|
$(ENCODER_SRC) \
|
||||||
src/mixer_api.c \
|
src/mixer_api.c \
|
||||||
src/mixer_control.c \
|
src/mixer_control.c \
|
||||||
|
src/mixer_type.c \
|
||||||
$(MIXER_SRC) \
|
$(MIXER_SRC) \
|
||||||
src/filter_plugin.c src/filter/chain_filter_plugin.c \
|
src/filter_plugin.c src/filter/chain_filter_plugin.c \
|
||||||
src/filter/convert_filter_plugin.c \
|
src/filter/convert_filter_plugin.c \
|
||||||
|
2
NEWS
2
NEWS
@ -7,6 +7,8 @@ ver 0.16 (20??/??/??)
|
|||||||
- ffmpeg: support multiple tags
|
- ffmpeg: support multiple tags
|
||||||
* mixers:
|
* mixers:
|
||||||
- removed support for legacy mixer configuration
|
- removed support for legacy mixer configuration
|
||||||
|
- reimplemented software volume as mixer+filter plugin
|
||||||
|
- per-device software/hardware mixer setting
|
||||||
* commands:
|
* commands:
|
||||||
- added new "status" line with more precise "elapsed time"
|
- added new "status" line with more precise "elapsed time"
|
||||||
* log unused/unknown block parameters
|
* log unused/unknown block parameters
|
||||||
|
@ -285,6 +285,12 @@ whatever audio format is passed to the audio output.
|
|||||||
.B device <dev>
|
.B device <dev>
|
||||||
This specifies the device to use for audio output. The default is "default".
|
This specifies the device to use for audio output. The default is "default".
|
||||||
.TP
|
.TP
|
||||||
|
.B mixer_type <hardware, software or none>
|
||||||
|
Specifies which mixer should be used for this audio output: the
|
||||||
|
hardware mixer (available for ALSA, OSS and PulseAudio), the software
|
||||||
|
mixer or no mixer ("none"). By default, the hardware mixer is used
|
||||||
|
for devices which support it, and none for the others.
|
||||||
|
.TP
|
||||||
.B mixer_device <mixer dev>
|
.B mixer_device <mixer dev>
|
||||||
This specifies which mixer to use. The default is "default". To use
|
This specifies which mixer to use. The default is "default". To use
|
||||||
the second sound card in a system, use "hw:1".
|
the second sound card in a system, use "hw:1".
|
||||||
|
@ -179,6 +179,7 @@ input {
|
|||||||
# name "My ALSA Device"
|
# name "My ALSA Device"
|
||||||
# device "hw:0,0" # optional
|
# device "hw:0,0" # optional
|
||||||
# format "44100:16:2" # optional
|
# format "44100:16:2" # optional
|
||||||
|
# mixer_type "hardware" # optional
|
||||||
# mixer_device "default" # optional
|
# mixer_device "default" # optional
|
||||||
# mixer_control "PCM" # optional
|
# mixer_control "PCM" # optional
|
||||||
# mixer_index "0" # optional
|
# mixer_index "0" # optional
|
||||||
@ -191,6 +192,7 @@ input {
|
|||||||
# name "My OSS Device"
|
# name "My OSS Device"
|
||||||
# device "/dev/dsp" # optional
|
# device "/dev/dsp" # optional
|
||||||
# format "44100:16:2" # optional
|
# format "44100:16:2" # optional
|
||||||
|
# mixer_type "hardware" # optional
|
||||||
# mixer_device "/dev/mixer" # optional
|
# mixer_device "/dev/mixer" # optional
|
||||||
# mixer_control "PCM" # optional
|
# mixer_control "PCM" # optional
|
||||||
#}
|
#}
|
||||||
@ -214,6 +216,7 @@ input {
|
|||||||
# genre "jazz" # optional
|
# genre "jazz" # optional
|
||||||
# public "no" # optional
|
# public "no" # optional
|
||||||
# timeout "2" # optional
|
# timeout "2" # optional
|
||||||
|
# mixer_type "software" # optional
|
||||||
#}
|
#}
|
||||||
#
|
#
|
||||||
# An example of a httpd output (built-in HTTP streaming server):
|
# An example of a httpd output (built-in HTTP streaming server):
|
||||||
@ -255,6 +258,7 @@ input {
|
|||||||
#audio_output {
|
#audio_output {
|
||||||
# type "null"
|
# type "null"
|
||||||
# name "My Null Output"
|
# name "My Null Output"
|
||||||
|
# mixer_type "none" # optional
|
||||||
#}
|
#}
|
||||||
#
|
#
|
||||||
# This setting will change all decoded audio to be converted to the specified
|
# This setting will change all decoded audio to be converted to the specified
|
||||||
@ -273,33 +277,6 @@ input {
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
# Volume control mixer ########################################################
|
|
||||||
#
|
|
||||||
# These are the global volume control settings. By default, this setting will
|
|
||||||
# be detected to the available audio output device, with preference going to
|
|
||||||
# hardware mixing. Hardware and software mixers for individual audio_output
|
|
||||||
# sections cannot yet be mixed.
|
|
||||||
#
|
|
||||||
# An example for controlling an ALSA, OSS or Pulseaudio mixer; If this
|
|
||||||
# setting is used other sound applications will be affected by the volume
|
|
||||||
# being controlled by MPD.
|
|
||||||
#
|
|
||||||
#mixer_type "hardware"
|
|
||||||
#
|
|
||||||
# An example for controlling all mixers through software. This will control
|
|
||||||
# all controls, even if the mixer is not supported by the device and will not
|
|
||||||
# affect any other sound producing applications.
|
|
||||||
#
|
|
||||||
#mixer_type "software"
|
|
||||||
#
|
|
||||||
# This example will not allow MPD to touch the mixer at all and will disable
|
|
||||||
# all volume controls.
|
|
||||||
#
|
|
||||||
#mixer_type "disabled"
|
|
||||||
#
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
# Normalization automatic volume adjustments ##################################
|
# Normalization automatic volume adjustments ##################################
|
||||||
#
|
#
|
||||||
# This setting specifies the type of ReplayGain to use. This setting can have
|
# This setting specifies the type of ReplayGain to use. This setting can have
|
||||||
|
12
doc/user.xml
12
doc/user.xml
@ -289,13 +289,15 @@ cd mpd-0.14.2</programlisting>
|
|||||||
</row>
|
</row>
|
||||||
<row>
|
<row>
|
||||||
<entry>
|
<entry>
|
||||||
<varname>mixer_enabled</varname>
|
<varname>mixer_type</varname>
|
||||||
<parameter>yes|no</parameter>
|
<parameter>hardware|software|none</parameter>
|
||||||
</entry>
|
</entry>
|
||||||
<entry>
|
<entry>
|
||||||
Specifies whether the hardware mixer of this audio
|
Specifies which mixer should be used for this audio
|
||||||
output should be used. By default, all hardware
|
output: the hardware mixer (available for ALSA, OSS
|
||||||
mixers are enabled if available.
|
and PulseAudio), the software mixer or no mixer
|
||||||
|
("none"). By default, the hardware mixer is used for
|
||||||
|
devices which support it, and none for the others.
|
||||||
</entry>
|
</entry>
|
||||||
</row>
|
</row>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -28,24 +28,11 @@
|
|||||||
#undef G_LOG_DOMAIN
|
#undef G_LOG_DOMAIN
|
||||||
#define G_LOG_DOMAIN "mixer"
|
#define G_LOG_DOMAIN "mixer"
|
||||||
|
|
||||||
static bool mixers_enabled = true;
|
|
||||||
|
|
||||||
void
|
|
||||||
mixer_disable_all(void)
|
|
||||||
{
|
|
||||||
g_debug("mixer api is disabled");
|
|
||||||
mixers_enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct mixer *
|
struct mixer *
|
||||||
mixer_new(const struct mixer_plugin *plugin, const struct config_param *param)
|
mixer_new(const struct mixer_plugin *plugin, const struct config_param *param)
|
||||||
{
|
{
|
||||||
struct mixer *mixer;
|
struct mixer *mixer;
|
||||||
|
|
||||||
//mixers are disabled (by using software volume)
|
|
||||||
if (!mixers_enabled) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
assert(plugin != NULL);
|
assert(plugin != NULL);
|
||||||
|
|
||||||
mixer = plugin->init(param);
|
mixer = plugin->init(param);
|
||||||
|
@ -31,9 +31,6 @@ struct mixer;
|
|||||||
struct mixer_plugin;
|
struct mixer_plugin;
|
||||||
struct config_param;
|
struct config_param;
|
||||||
|
|
||||||
void
|
|
||||||
mixer_disable_all(void);
|
|
||||||
|
|
||||||
struct mixer *
|
struct mixer *
|
||||||
mixer_new(const struct mixer_plugin *plugin, const struct config_param *param);
|
mixer_new(const struct mixer_plugin *plugin, const struct config_param *param);
|
||||||
|
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
#include "output_list.h"
|
#include "output_list.h"
|
||||||
#include "audio_parser.h"
|
#include "audio_parser.h"
|
||||||
#include "mixer_control.h"
|
#include "mixer_control.h"
|
||||||
|
#include "mixer_type.h"
|
||||||
|
#include "mixer_list.h"
|
||||||
|
#include "mixer/software_mixer_plugin.h"
|
||||||
#include "filter_plugin.h"
|
#include "filter_plugin.h"
|
||||||
#include "filter_registry.h"
|
#include "filter_registry.h"
|
||||||
#include "filter/chain_filter_plugin.h"
|
#include "filter/chain_filter_plugin.h"
|
||||||
@ -61,17 +64,59 @@ audio_output_detect(GError **error)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the mixer type which should be used for the specified
|
||||||
|
* configuration block.
|
||||||
|
*
|
||||||
|
* This handles the deprecated options mixer_type (global) and
|
||||||
|
* mixer_enabled, if the mixer_type setting is not configured.
|
||||||
|
*/
|
||||||
|
static enum mixer_type
|
||||||
|
audio_output_mixer_type(const struct config_param *param)
|
||||||
|
{
|
||||||
|
/* read the local "mixer_type" setting */
|
||||||
|
const char *p = config_get_block_string(param, "mixer_type", NULL);
|
||||||
|
if (p != NULL)
|
||||||
|
return mixer_type_parse(p);
|
||||||
|
|
||||||
|
/* try the local "mixer_enabled" setting next (deprecated) */
|
||||||
|
if (!config_get_block_bool(param, "mixer_enabled", true))
|
||||||
|
return MIXER_TYPE_NONE;
|
||||||
|
|
||||||
|
/* fall back to the global "mixer_type" setting (also
|
||||||
|
deprecated) */
|
||||||
|
return mixer_type_parse(config_get_string("mixer_type", "hardware"));
|
||||||
|
}
|
||||||
|
|
||||||
static struct mixer *
|
static struct mixer *
|
||||||
audio_output_load_mixer(const struct config_param *param,
|
audio_output_load_mixer(const struct config_param *param,
|
||||||
const struct mixer_plugin *plugin)
|
const struct mixer_plugin *plugin,
|
||||||
|
struct filter *filter_chain)
|
||||||
{
|
{
|
||||||
if (!config_get_block_bool(param, "mixer_enabled", true))
|
struct mixer *mixer;
|
||||||
|
|
||||||
|
switch (audio_output_mixer_type(param)) {
|
||||||
|
case MIXER_TYPE_NONE:
|
||||||
|
case MIXER_TYPE_UNKNOWN:
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
case MIXER_TYPE_HARDWARE:
|
||||||
if (plugin == NULL)
|
if (plugin == NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
return mixer_new(plugin, param);
|
return mixer_new(plugin, param);
|
||||||
|
|
||||||
|
case MIXER_TYPE_SOFTWARE:
|
||||||
|
mixer = mixer_new(&software_mixer_plugin, NULL);
|
||||||
|
assert(mixer != NULL);
|
||||||
|
|
||||||
|
filter_chain_append(filter_chain,
|
||||||
|
software_mixer_get_filter(mixer));
|
||||||
|
return mixer;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(false);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -152,7 +197,8 @@ audio_output_init(struct audio_output *ao, const struct config_param *param,
|
|||||||
if (ao->data == NULL)
|
if (ao->data == NULL)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ao->mixer = audio_output_load_mixer(param, plugin->mixer_plugin);
|
ao->mixer = audio_output_load_mixer(param, plugin->mixer_plugin,
|
||||||
|
ao->filter);
|
||||||
|
|
||||||
/* the "convert" filter must be the last one in the chain */
|
/* the "convert" filter must be the last one in the chain */
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ void pc_init(unsigned buffer_chunks, unsigned int buffered_before_play)
|
|||||||
pc.error = PLAYER_ERROR_NOERROR;
|
pc.error = PLAYER_ERROR_NOERROR;
|
||||||
pc.state = PLAYER_STATE_STOP;
|
pc.state = PLAYER_STATE_STOP;
|
||||||
pc.cross_fade_seconds = 0;
|
pc.cross_fade_seconds = 0;
|
||||||
pc.software_volume = PCM_VOLUME_1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void pc_deinit(void)
|
void pc_deinit(void)
|
||||||
@ -253,16 +252,6 @@ void setPlayerCrossFade(float crossFadeInSeconds)
|
|||||||
idle_add(IDLE_OPTIONS);
|
idle_add(IDLE_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPlayerSoftwareVolume(int volume)
|
|
||||||
{
|
|
||||||
if (volume > PCM_VOLUME_1)
|
|
||||||
volume = PCM_VOLUME_1;
|
|
||||||
else if (volume < 0)
|
|
||||||
volume = 0;
|
|
||||||
|
|
||||||
pc.software_volume = volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
double getPlayerTotalPlayTime(void)
|
double getPlayerTotalPlayTime(void)
|
||||||
{
|
{
|
||||||
return pc.total_play_time;
|
return pc.total_play_time;
|
||||||
|
@ -81,7 +81,6 @@ struct player_control {
|
|||||||
struct song *errored_song;
|
struct song *errored_song;
|
||||||
volatile double seek_where;
|
volatile double seek_where;
|
||||||
float cross_fade_seconds;
|
float cross_fade_seconds;
|
||||||
uint16_t software_volume;
|
|
||||||
double total_play_time;
|
double total_play_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -145,8 +144,6 @@ void setPlayerCrossFade(float crossFadeInSeconds);
|
|||||||
|
|
||||||
float getPlayerCrossFade(void);
|
float getPlayerCrossFade(void);
|
||||||
|
|
||||||
void setPlayerSoftwareVolume(int volume);
|
|
||||||
|
|
||||||
double getPlayerTotalPlayTime(void);
|
double getPlayerTotalPlayTime(void);
|
||||||
|
|
||||||
static inline const struct audio_format *
|
static inline const struct audio_format *
|
||||||
|
@ -423,8 +423,6 @@ static bool
|
|||||||
play_chunk(struct song *song, struct music_chunk *chunk,
|
play_chunk(struct song *song, struct music_chunk *chunk,
|
||||||
const struct audio_format *format, double sizeToTime)
|
const struct audio_format *format, double sizeToTime)
|
||||||
{
|
{
|
||||||
bool success;
|
|
||||||
|
|
||||||
assert(music_chunk_check_format(chunk, format));
|
assert(music_chunk_check_format(chunk, format));
|
||||||
|
|
||||||
if (chunk->tag != NULL) {
|
if (chunk->tag != NULL) {
|
||||||
@ -455,18 +453,6 @@ play_chunk(struct song *song, struct music_chunk *chunk,
|
|||||||
pc.elapsed_time = chunk->times;
|
pc.elapsed_time = chunk->times;
|
||||||
pc.bit_rate = chunk->bit_rate;
|
pc.bit_rate = chunk->bit_rate;
|
||||||
|
|
||||||
/* apply software volume */
|
|
||||||
|
|
||||||
success = pcm_volume(chunk->data, chunk->length,
|
|
||||||
format, pc.software_volume);
|
|
||||||
if (!success) {
|
|
||||||
g_warning("pcm_volume() failed on %u:%u:%u",
|
|
||||||
format->sample_rate, format->bits, format->channels);
|
|
||||||
pc.errored_song = dc.current_song;
|
|
||||||
pc.error = PLAYER_ERROR_AUDIO;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* send the chunk to the audio outputs */
|
/* send the chunk to the audio outputs */
|
||||||
|
|
||||||
if (!audio_output_all_play(chunk)) {
|
if (!audio_output_all_play(chunk)) {
|
||||||
|
71
src/volume.c
71
src/volume.c
@ -40,8 +40,6 @@
|
|||||||
|
|
||||||
#define SW_VOLUME_STATE "sw_volume: "
|
#define SW_VOLUME_STATE "sw_volume: "
|
||||||
|
|
||||||
static enum mixer_type volume_mixer_type = MIXER_TYPE_HARDWARE;
|
|
||||||
|
|
||||||
static unsigned volume_software_set = 100;
|
static unsigned volume_software_set = 100;
|
||||||
|
|
||||||
/** the cached hardware mixer value; invalid if negative */
|
/** the cached hardware mixer value; invalid if negative */
|
||||||
@ -51,37 +49,15 @@ static GTimer *hardware_volume_timer;
|
|||||||
|
|
||||||
void volume_finish(void)
|
void volume_finish(void)
|
||||||
{
|
{
|
||||||
if (volume_mixer_type == MIXER_TYPE_HARDWARE)
|
|
||||||
g_timer_destroy(hardware_volume_timer);
|
g_timer_destroy(hardware_volume_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void volume_init(void)
|
void volume_init(void)
|
||||||
{
|
{
|
||||||
const struct config_param *param = config_get_param(CONF_MIXER_TYPE);
|
|
||||||
//hw mixing is by default
|
|
||||||
if (param) {
|
|
||||||
volume_mixer_type = mixer_type_parse(param->value);
|
|
||||||
switch (volume_mixer_type) {
|
|
||||||
case MIXER_TYPE_NONE:
|
|
||||||
case MIXER_TYPE_SOFTWARE:
|
|
||||||
mixer_disable_all();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MIXER_TYPE_HARDWARE:
|
|
||||||
//nothing to do
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MIXER_TYPE_UNKNOWN:
|
|
||||||
g_error("unknown mixer type %s at line %i\n",
|
|
||||||
param->value, param->line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (volume_mixer_type == MIXER_TYPE_HARDWARE)
|
|
||||||
hardware_volume_timer = g_timer_new();
|
hardware_volume_timer = g_timer_new();
|
||||||
}
|
}
|
||||||
|
|
||||||
static int hardware_volume_get(void)
|
int volume_level_get(void)
|
||||||
{
|
{
|
||||||
assert(hardware_volume_timer != NULL);
|
assert(hardware_volume_timer != NULL);
|
||||||
|
|
||||||
@ -95,43 +71,12 @@ static int hardware_volume_get(void)
|
|||||||
return last_hardware_volume;
|
return last_hardware_volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int software_volume_get(void)
|
|
||||||
{
|
|
||||||
return volume_software_set;
|
|
||||||
}
|
|
||||||
|
|
||||||
int volume_level_get(void)
|
|
||||||
{
|
|
||||||
switch (volume_mixer_type) {
|
|
||||||
case MIXER_TYPE_SOFTWARE:
|
|
||||||
return software_volume_get();
|
|
||||||
case MIXER_TYPE_HARDWARE:
|
|
||||||
return hardware_volume_get();
|
|
||||||
case MIXER_TYPE_NONE:
|
|
||||||
case MIXER_TYPE_UNKNOWN:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* unreachable */
|
|
||||||
assert(false);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool software_volume_change(unsigned volume)
|
static bool software_volume_change(unsigned volume)
|
||||||
{
|
{
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
volume_software_set = volume;
|
volume_software_set = volume;
|
||||||
|
mixer_all_set_software_volume(volume);
|
||||||
if (volume >= 100)
|
|
||||||
volume = PCM_VOLUME_1;
|
|
||||||
else if (volume <= 0)
|
|
||||||
volume = 0;
|
|
||||||
else
|
|
||||||
volume = pcm_float_to_volume((exp(volume / 25.0) - 1) /
|
|
||||||
(54.5981500331F - 1));
|
|
||||||
|
|
||||||
setPlayerSoftwareVolume(volume);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -148,16 +93,11 @@ bool volume_level_change(unsigned volume)
|
|||||||
{
|
{
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
|
volume_software_set = volume;
|
||||||
|
|
||||||
idle_add(IDLE_MIXER);
|
idle_add(IDLE_MIXER);
|
||||||
|
|
||||||
switch (volume_mixer_type) {
|
|
||||||
case MIXER_TYPE_HARDWARE:
|
|
||||||
return hardware_volume_change(volume);
|
return hardware_volume_change(volume);
|
||||||
case MIXER_TYPE_SOFTWARE:
|
|
||||||
return software_volume_change(volume);
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void read_sw_volume_state(FILE *fp)
|
void read_sw_volume_state(FILE *fp)
|
||||||
@ -166,8 +106,6 @@ void read_sw_volume_state(FILE *fp)
|
|||||||
char *end = NULL;
|
char *end = NULL;
|
||||||
long int sv;
|
long int sv;
|
||||||
|
|
||||||
if (volume_mixer_type != MIXER_TYPE_SOFTWARE)
|
|
||||||
return;
|
|
||||||
while (fgets(buf, sizeof(buf), fp)) {
|
while (fgets(buf, sizeof(buf), fp)) {
|
||||||
if (!g_str_has_prefix(buf, SW_VOLUME_STATE))
|
if (!g_str_has_prefix(buf, SW_VOLUME_STATE))
|
||||||
continue;
|
continue;
|
||||||
@ -184,6 +122,5 @@ void read_sw_volume_state(FILE *fp)
|
|||||||
|
|
||||||
void save_sw_volume_state(FILE *fp)
|
void save_sw_volume_state(FILE *fp)
|
||||||
{
|
{
|
||||||
if (volume_mixer_type == MIXER_TYPE_SOFTWARE)
|
|
||||||
fprintf(fp, SW_VOLUME_STATE "%u\n", volume_software_set);
|
fprintf(fp, SW_VOLUME_STATE "%u\n", volume_software_set);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user