Fixed:
* Prepare code for exposing multi_audio interface on Audio Function Group level * Some minor cleanups. Todo: * Publish devfs entries based on available Function Groups instead of HDA cards. * Publish all mixer controls / output busses through multi_audio interface. * Make recording work :) * Check why we need 4096 samples to play ok. * Figure out problems with driver on Haiku... currently only successfully tested on R5. As you can see, plenty of things left to do, but progressing nicely. At least people can have sound on R5 now! git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@21159 a95241bf-73f2-0310-859d-f6bbb57e9c96
This commit is contained in:
parent
7c8482e991
commit
9432d60991
@ -24,13 +24,15 @@
|
||||
/* values for the class_sub field for class_base = 0x04 (multimedia device) */
|
||||
#define PCI_hd_audio 3
|
||||
|
||||
#define MAXWORK 16
|
||||
#define HDA_MAXAFGS 15
|
||||
#define HDA_MAXCODECS 15
|
||||
#define HDA_MAXSTREAMS 16
|
||||
#define MAX_CODEC_RESPONSES 10
|
||||
#define MAXINPUTS 32
|
||||
|
||||
typedef struct hda_controller_s hda_controller;
|
||||
typedef struct hda_codec_s hda_codec;
|
||||
typedef struct hda_afg_s hda_afg;
|
||||
|
||||
#define STRMAXBUF 10
|
||||
#define STRMINBUF 2
|
||||
@ -40,6 +42,12 @@ enum {
|
||||
STRM_RECORD
|
||||
};
|
||||
|
||||
/* hda_stream_info
|
||||
*
|
||||
* This structure describes a single stream of audio data,
|
||||
* which is can have multiple channels (for stereo or better).
|
||||
*/
|
||||
|
||||
typedef struct hda_stream_info_s {
|
||||
uint32 id; /* HDA controller stream # */
|
||||
uint32 off; /* HDA I/O/B descriptor offset */
|
||||
@ -68,27 +76,29 @@ typedef struct hda_stream_info_s {
|
||||
uint32 bdl_pa; /* BDL physical address */
|
||||
} hda_stream;
|
||||
|
||||
typedef struct hda_codec_s {
|
||||
uint16 vendor_id;
|
||||
uint16 product_id;
|
||||
uint8 hda_rev;
|
||||
uint16 rev_stepping;
|
||||
uint8 addr;
|
||||
/* hda_afg
|
||||
*
|
||||
* This structure describes a single Audio Function Group. An afg
|
||||
* is a group of audio widgets which can be used to configure multiple
|
||||
* streams of audio either from the HDA Link to an output device (= playback)
|
||||
* or from an input device to the HDA link (= recording).
|
||||
*/
|
||||
|
||||
sem_id response_sem;
|
||||
uint32 responses[MAX_CODEC_RESPONSES];
|
||||
uint32 response_count;
|
||||
struct hda_afg_s {
|
||||
hda_codec* codec;
|
||||
|
||||
/* Multi Audio API data */
|
||||
hda_stream* playback_stream;
|
||||
hda_stream* record_stream;
|
||||
|
||||
/* Function Group Data */
|
||||
uint32 afg_nid;
|
||||
uint32 afg_wid_start, afg_wid_count;
|
||||
uint32 afg_deffmts;
|
||||
uint32 afg_defrates;
|
||||
uint32 afg_defpm;
|
||||
|
||||
uint32 root_nid,
|
||||
wid_start,
|
||||
wid_count;
|
||||
|
||||
uint32 deffmts,
|
||||
defrates,
|
||||
defpm;
|
||||
|
||||
struct {
|
||||
uint32 num_inputs;
|
||||
@ -116,10 +126,43 @@ typedef struct hda_codec_s {
|
||||
pin_dev_type device;
|
||||
} pin;
|
||||
} d;
|
||||
} *afg_widgets;
|
||||
} *widgets;
|
||||
};
|
||||
|
||||
/* hda_codec
|
||||
*
|
||||
* This structure describes a single codec module in the
|
||||
* HDA compliant device. This is a discrete component, which
|
||||
* can contain both Audio Function Groups, Modem Function Groups,
|
||||
* and other customized (vendor specific) Function Groups.
|
||||
*
|
||||
* NOTE: Atm, only Audio Function Groups are supported.
|
||||
*/
|
||||
|
||||
struct hda_codec_s {
|
||||
uint16 vendor_id;
|
||||
uint16 product_id;
|
||||
uint8 hda_rev;
|
||||
uint16 rev_stepping;
|
||||
uint8 addr;
|
||||
|
||||
sem_id response_sem;
|
||||
uint32 responses[MAX_CODEC_RESPONSES];
|
||||
uint32 response_count;
|
||||
|
||||
hda_afg* afgs[HDA_MAXAFGS];
|
||||
uint32 num_afgs;
|
||||
|
||||
struct hda_controller_s* ctrlr;
|
||||
} hda_codec;
|
||||
};
|
||||
|
||||
/* hda_controller
|
||||
*
|
||||
* This structure describes a single HDA compliant
|
||||
* controller. It contains a list of available streams
|
||||
* for use by the codecs contained, and the messaging queue
|
||||
* (verb/response) buffers for communication.
|
||||
*/
|
||||
|
||||
struct hda_controller_s {
|
||||
pci_info pcii;
|
||||
@ -149,21 +192,27 @@ struct hda_controller_s {
|
||||
hda_stream* streams[HDA_MAXSTREAMS];
|
||||
};
|
||||
|
||||
/* driver.c */
|
||||
extern device_hooks driver_hooks;
|
||||
|
||||
extern pci_module_info* pci;
|
||||
|
||||
extern hda_controller cards[MAXCARDS];
|
||||
extern uint32 num_cards;
|
||||
|
||||
/* hda_codec.c */
|
||||
hda_codec* hda_codec_new(hda_controller* ctrlr, uint32 cad);
|
||||
|
||||
/* hda_multi_audio.c */
|
||||
status_t multi_audio_control(void* cookie, uint32 op, void* arg, size_t len);
|
||||
|
||||
/* hda_controller.c: Basic controller support */
|
||||
status_t hda_hw_init(hda_controller* ctrlr);
|
||||
void hda_hw_stop(hda_controller* ctrlr);
|
||||
void hda_hw_uninit(hda_controller* ctrlr);
|
||||
hda_codec* hda_codec_new(hda_controller* ctrlr, uint32 cad);
|
||||
status_t hda_send_verbs(hda_codec* codec, corb_t* verbs, uint32* responses, int count);
|
||||
status_t multi_audio_control(void* cookie, uint32 op, void* arg, size_t len);
|
||||
|
||||
/* hda_controller.c: Stream support */
|
||||
hda_stream* hda_stream_alloc(hda_controller* ctrlr, int type);
|
||||
status_t hda_stream_setup_buffers(hda_codec* codec, hda_stream* s, const char* desc);
|
||||
status_t hda_stream_setup_buffers(hda_afg* afg, hda_stream* s, const char* desc);
|
||||
status_t hda_stream_start(hda_controller* ctrlr, hda_stream* s);
|
||||
status_t hda_stream_stop(hda_controller* ctrlr, hda_stream* s);
|
||||
status_t hda_stream_check_intr(hda_controller* ctrlr, hda_stream* s);
|
||||
|
@ -101,20 +101,20 @@ hda_widget_get_amplifier_capabilities(hda_codec* codec, uint32 nid)
|
||||
}
|
||||
|
||||
static status_t
|
||||
hda_codec_parse_afg(hda_codec* codec, uint32 afg_nid)
|
||||
hda_codec_parse_afg(hda_afg* afg)
|
||||
{
|
||||
corb_t verbs[6];
|
||||
uint32 resp[6];
|
||||
uint32 widx;
|
||||
|
||||
hda_widget_get_stream_support(codec, afg_nid, &codec->afg_deffmts, &codec->afg_defrates);
|
||||
hda_widget_get_pm_support(codec, afg_nid, &codec->afg_defpm);
|
||||
hda_widget_get_stream_support(afg->codec, afg->root_nid, &afg->deffmts, &afg->defrates);
|
||||
hda_widget_get_pm_support(afg->codec, afg->root_nid, &afg->defpm);
|
||||
|
||||
verbs[0] = MAKE_VERB(codec->addr,afg_nid,VID_GET_PARAM,PID_AUDIO_FG_CAP);
|
||||
verbs[1] = MAKE_VERB(codec->addr,afg_nid,VID_GET_PARAM,PID_GPIO_COUNT);
|
||||
verbs[2] = MAKE_VERB(codec->addr,afg_nid,VID_GET_PARAM,PID_SUBORD_NODE_COUNT);
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,afg->root_nid,VID_GET_PARAM,PID_AUDIO_FG_CAP);
|
||||
verbs[1] = MAKE_VERB(afg->codec->addr,afg->root_nid,VID_GET_PARAM,PID_GPIO_COUNT);
|
||||
verbs[2] = MAKE_VERB(afg->codec->addr,afg->root_nid,VID_GET_PARAM,PID_SUBORD_NODE_COUNT);
|
||||
|
||||
if (hda_send_verbs(codec, verbs, resp, 3) == B_OK) {
|
||||
if (hda_send_verbs(afg->codec, verbs, resp, 3) == B_OK) {
|
||||
dprintf("%s: Output delay: %ld samples, Input delay: %ld samples, Beep Generator: %s\n", __func__,
|
||||
resp[0] & 0xf, (resp[0] >> 8) & 0xf, (resp[0] & (1 << 16)) ? "yes" : "no");
|
||||
|
||||
@ -123,30 +123,27 @@ hda_codec_parse_afg(hda_codec* codec, uint32 afg_nid)
|
||||
(resp[1] & (1 << 30)) ? "yes" : "no",
|
||||
(resp[1] & (1 << 31)) ? "yes" : "no");
|
||||
|
||||
codec->afg_wid_start = resp[2] >> 16;
|
||||
codec->afg_wid_count = resp[2] & 0xFF;
|
||||
afg->wid_start = resp[2] >> 16;
|
||||
afg->wid_count = resp[2] & 0xFF;
|
||||
|
||||
codec->afg_widgets = calloc(sizeof(*codec->afg_widgets), codec->afg_wid_count);
|
||||
if (codec->afg_widgets == NULL) {
|
||||
afg->widgets = calloc(sizeof(*afg->widgets), afg->wid_count);
|
||||
if (afg->widgets == NULL) {
|
||||
dprintf("ERROR: Not enough memory!\n");
|
||||
return B_NO_MEMORY;
|
||||
}
|
||||
|
||||
/* Only now mark the AFG as found and used */
|
||||
codec->afg_nid = afg_nid;
|
||||
|
||||
/* Iterate over all Widgets and collect info */
|
||||
for (widx=0; widx < codec->afg_wid_count; widx++) {
|
||||
uint32 wid = codec->afg_wid_start + widx;
|
||||
for (widx=0; widx < afg->wid_count; widx++) {
|
||||
uint32 wid = afg->wid_start + widx;
|
||||
char buf[256];
|
||||
int off;
|
||||
|
||||
verbs[0] = MAKE_VERB(codec->addr,wid,VID_GET_PARAM,PID_AUDIO_WIDGET_CAP);
|
||||
verbs[1] = MAKE_VERB(codec->addr,wid,VID_GET_PARAM,PID_CONNLIST_LEN);
|
||||
hda_send_verbs(codec, verbs, resp, 2);
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,wid,VID_GET_PARAM,PID_AUDIO_WIDGET_CAP);
|
||||
verbs[1] = MAKE_VERB(afg->codec->addr,wid,VID_GET_PARAM,PID_CONNLIST_LEN);
|
||||
hda_send_verbs(afg->codec, verbs, resp, 2);
|
||||
|
||||
codec->afg_widgets[widx].type = resp[0] >> 20;
|
||||
codec->afg_widgets[widx].num_inputs = resp[1] & 0x7F;
|
||||
afg->widgets[widx].type = resp[0] >> 20;
|
||||
afg->widgets[widx].num_inputs = resp[1] & 0x7F;
|
||||
|
||||
off = 0;
|
||||
if (resp[0] & (1 << 11)) off += sprintf(buf+off, "[L-R Swap] ");
|
||||
@ -161,48 +158,48 @@ hda_codec_parse_afg(hda_codec* codec, uint32 afg_nid)
|
||||
if (resp[0] & (1 << 1)) off += sprintf(buf+off, "[In Amp] ");
|
||||
if (resp[0] & (1 << 0)) off += sprintf(buf+off, "[Stereo] ");
|
||||
|
||||
switch(codec->afg_widgets[widx].type) {
|
||||
switch(afg->widgets[widx].type) {
|
||||
case WT_AUDIO_OUTPUT:
|
||||
dprintf("%ld:\tAudio Output\n", wid);
|
||||
hda_widget_get_stream_support(codec, wid,
|
||||
&codec->afg_widgets[widx].d.input.formats,
|
||||
&codec->afg_widgets[widx].d.input.rates);
|
||||
hda_widget_get_amplifier_capabilities(codec, wid);
|
||||
hda_widget_get_stream_support(afg->codec, wid,
|
||||
&afg->widgets[widx].d.input.formats,
|
||||
&afg->widgets[widx].d.input.rates);
|
||||
hda_widget_get_amplifier_capabilities(afg->codec, wid);
|
||||
break;
|
||||
case WT_AUDIO_INPUT:
|
||||
dprintf("%ld:\tAudio Input\n", wid);
|
||||
hda_widget_get_stream_support(codec, wid,
|
||||
&codec->afg_widgets[widx].d.input.formats,
|
||||
&codec->afg_widgets[widx].d.input.rates);
|
||||
hda_widget_get_amplifier_capabilities(codec, wid);
|
||||
hda_widget_get_stream_support(afg->codec, wid,
|
||||
&afg->widgets[widx].d.input.formats,
|
||||
&afg->widgets[widx].d.input.rates);
|
||||
hda_widget_get_amplifier_capabilities(afg->codec, wid);
|
||||
break;
|
||||
case WT_AUDIO_MIXER:
|
||||
dprintf("%ld:\tAudio Mixer\n", wid);
|
||||
hda_widget_get_amplifier_capabilities(codec, wid);
|
||||
hda_widget_get_amplifier_capabilities(afg->codec, wid);
|
||||
break;
|
||||
case WT_AUDIO_SELECTOR:
|
||||
dprintf("%ld:\tAudio Selector\n", wid);
|
||||
hda_widget_get_amplifier_capabilities(codec, wid);
|
||||
hda_widget_get_amplifier_capabilities(afg->codec, wid);
|
||||
break;
|
||||
case WT_PIN_COMPLEX:
|
||||
dprintf("%ld:\tPin Complex\n", wid);
|
||||
verbs[0] = MAKE_VERB(codec->addr,wid,VID_GET_PARAM,PID_PIN_CAP);
|
||||
if (hda_send_verbs(codec, verbs, resp, 1) == B_OK) {
|
||||
codec->afg_widgets[widx].d.pin.input = resp[0] & (1 << 5);
|
||||
codec->afg_widgets[widx].d.pin.output = resp[0] & (1 << 4);
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,wid,VID_GET_PARAM,PID_PIN_CAP);
|
||||
if (hda_send_verbs(afg->codec, verbs, resp, 1) == B_OK) {
|
||||
afg->widgets[widx].d.pin.input = resp[0] & (1 << 5);
|
||||
afg->widgets[widx].d.pin.output = resp[0] & (1 << 4);
|
||||
}
|
||||
|
||||
verbs[0] = MAKE_VERB(codec->addr,wid,VID_GET_CFGDEFAULT,0);
|
||||
if (hda_send_verbs(codec, verbs, resp, 1) == B_OK) {
|
||||
codec->afg_widgets[widx].d.pin.device = (resp[0] >> 20) & 0xF;
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,wid,VID_GET_CFGDEFAULT,0);
|
||||
if (hda_send_verbs(afg->codec, verbs, resp, 1) == B_OK) {
|
||||
afg->widgets[widx].d.pin.device = (resp[0] >> 20) & 0xF;
|
||||
dprintf("\t%s, %s, %s, %s\n",
|
||||
portcon[resp[0] >> 30],
|
||||
defdev[codec->afg_widgets[widx].d.pin.device],
|
||||
defdev[afg->widgets[widx].d.pin.device],
|
||||
conntype[(resp[0] >> 16) & 0xF],
|
||||
jcolor[(resp[0] >> 12) & 0xF]);
|
||||
}
|
||||
|
||||
hda_widget_get_amplifier_capabilities(codec, wid);
|
||||
hda_widget_get_amplifier_capabilities(afg->codec, wid);
|
||||
break;
|
||||
case WT_POWER:
|
||||
dprintf("%ld:\tPower\n", wid);
|
||||
@ -222,37 +219,37 @@ hda_codec_parse_afg(hda_codec* codec, uint32 afg_nid)
|
||||
|
||||
dprintf("\t%s\n", buf);
|
||||
|
||||
hda_widget_get_pm_support(codec, wid, &codec->afg_widgets[widx].pm);
|
||||
hda_widget_get_pm_support(afg->codec, wid, &afg->widgets[widx].pm);
|
||||
|
||||
if (codec->afg_widgets[widx].num_inputs) {
|
||||
if (afg->widgets[widx].num_inputs) {
|
||||
int idx;
|
||||
|
||||
off = 0;
|
||||
|
||||
if (codec->afg_widgets[widx].num_inputs > 1) {
|
||||
verbs[0] = MAKE_VERB(codec->addr,wid,VID_GET_CONNSEL,0);
|
||||
if (hda_send_verbs(codec, verbs, resp, 1) == B_OK)
|
||||
codec->afg_widgets[widx].active_input = resp[0] & 0xFF;
|
||||
if (afg->widgets[widx].num_inputs > 1) {
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,wid,VID_GET_CONNSEL,0);
|
||||
if (hda_send_verbs(afg->codec, verbs, resp, 1) == B_OK)
|
||||
afg->widgets[widx].active_input = resp[0] & 0xFF;
|
||||
else
|
||||
codec->afg_widgets[widx].active_input = -1;
|
||||
afg->widgets[widx].active_input = -1;
|
||||
} else
|
||||
codec->afg_widgets[widx].active_input = -1;
|
||||
afg->widgets[widx].active_input = -1;
|
||||
|
||||
for (idx=0; idx < codec->afg_widgets[widx].num_inputs; idx ++) {
|
||||
for (idx=0; idx < afg->widgets[widx].num_inputs; idx ++) {
|
||||
if (!(idx % 4)) {
|
||||
verbs[0] = MAKE_VERB(codec->addr,wid,VID_GET_CONNLENTRY,idx);
|
||||
if (hda_send_verbs(codec, verbs, resp, 1) != B_OK) {
|
||||
verbs[0] = MAKE_VERB(afg->codec->addr,wid,VID_GET_CONNLENTRY,idx);
|
||||
if (hda_send_verbs(afg->codec, verbs, resp, 1) != B_OK) {
|
||||
dprintf("%s: Error parsing inputs for widget %ld!\n", __func__, wid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx != codec->afg_widgets[widx].active_input)
|
||||
if (idx != afg->widgets[widx].active_input)
|
||||
off += sprintf(buf+off, "%ld ", (resp[0] >> (8*(idx%4))) & 0xFF);
|
||||
else
|
||||
off += sprintf(buf+off, "(%ld) ", (resp[0] >> (8*(idx%4))) & 0xFF);
|
||||
|
||||
codec->afg_widgets[widx].inputs[idx] = (resp[0] >> (8*(idx%4))) & 0xFF;
|
||||
afg->widgets[widx].inputs[idx] = (resp[0] >> (8*(idx%4))) & 0xFF;
|
||||
}
|
||||
|
||||
dprintf("\t[ %s]\n", buf);
|
||||
@ -264,32 +261,32 @@ hda_codec_parse_afg(hda_codec* codec, uint32 afg_nid)
|
||||
}
|
||||
|
||||
static uint32
|
||||
hda_codec_afg_find_dac_path(hda_codec* codec, uint32 wid, uint32 depth)
|
||||
hda_codec_afg_find_dac_path(hda_afg* afg, uint32 wid, uint32 depth)
|
||||
{
|
||||
int widx = wid - codec->afg_wid_start;
|
||||
int widx = wid - afg->wid_start;
|
||||
int idx;
|
||||
|
||||
switch(codec->afg_widgets[widx].type) {
|
||||
switch(afg->widgets[widx].type) {
|
||||
case WT_AUDIO_OUTPUT:
|
||||
return wid;
|
||||
|
||||
case WT_AUDIO_MIXER:
|
||||
for (idx=0; idx < codec->afg_widgets[widx].num_inputs; idx++) {
|
||||
if (hda_codec_afg_find_dac_path(codec, codec->afg_widgets[widx].inputs[idx], depth +1)) {
|
||||
if (codec->afg_widgets[widx].active_input == -1)
|
||||
codec->afg_widgets[widx].active_input = idx;
|
||||
for (idx=0; idx < afg->widgets[widx].num_inputs; idx++) {
|
||||
if (hda_codec_afg_find_dac_path(afg, afg->widgets[widx].inputs[idx], depth +1)) {
|
||||
if (afg->widgets[widx].active_input == -1)
|
||||
afg->widgets[widx].active_input = idx;
|
||||
|
||||
return codec->afg_widgets[widx].inputs[idx];
|
||||
return afg->widgets[widx].inputs[idx];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WT_AUDIO_SELECTOR:
|
||||
{
|
||||
int idx = codec->afg_widgets[widx].active_input;
|
||||
int idx = afg->widgets[widx].active_input;
|
||||
if (idx != -1) {
|
||||
uint32 wid = codec->afg_widgets[widx].inputs[idx];
|
||||
if (hda_codec_afg_find_dac_path(codec, wid, depth +1)) {
|
||||
uint32 wid = afg->widgets[widx].inputs[idx];
|
||||
if (hda_codec_afg_find_dac_path(afg, wid, depth +1)) {
|
||||
return wid;
|
||||
}
|
||||
}
|
||||
@ -303,41 +300,52 @@ hda_codec_afg_find_dac_path(hda_codec* codec, uint32 wid, uint32 depth)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
hda_codec_audiofg_new(hda_codec* codec, uint32 afg_nid)
|
||||
static status_t
|
||||
hda_codec_afg_new(hda_codec* codec, uint32 afg_nid)
|
||||
{
|
||||
hda_afg* afg;
|
||||
status_t rc;
|
||||
uint32 idx;
|
||||
|
||||
/* FIXME: Bail if this isn't the first Audio Function Group we find... */
|
||||
if (codec->afg_nid != 0) {
|
||||
dprintf("SORRY: This driver currently only supports a single Audio Function Group!\n");
|
||||
return;
|
||||
if ((afg=calloc(1, sizeof(hda_afg))) == NULL) {
|
||||
rc = B_NO_MEMORY;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Setup minimal info needed by hda_codec_parse_afg */
|
||||
afg->root_nid = afg_nid;
|
||||
afg->codec = codec;
|
||||
|
||||
/* Parse all widgets in Audio Function Group */
|
||||
hda_codec_parse_afg(codec, afg_nid);
|
||||
rc = hda_codec_parse_afg(afg);
|
||||
if (rc != B_OK)
|
||||
goto free_afg;
|
||||
|
||||
/* Setup for worst-case scenario;
|
||||
we cannot find any output Pin Widgets */
|
||||
rc = ENODEV;
|
||||
|
||||
/* Try to locate all output channels */
|
||||
for (idx=0; idx < codec->afg_wid_count; idx++) {
|
||||
for (idx=0; idx < afg->wid_count; idx++) {
|
||||
uint32 iidx, output_wid = 0;
|
||||
|
||||
if (codec->afg_widgets[idx].type != WT_PIN_COMPLEX)
|
||||
if (afg->widgets[idx].type != WT_PIN_COMPLEX)
|
||||
continue;
|
||||
if (codec->afg_widgets[idx].d.pin.output)
|
||||
if (afg->widgets[idx].d.pin.output)
|
||||
continue;
|
||||
if (codec->afg_widgets[idx].d.pin.device != PIN_DEV_HP_OUT &&
|
||||
codec->afg_widgets[idx].d.pin.device != PIN_DEV_SPEAKER &&
|
||||
codec->afg_widgets[idx].d.pin.device != PIN_DEV_LINE_OUT)
|
||||
if (afg->widgets[idx].d.pin.device != PIN_DEV_HP_OUT &&
|
||||
afg->widgets[idx].d.pin.device != PIN_DEV_SPEAKER &&
|
||||
afg->widgets[idx].d.pin.device != PIN_DEV_LINE_OUT)
|
||||
continue;
|
||||
|
||||
iidx = codec->afg_widgets[idx].active_input;
|
||||
iidx = afg->widgets[idx].active_input;
|
||||
if (iidx != -1) {
|
||||
output_wid = hda_codec_afg_find_dac_path(codec, codec->afg_widgets[idx].inputs[iidx], 0);
|
||||
output_wid = hda_codec_afg_find_dac_path(afg, afg->widgets[idx].inputs[iidx], 0);
|
||||
} else {
|
||||
for (iidx=0; iidx < codec->afg_widgets[idx].num_inputs; iidx++) {
|
||||
output_wid = hda_codec_afg_find_dac_path(codec, codec->afg_widgets[idx].inputs[iidx], 0);
|
||||
for (iidx=0; iidx < afg->widgets[idx].num_inputs; iidx++) {
|
||||
output_wid = hda_codec_afg_find_dac_path(afg, afg->widgets[idx].inputs[iidx], 0);
|
||||
if (output_wid) {
|
||||
corb_t verb = MAKE_VERB(codec->addr,idx+codec->afg_wid_start,VID_SET_CONNSEL,iidx);
|
||||
corb_t verb = MAKE_VERB(codec->addr,idx+afg->wid_start,VID_SET_CONNSEL,iidx);
|
||||
if (hda_send_verbs(codec, &verb, NULL, 1) != B_OK)
|
||||
dprintf("%s: Setting output selector failed!\n", __func__);
|
||||
break;
|
||||
@ -348,25 +356,43 @@ hda_codec_audiofg_new(hda_codec* codec, uint32 afg_nid)
|
||||
if (output_wid) {
|
||||
corb_t verb;
|
||||
|
||||
codec->playback_stream->pin_wid = idx + codec->afg_wid_start;
|
||||
codec->playback_stream->io_wid = output_wid;
|
||||
/* Setup playback/record streams for Multi Audio API */
|
||||
afg->playback_stream = hda_stream_alloc(afg->codec->ctrlr, STRM_PLAYBACK);
|
||||
afg->record_stream = hda_stream_alloc(afg->codec->ctrlr, STRM_RECORD);
|
||||
|
||||
afg->playback_stream->pin_wid = idx + afg->wid_start;
|
||||
afg->playback_stream->io_wid = output_wid;
|
||||
|
||||
dprintf("%s: Found output PIN (%s) connected to output CONV wid:%ld\n",
|
||||
__func__, defdev[codec->afg_widgets[idx].d.pin.device], output_wid);
|
||||
__func__, defdev[afg->widgets[idx].d.pin.device], output_wid);
|
||||
|
||||
/* FIXME: Force Pin Widget to unmute */
|
||||
verb = MAKE_VERB(codec->addr, codec->playback_stream->pin_wid,
|
||||
verb = MAKE_VERB(codec->addr, afg->playback_stream->pin_wid,
|
||||
VID_SET_AMPGAINMUTE, (1 << 15) | (1 << 13) | (1 << 12));
|
||||
hda_send_verbs(codec, &verb, NULL, 1);
|
||||
|
||||
rc = B_OK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we found any valid output channels, we're in the clear */
|
||||
if (rc == B_OK) {
|
||||
codec->afgs[codec->num_afgs++] = afg;
|
||||
goto done;
|
||||
}
|
||||
|
||||
free_afg:
|
||||
free(afg);
|
||||
|
||||
done:
|
||||
return rc;
|
||||
}
|
||||
|
||||
hda_codec*
|
||||
hda_codec_new(hda_controller* ctrlr, uint32 cad)
|
||||
{
|
||||
hda_codec* codec = calloc(sizeof(hda_codec),1);
|
||||
hda_codec* codec = calloc(1, sizeof(hda_codec));
|
||||
if (codec) {
|
||||
uint32 responses[3];
|
||||
corb_t verbs[3];
|
||||
@ -377,13 +403,8 @@ hda_codec_new(hda_controller* ctrlr, uint32 cad)
|
||||
codec->addr = cad;
|
||||
codec->response_count = 0;
|
||||
codec->response_sem = create_sem(0, "hda_codec_response_sem");
|
||||
codec->afg_nid = 0;
|
||||
ctrlr->codecs[cad] = codec;
|
||||
|
||||
/* Setup playback/record streams for Multi Audio API */
|
||||
codec->playback_stream = hda_stream_alloc(ctrlr, STRM_PLAYBACK);
|
||||
codec->record_stream = hda_stream_alloc(ctrlr, STRM_RECORD);
|
||||
|
||||
verbs[0] = MAKE_VERB(cad,0,VID_GET_PARAM,PID_VENDORID);
|
||||
verbs[1] = MAKE_VERB(cad,0,VID_GET_PARAM,PID_REVISIONID);
|
||||
verbs[2] = MAKE_VERB(cad,0,VID_GET_PARAM,PID_SUBORD_NODE_COUNT);
|
||||
@ -398,14 +419,16 @@ hda_codec_new(hda_controller* ctrlr, uint32 cad)
|
||||
uint32 resp;
|
||||
verbs[0] = MAKE_VERB(cad,nid,VID_GET_PARAM,PID_FUNCGRP_TYPE);
|
||||
|
||||
if ((rc=hda_send_verbs(codec, verbs, &resp, 1)) == B_OK && (resp&0xFF) == 1) {
|
||||
if ((rc=hda_send_verbs(codec, verbs, &resp, 1)) == B_OK &&
|
||||
(resp&0xFF) == 1 &&
|
||||
(rc=hda_codec_afg_new(codec, nid)) == B_OK) {
|
||||
/* Found an Audio Function Group! */
|
||||
hda_codec_audiofg_new(codec, nid);
|
||||
} else
|
||||
} else {
|
||||
dprintf("%s: FG %ld: %s\n", __func__, nid, strerror(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return codec;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ hda_stream_stop(hda_controller* ctrlr, hda_stream* s)
|
||||
}
|
||||
|
||||
status_t
|
||||
hda_stream_setup_buffers(hda_codec* codec, hda_stream* s, const char* desc)
|
||||
hda_stream_setup_buffers(hda_afg* afg, hda_stream* s, const char* desc)
|
||||
{
|
||||
uint32 buffer_size, buffer_pa, alloc;
|
||||
uint32 response[2], idx;
|
||||
@ -200,17 +200,17 @@ hda_stream_setup_buffers(hda_codec* codec, hda_stream* s, const char* desc)
|
||||
default: dprintf("%s: Invalid sample rate: 0x%lx\n", __func__, s->samplerate); break;
|
||||
}
|
||||
|
||||
OREG16(codec->ctrlr,s->off,FMT) = wfmt;
|
||||
OREG32(codec->ctrlr,s->off,BDPL) = s->bdl_pa;
|
||||
OREG32(codec->ctrlr,s->off,BDPU) = 0;
|
||||
OREG16(codec->ctrlr,s->off,LVI) = s->num_buffers -1;
|
||||
OREG32(codec->ctrlr,s->off,CBL) = s->num_channels * s->num_buffers;
|
||||
OREG8(codec->ctrlr,s->off,CTL0) = CTL0_IOCE | CTL0_FEIE | CTL0_DEIE;
|
||||
OREG8(codec->ctrlr,s->off,CTL2) = s->id << 4;
|
||||
OREG16(afg->codec->ctrlr,s->off,FMT) = wfmt;
|
||||
OREG32(afg->codec->ctrlr,s->off,BDPL) = s->bdl_pa;
|
||||
OREG32(afg->codec->ctrlr,s->off,BDPU) = 0;
|
||||
OREG16(afg->codec->ctrlr,s->off,LVI) = s->num_buffers -1;
|
||||
OREG32(afg->codec->ctrlr,s->off,CBL) = s->num_channels * s->num_buffers;
|
||||
OREG8(afg->codec->ctrlr,s->off,CTL0) = CTL0_IOCE | CTL0_FEIE | CTL0_DEIE;
|
||||
OREG8(afg->codec->ctrlr,s->off,CTL2) = s->id << 4;
|
||||
|
||||
verb[0] = MAKE_VERB(codec->addr, s->io_wid, VID_SET_CONVFORMAT, wfmt);
|
||||
verb[1] = MAKE_VERB(codec->addr, s->io_wid, VID_SET_CVTSTRCHN, s->id << 4);
|
||||
rc = hda_send_verbs(codec, verb, response, 2);
|
||||
verb[0] = MAKE_VERB(afg->codec->addr, s->io_wid, VID_SET_CONVFORMAT, wfmt);
|
||||
verb[1] = MAKE_VERB(afg->codec->addr, s->io_wid, VID_SET_CVTSTRCHN, s->id << 4);
|
||||
rc = hda_send_verbs(afg->codec, verb, response, 2);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ format2size(uint32 format)
|
||||
}
|
||||
|
||||
static status_t
|
||||
get_description(hda_codec* codec, multi_description* data)
|
||||
get_description(hda_afg* afg, multi_description* data)
|
||||
{
|
||||
data->interface_version = B_CURRENT_INTERFACE_VERSION;
|
||||
data->interface_minimum = B_CURRENT_INTERFACE_VERSION;
|
||||
@ -55,8 +55,8 @@ get_description(hda_codec* codec, multi_description* data)
|
||||
}
|
||||
|
||||
/* determine output/input rates */
|
||||
data->output_rates = codec->afg_defrates;
|
||||
data->input_rates = codec->afg_defrates;
|
||||
data->output_rates = afg->defrates;
|
||||
data->input_rates = afg->defrates;
|
||||
|
||||
/* force existance of 48kHz if variable rates are not supported */
|
||||
if (data->output_rates == 0)
|
||||
@ -67,8 +67,8 @@ get_description(hda_codec* codec, multi_description* data)
|
||||
data->max_cvsr_rate = 0;
|
||||
data->min_cvsr_rate = 0;
|
||||
|
||||
data->output_formats = codec->afg_deffmts;
|
||||
data->input_formats = codec->afg_deffmts;
|
||||
data->output_formats = afg->deffmts;
|
||||
data->input_formats = afg->deffmts;
|
||||
data->lock_sources = B_MULTI_LOCK_INTERNAL;
|
||||
data->timecode_sources = 0;
|
||||
data->interface_flags = B_MULTI_INTERFACE_PLAYBACK /* | B_MULTI_INTERFACE_RECORD */;
|
||||
@ -80,7 +80,7 @@ get_description(hda_codec* codec, multi_description* data)
|
||||
}
|
||||
|
||||
static status_t
|
||||
get_enabled_channels(hda_codec* codec, multi_channel_enable* data)
|
||||
get_enabled_channels(hda_afg* afg, multi_channel_enable* data)
|
||||
{
|
||||
B_SET_CHANNEL(data->enable_bits, 0, true);
|
||||
B_SET_CHANNEL(data->enable_bits, 1, true);
|
||||
@ -92,59 +92,59 @@ get_enabled_channels(hda_codec* codec, multi_channel_enable* data)
|
||||
}
|
||||
|
||||
static status_t
|
||||
get_global_format(hda_codec* codec, multi_format_info* data)
|
||||
get_global_format(hda_afg* afg, multi_format_info* data)
|
||||
{
|
||||
data->output_latency = 0;
|
||||
data->input_latency = 0;
|
||||
data->timecode_kind = 0;
|
||||
|
||||
data->output.format = codec->playback_stream->sampleformat;
|
||||
data->output.rate = codec->playback_stream->samplerate;
|
||||
data->output.format = afg->playback_stream->sampleformat;
|
||||
data->output.rate = afg->playback_stream->samplerate;
|
||||
|
||||
data->input.format = codec->record_stream->sampleformat;
|
||||
data->input.rate = codec->record_stream->sampleformat;
|
||||
data->input.format = afg->record_stream->sampleformat;
|
||||
data->input.rate = afg->record_stream->sampleformat;
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
static status_t
|
||||
set_global_format(hda_codec* codec, multi_format_info* data)
|
||||
set_global_format(hda_afg* afg, multi_format_info* data)
|
||||
{
|
||||
codec->playback_stream->sampleformat = data->output.format;
|
||||
codec->playback_stream->samplerate = data->output.rate;
|
||||
codec->playback_stream->sample_size = format2size(codec->playback_stream->sampleformat);
|
||||
afg->playback_stream->sampleformat = data->output.format;
|
||||
afg->playback_stream->samplerate = data->output.rate;
|
||||
afg->playback_stream->sample_size = format2size(afg->playback_stream->sampleformat);
|
||||
|
||||
codec->record_stream->samplerate = data->input.rate;
|
||||
codec->record_stream->sampleformat = data->input.format;
|
||||
codec->record_stream->sample_size = format2size(codec->record_stream->sampleformat);
|
||||
afg->record_stream->samplerate = data->input.rate;
|
||||
afg->record_stream->sampleformat = data->input.format;
|
||||
afg->record_stream->sample_size = format2size(afg->record_stream->sampleformat);
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
static status_t
|
||||
list_mix_controls(hda_codec* codec, multi_mix_control_info * data)
|
||||
list_mix_controls(hda_afg* afg, multi_mix_control_info * data)
|
||||
{
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
static status_t
|
||||
list_mix_connections(hda_codec* codec, multi_mix_connection_info * data)
|
||||
list_mix_connections(hda_afg* afg, multi_mix_connection_info * data)
|
||||
{
|
||||
data->actual_count = 0;
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
static status_t
|
||||
list_mix_channels(hda_codec* codec, multi_mix_channel_info *data)
|
||||
list_mix_channels(hda_afg* afg, multi_mix_channel_info *data)
|
||||
{
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
static status_t
|
||||
get_buffers(hda_codec* codec, multi_buffer_list* data)
|
||||
get_buffers(hda_afg* afg, multi_buffer_list* data)
|
||||
{
|
||||
uint32 playback_sample_size = codec->playback_stream->sample_size;
|
||||
uint32 record_sample_size = codec->record_stream->sample_size;
|
||||
uint32 playback_sample_size = afg->playback_stream->sample_size;
|
||||
uint32 record_sample_size = afg->record_stream->sample_size;
|
||||
uint32 cidx, bidx;
|
||||
status_t rc;
|
||||
|
||||
@ -163,18 +163,18 @@ get_buffers(hda_codec* codec, multi_buffer_list* data)
|
||||
data->flags = 0;
|
||||
|
||||
/* Copy the requested settings into the streams */
|
||||
codec->playback_stream->num_buffers = data->request_playback_buffers;
|
||||
codec->playback_stream->num_channels = data->request_playback_channels;
|
||||
codec->playback_stream->buffer_length = data->request_playback_buffer_size;
|
||||
if ((rc=hda_stream_setup_buffers(codec, codec->playback_stream, "Playback")) != B_OK) {
|
||||
afg->playback_stream->num_buffers = data->request_playback_buffers;
|
||||
afg->playback_stream->num_channels = data->request_playback_channels;
|
||||
afg->playback_stream->buffer_length = data->request_playback_buffer_size;
|
||||
if ((rc=hda_stream_setup_buffers(afg, afg->playback_stream, "Playback")) != B_OK) {
|
||||
dprintf("%s: Error setting up playback buffers (%s)\n", __func__, strerror(rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
codec->record_stream->num_buffers = data->request_record_buffers;
|
||||
codec->record_stream->num_channels = data->request_record_channels;
|
||||
codec->record_stream->buffer_length = data->request_record_buffer_size;
|
||||
if ((rc=hda_stream_setup_buffers(codec, codec->record_stream, "Recording")) != B_OK) {
|
||||
afg->record_stream->num_buffers = data->request_record_buffers;
|
||||
afg->record_stream->num_channels = data->request_record_channels;
|
||||
afg->record_stream->buffer_length = data->request_record_buffer_size;
|
||||
if ((rc=hda_stream_setup_buffers(afg, afg->record_stream, "Recording")) != B_OK) {
|
||||
dprintf("%s: Error setting up recording buffers (%s)\n", __func__, strerror(rc));
|
||||
return rc;
|
||||
}
|
||||
@ -186,7 +186,7 @@ get_buffers(hda_codec* codec, multi_buffer_list* data)
|
||||
|
||||
for (bidx=0; bidx < data->return_playback_buffers; bidx++) {
|
||||
for (cidx=0; cidx < data->return_playback_channels; cidx++) {
|
||||
data->playback_buffers[bidx][cidx].base = codec->playback_stream->buffers[bidx] + (playback_sample_size * cidx);
|
||||
data->playback_buffers[bidx][cidx].base = afg->playback_stream->buffers[bidx] + (playback_sample_size * cidx);
|
||||
data->playback_buffers[bidx][cidx].stride = playback_sample_size * data->return_playback_channels;
|
||||
}
|
||||
}
|
||||
@ -197,7 +197,7 @@ get_buffers(hda_codec* codec, multi_buffer_list* data)
|
||||
|
||||
for (bidx=0; bidx < data->return_record_buffers; bidx++) {
|
||||
for (cidx=0; cidx < data->return_record_channels; cidx++) {
|
||||
data->record_buffers[bidx][cidx].base = codec->record_stream->buffers[bidx] + (record_sample_size * cidx);
|
||||
data->record_buffers[bidx][cidx].base = afg->record_stream->buffers[bidx] + (record_sample_size * cidx);
|
||||
data->record_buffers[bidx][cidx].stride = record_sample_size * data->return_record_channels;
|
||||
}
|
||||
}
|
||||
@ -206,17 +206,17 @@ get_buffers(hda_codec* codec, multi_buffer_list* data)
|
||||
}
|
||||
|
||||
static status_t
|
||||
buffer_exchange(hda_codec* codec, multi_buffer_info* data)
|
||||
buffer_exchange(hda_afg* afg, multi_buffer_info* data)
|
||||
{
|
||||
static int debug_buffers_exchanged = 0;
|
||||
cpu_status status;
|
||||
status_t rc;
|
||||
|
||||
if (!codec->playback_stream->running)
|
||||
hda_stream_start(codec->ctrlr, codec->playback_stream);
|
||||
if (!afg->playback_stream->running)
|
||||
hda_stream_start(afg->codec->ctrlr, afg->playback_stream);
|
||||
|
||||
/* do playback */
|
||||
rc=acquire_sem(codec->playback_stream->buffer_ready_sem);
|
||||
rc=acquire_sem(afg->playback_stream->buffer_ready_sem);
|
||||
if (rc != B_OK) {
|
||||
dprintf("%s: Error waiting for playback buffer to finish (%s)!\n", __func__,
|
||||
strerror(rc));
|
||||
@ -224,13 +224,13 @@ buffer_exchange(hda_codec* codec, multi_buffer_info* data)
|
||||
}
|
||||
|
||||
status = disable_interrupts();
|
||||
acquire_spinlock(&codec->playback_stream->lock);
|
||||
acquire_spinlock(&afg->playback_stream->lock);
|
||||
|
||||
data->playback_buffer_cycle = codec->playback_stream->buffer_cycle;
|
||||
data->played_real_time = codec->playback_stream->real_time;
|
||||
data->played_frames_count = codec->playback_stream->frames_count;
|
||||
data->playback_buffer_cycle = afg->playback_stream->buffer_cycle;
|
||||
data->played_real_time = afg->playback_stream->real_time;
|
||||
data->played_frames_count = afg->playback_stream->frames_count;
|
||||
|
||||
release_spinlock(&codec->playback_stream->lock);
|
||||
release_spinlock(&afg->playback_stream->lock);
|
||||
restore_interrupts(status);
|
||||
|
||||
debug_buffers_exchanged++;
|
||||
@ -242,13 +242,13 @@ buffer_exchange(hda_codec* codec, multi_buffer_info* data)
|
||||
}
|
||||
|
||||
static status_t
|
||||
buffer_force_stop(hda_codec* codec)
|
||||
buffer_force_stop(hda_afg* afg)
|
||||
{
|
||||
hda_stream_stop(codec->ctrlr, codec->playback_stream);
|
||||
hda_stream_stop(codec->ctrlr, codec->record_stream);
|
||||
hda_stream_stop(afg->codec->ctrlr, afg->playback_stream);
|
||||
hda_stream_stop(afg->codec->ctrlr, afg->record_stream);
|
||||
|
||||
delete_sem(codec->playback_stream->buffer_ready_sem);
|
||||
// delete_sem(codec->record_stream->buffer_ready_sem);
|
||||
delete_sem(afg->playback_stream->buffer_ready_sem);
|
||||
// delete_sem(afg->record_stream->buffer_ready_sem);
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
@ -257,28 +257,35 @@ status_t
|
||||
multi_audio_control(void* cookie, uint32 op, void* arg, size_t len)
|
||||
{
|
||||
hda_codec* codec = (hda_codec*)cookie;
|
||||
hda_afg* afg;
|
||||
|
||||
/* FIXME: Make sure we have a valid codec & afg... */
|
||||
if (!codec || codec->num_afgs == 0)
|
||||
return ENODEV;
|
||||
|
||||
afg = codec->afgs[0];
|
||||
|
||||
switch(op) {
|
||||
case B_MULTI_GET_DESCRIPTION: return get_description(codec, arg);
|
||||
case B_MULTI_GET_DESCRIPTION: return get_description(afg, arg);
|
||||
case B_MULTI_GET_EVENT_INFO: return B_ERROR;
|
||||
case B_MULTI_SET_EVENT_INFO: return B_ERROR;
|
||||
case B_MULTI_GET_EVENT: return B_ERROR;
|
||||
case B_MULTI_GET_ENABLED_CHANNELS: return get_enabled_channels(codec, arg);
|
||||
case B_MULTI_GET_ENABLED_CHANNELS: return get_enabled_channels(afg, arg);
|
||||
case B_MULTI_SET_ENABLED_CHANNELS: return B_OK;
|
||||
case B_MULTI_GET_GLOBAL_FORMAT: return get_global_format(codec, arg);
|
||||
case B_MULTI_SET_GLOBAL_FORMAT: return set_global_format(codec, arg);
|
||||
case B_MULTI_GET_GLOBAL_FORMAT: return get_global_format(afg, arg);
|
||||
case B_MULTI_SET_GLOBAL_FORMAT: return set_global_format(afg, arg);
|
||||
case B_MULTI_GET_CHANNEL_FORMATS: return B_ERROR;
|
||||
case B_MULTI_SET_CHANNEL_FORMATS: return B_ERROR;
|
||||
case B_MULTI_GET_MIX: return B_ERROR;
|
||||
case B_MULTI_SET_MIX: return B_ERROR;
|
||||
case B_MULTI_LIST_MIX_CHANNELS: return list_mix_channels(codec, arg);
|
||||
case B_MULTI_LIST_MIX_CONTROLS: return list_mix_controls(codec, arg);
|
||||
case B_MULTI_LIST_MIX_CONNECTIONS: return list_mix_connections(codec, arg);
|
||||
case B_MULTI_GET_BUFFERS: return get_buffers(codec, arg);
|
||||
case B_MULTI_LIST_MIX_CHANNELS: return list_mix_channels(afg, arg);
|
||||
case B_MULTI_LIST_MIX_CONTROLS: return list_mix_controls(afg, arg);
|
||||
case B_MULTI_LIST_MIX_CONNECTIONS: return list_mix_connections(afg, arg);
|
||||
case B_MULTI_GET_BUFFERS: return get_buffers(afg, arg);
|
||||
case B_MULTI_SET_BUFFERS: return B_ERROR;
|
||||
case B_MULTI_SET_START_TIME: return B_ERROR;
|
||||
case B_MULTI_BUFFER_EXCHANGE: return buffer_exchange(codec, arg);
|
||||
case B_MULTI_BUFFER_FORCE_STOP: return buffer_force_stop(codec);
|
||||
case B_MULTI_BUFFER_EXCHANGE: return buffer_exchange(afg, arg);
|
||||
case B_MULTI_BUFFER_FORCE_STOP: return buffer_force_stop(afg);
|
||||
}
|
||||
|
||||
return B_BAD_VALUE;
|
||||
|
Loading…
Reference in New Issue
Block a user