Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wine-winehq
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wine
wine-winehq
Commits
02c416a1
Commit
02c416a1
authored
Apr 24, 2007
by
Maarten Lankhorst
Committed by
Alexandre Julliard
Apr 24, 2007
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
winealsa: Implement Get/SetControlDetails in mixer.
parent
143f10a4
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
438 additions
and
1 deletion
+438
-1
mixer.c
dlls/winealsa.drv/mixer.c
+438
-1
No files found.
dlls/winealsa.drv/mixer.c
View file @
02c416a1
...
...
@@ -342,6 +342,7 @@ static void ALSA_MixerInit(void)
snd_ctl_card_info_alloca
(
&
info
);
mixdev
[
mixnum
].
lines
=
NULL
;
mixdev
[
mixnum
].
callback
=
0
;
mixdev
[
mixnum
].
controls
=
NULL
;
snprintf
(
cardind
,
sizeof
(
cardind
),
"%d"
,
x
);
card
=
snd_card_get_index
(
cardind
);
...
...
@@ -748,6 +749,20 @@ static DWORD MIX_GetDevCaps(UINT wDevID, LPMIXERCAPS2W caps, DWORD_PTR parm2)
return
MMSYSERR_NOERROR
;
}
/* convert win32 volume to alsa volume, and vice versa */
static
INT
normalized
(
INT
value
,
INT
prevmax
,
INT
nextmax
)
{
int
ret
=
MulDiv
(
value
,
nextmax
,
prevmax
);
/* Have to stay in range */
TRACE
(
"%d/%d -> %d/%d
\n
"
,
value
,
prevmax
,
ret
,
nextmax
);
if
(
ret
>
nextmax
)
ret
=
nextmax
;
else
if
(
ret
<
0
)
ret
=
0
;
return
ret
;
}
/* get amount of sources for dest */
static
int
getsrccntfromchan
(
mixer
*
mmixer
,
int
dad
)
...
...
@@ -795,6 +810,422 @@ static int getsrcfromline(mixer *mmixer, int line)
return
0
;
}
/* Get volume/muted/capture channel */
static
DWORD
MIX_GetControlDetails
(
UINT
wDevID
,
LPMIXERCONTROLDETAILS
mctrld
,
DWORD_PTR
flags
)
{
mixer
*
mmixer
=
MIX_GetMix
(
wDevID
);
DWORD
ctrl
;
DWORD
line
;
control
*
ct
;
if
(
!
mctrld
)
return
MMSYSERR_INVALPARAM
;
ctrl
=
mctrld
->
dwControlID
;
line
=
ctrl
/
CONTROLSPERLINE
;
if
(
mctrld
->
cbStruct
!=
sizeof
(
*
mctrld
))
return
MMSYSERR_INVALPARAM
;
if
(
!
mmixer
)
return
MMSYSERR_BADDEVICEID
;
if
(
line
<
0
||
line
>=
mmixer
->
chans
||
!
mmixer
->
controls
[
ctrl
].
enabled
)
return
MIXERR_INVALCONTROL
;
ct
=
&
mmixer
->
controls
[
ctrl
];
flags
&=
MIXER_GETCONTROLDETAILSF_QUERYMASK
;
switch
(
flags
)
{
case
MIXER_GETCONTROLDETAILSF_VALUE
:
TRACE
(
"MIXER_GETCONTROLDETAILSF_VALUE (%d/%d)
\n
"
,
ctrl
,
line
);
switch
(
ct
->
c
.
dwControlType
)
{
case
MIXERCONTROL_CONTROLTYPE_VOLUME
:
{
long
min
=
0
,
max
=
0
,
vol
=
0
;
int
chn
;
LPMIXERCONTROLDETAILS_UNSIGNED
mcdu
;
snd_mixer_elem_t
*
elem
=
mmixer
->
lines
[
line
].
elem
;
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_UNSIGNED
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_UNSIGNED[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdu
=
(
LPMIXERCONTROLDETAILS_UNSIGNED
)
mctrld
->
paDetails
;
if
(
mctrld
->
cChannels
!=
1
&&
mmixer
->
lines
[
line
].
chans
!=
mctrld
->
cChannels
)
{
WARN
(
"Unsupported cChannels (%d instead of %d)
\n
"
,
mctrld
->
cChannels
,
mmixer
->
lines
[
line
].
chans
);
return
MMSYSERR_INVALPARAM
;
}
if
(
mmixer
->
lines
[
line
].
capt
&&
snd_mixer_selem_has_capture_volume
(
elem
))
{
snd_mixer_selem_get_capture_volume_range
(
elem
,
&
min
,
&
max
);
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
if
(
snd_mixer_selem_has_capture_channel
(
elem
,
chn
))
{
snd_mixer_selem_get_capture_volume
(
elem
,
chn
,
&
vol
);
mcdu
->
dwValue
=
normalized
(
vol
-
min
,
max
,
65535
);
if
(
mctrld
->
cChannels
==
1
)
break
;
++
mcdu
;
}
}
else
{
snd_mixer_selem_get_playback_volume_range
(
elem
,
&
min
,
&
max
);
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
if
(
snd_mixer_selem_has_playback_channel
(
elem
,
chn
))
{
snd_mixer_selem_get_playback_volume
(
elem
,
chn
,
&
vol
);
mcdu
->
dwValue
=
normalized
(
vol
-
min
,
max
,
65535
);
if
(
mctrld
->
cChannels
==
1
)
break
;
++
mcdu
;
}
}
return
MMSYSERR_NOERROR
;
}
case
MIXERCONTROL_CONTROLTYPE_ONOFF
:
case
MIXERCONTROL_CONTROLTYPE_MUTE
:
{
LPMIXERCONTROLDETAILS_BOOLEAN
mcdb
;
int
chn
,
ival
;
snd_mixer_elem_t
*
elem
=
mmixer
->
lines
[
line
].
elem
;
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_BOOLEAN
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_BOOLEAN[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdb
=
(
LPMIXERCONTROLDETAILS_BOOLEAN
)
mctrld
->
paDetails
;
if
(
line
==
1
)
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
{
if
(
!
snd_mixer_selem_has_capture_channel
(
elem
,
chn
))
continue
;
snd_mixer_selem_get_capture_switch
(
elem
,
chn
,
&
ival
);
break
;
}
else
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
{
if
(
!
snd_mixer_selem_has_playback_channel
(
elem
,
chn
))
continue
;
snd_mixer_selem_get_playback_switch
(
elem
,
chn
,
&
ival
);
break
;
}
mcdb
->
fValue
=
!
ival
;
TRACE
(
"=> %s
\n
"
,
mcdb
->
fValue
?
"on"
:
"off"
);
return
MMSYSERR_NOERROR
;
}
case
MIXERCONTROL_CONTROLTYPE_MIXER
:
case
MIXERCONTROL_CONTROLTYPE_MUX
:
{
LPMIXERCONTROLDETAILS_BOOLEAN
mcdb
;
int
x
,
i
=
0
,
ival
=
0
,
chn
;
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_BOOLEAN
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_BOOLEAN[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdb
=
(
LPMIXERCONTROLDETAILS_BOOLEAN
)
mctrld
->
paDetails
;
for
(
x
=
0
;
x
<
mmixer
->
chans
;
++
x
)
if
(
line
!=
x
&&
mmixer
->
lines
[
x
].
dst
==
line
)
{
ival
=
0
;
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
{
if
(
!
snd_mixer_selem_has_capture_channel
(
mmixer
->
lines
[
x
].
elem
,
chn
))
continue
;
snd_mixer_selem_get_capture_switch
(
mmixer
->
lines
[
x
].
elem
,
chn
,
&
ival
);
if
(
ival
)
break
;
}
if
(
i
>=
mctrld
->
u
.
cMultipleItems
)
{
TRACE
(
"overflow
\n
"
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"fVal[%i] = %sselected
\n
"
,
i
,
(
!
ival
?
"un"
:
""
));
mcdb
[
i
++
].
fValue
=
ival
;
}
break
;
}
default:
FIXME
(
"Unhandled controltype %s
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
));
return
MMSYSERR_INVALPARAM
;
}
return
MMSYSERR_NOERROR
;
case
MIXER_GETCONTROLDETAILSF_LISTTEXT
:
TRACE
(
"MIXER_GETCONTROLDETAILSF_LISTTEXT (%d)
\n
"
,
ctrl
);
if
(
ct
->
c
.
dwControlType
==
MIXERCONTROL_CONTROLTYPE_MUX
||
ct
->
c
.
dwControlType
==
MIXERCONTROL_CONTROLTYPE_MIXER
)
{
LPMIXERCONTROLDETAILS_LISTTEXTW
mcdlt
=
(
LPMIXERCONTROLDETAILS_LISTTEXTW
)
mctrld
->
paDetails
;
int
i
,
j
;
for
(
i
=
j
=
0
;
j
<
mmixer
->
chans
;
++
j
)
if
(
j
!=
line
&&
mmixer
->
lines
[
j
].
dst
==
line
)
{
if
(
i
>
mctrld
->
u
.
cMultipleItems
)
return
MMSYSERR_INVALPARAM
;
mcdlt
->
dwParam1
=
j
;
mcdlt
->
dwParam2
=
mmixer
->
lines
[
j
].
component
;
lstrcpynW
(
mcdlt
->
szName
,
mmixer
->
lines
[
j
].
name
,
sizeof
(
mcdlt
->
szName
)
/
sizeof
(
WCHAR
));
TRACE
(
"Adding %i as %s
\n
"
,
j
,
debugstr_w
(
mcdlt
->
szName
));
++
i
;
++
mcdlt
;
}
if
(
i
<
mctrld
->
u
.
cMultipleItems
)
return
MMSYSERR_INVALPARAM
;
return
MMSYSERR_NOERROR
;
}
FIXME
(
"Imagine this code being horribly broken and incomplete, introducing: reality
\n
"
);
return
MMSYSERR_INVALPARAM
;
default:
WARN
(
"Unknown flag (%08lx)
\n
"
,
flags
);
return
MMSYSERR_INVALPARAM
;
}
}
/* Set volume/capture channel/muted for control */
static
DWORD
MIX_SetControlDetails
(
UINT
wDevID
,
LPMIXERCONTROLDETAILS
mctrld
,
DWORD_PTR
flags
)
{
mixer
*
mmixer
=
MIX_GetMix
(
wDevID
);
DWORD
ctrl
,
line
,
i
;
control
*
ct
;
snd_mixer_elem_t
*
elem
;
if
(
!
mctrld
)
return
MMSYSERR_INVALPARAM
;
ctrl
=
mctrld
->
dwControlID
;
line
=
ctrl
/
CONTROLSPERLINE
;
if
(
mctrld
->
cbStruct
!=
sizeof
(
*
mctrld
))
{
WARN
(
"Invalid size of mctrld %d
\n
"
,
mctrld
->
cbStruct
);
return
MMSYSERR_INVALPARAM
;
}
if
(
!
mmixer
)
return
MMSYSERR_BADDEVICEID
;
if
(
line
<
0
||
line
>=
mmixer
->
chans
)
{
WARN
(
"Invalid line id: %d not in range of 0-%d
\n
"
,
line
,
mmixer
->
chans
-
1
);
return
MMSYSERR_INVALPARAM
;
}
if
(
!
mmixer
->
controls
[
ctrl
].
enabled
)
{
WARN
(
"Control %d not enabled
\n
"
,
ctrl
);
return
MIXERR_INVALCONTROL
;
}
ct
=
&
mmixer
->
controls
[
ctrl
];
elem
=
mmixer
->
lines
[
line
].
elem
;
flags
&=
MIXER_SETCONTROLDETAILSF_QUERYMASK
;
switch
(
flags
)
{
case
MIXER_SETCONTROLDETAILSF_VALUE
:
TRACE
(
"MIXER_SETCONTROLDETAILSF_VALUE (%d)
\n
"
,
ctrl
);
break
;
default:
WARN
(
"Unknown flag (%08lx)
\n
"
,
flags
);
return
MMSYSERR_INVALPARAM
;
}
switch
(
ct
->
c
.
dwControlType
)
{
case
MIXERCONTROL_CONTROLTYPE_VOLUME
:
{
long
min
=
0
,
max
=
0
;
int
chn
;
LPMIXERCONTROLDETAILS_UNSIGNED
mcdu
;
snd_mixer_elem_t
*
elem
=
mmixer
->
lines
[
line
].
elem
;
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_UNSIGNED
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
if
(
mctrld
->
cChannels
!=
1
&&
mmixer
->
lines
[
line
].
chans
!=
mctrld
->
cChannels
)
{
WARN
(
"Unsupported cChannels (%d instead of %d)
\n
"
,
mctrld
->
cChannels
,
mmixer
->
lines
[
line
].
chans
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_UNSIGNED[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdu
=
(
LPMIXERCONTROLDETAILS_UNSIGNED
)
mctrld
->
paDetails
;
for
(
chn
=
0
;
chn
<
mctrld
->
cChannels
;
++
chn
)
{
TRACE
(
"Chan %d value %d
\n
"
,
chn
,
mcdu
[
chn
].
dwValue
);
}
/* There isn't always a capture volume, so in that case change playback volume */
if
(
mmixer
->
lines
[
line
].
capt
&&
snd_mixer_selem_has_capture_volume
(
elem
))
{
snd_mixer_selem_get_capture_volume_range
(
elem
,
&
min
,
&
max
);
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
if
(
snd_mixer_selem_has_capture_channel
(
elem
,
chn
))
{
snd_mixer_selem_set_capture_volume
(
elem
,
chn
,
min
+
normalized
(
mcdu
->
dwValue
,
65535
,
max
));
if
(
mctrld
->
cChannels
!=
1
)
mcdu
++
;
}
}
else
{
snd_mixer_selem_get_playback_volume_range
(
elem
,
&
min
,
&
max
);
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
if
(
snd_mixer_selem_has_playback_channel
(
elem
,
chn
))
{
snd_mixer_selem_set_playback_volume
(
elem
,
chn
,
min
+
normalized
(
mcdu
->
dwValue
,
65535
,
max
));
if
(
mctrld
->
cChannels
!=
1
)
mcdu
++
;
}
}
break
;
}
case
MIXERCONTROL_CONTROLTYPE_MUTE
:
case
MIXERCONTROL_CONTROLTYPE_ONOFF
:
{
LPMIXERCONTROLDETAILS_BOOLEAN
mcdb
;
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_BOOLEAN
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_BOOLEAN[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdb
=
(
LPMIXERCONTROLDETAILS_BOOLEAN
)
mctrld
->
paDetails
;
if
(
line
==
1
)
/* Mute/unmute capturing */
for
(
i
=
0
;
i
<=
SND_MIXER_SCHN_LAST
;
++
i
)
{
if
(
snd_mixer_selem_has_capture_channel
(
elem
,
i
))
snd_mixer_selem_set_capture_switch
(
elem
,
i
,
!
mcdb
->
fValue
);
}
else
for
(
i
=
0
;
i
<=
SND_MIXER_SCHN_LAST
;
++
i
)
if
(
snd_mixer_selem_has_playback_channel
(
elem
,
i
))
snd_mixer_selem_set_playback_switch
(
elem
,
i
,
!
mcdb
->
fValue
);
break
;
}
case
MIXERCONTROL_CONTROLTYPE_MIXER
:
case
MIXERCONTROL_CONTROLTYPE_MUX
:
{
LPMIXERCONTROLDETAILS_BOOLEAN
mcdb
;
int
x
,
i
=
0
,
chn
;
int
didone
=
0
,
canone
=
(
ct
->
c
.
dwControlType
==
MIXERCONTROL_CONTROLTYPE_MUX
);
if
(
mctrld
->
cbDetails
!=
sizeof
(
MIXERCONTROLDETAILS_BOOLEAN
))
{
WARN
(
"invalid parameter: cbDetails %d
\n
"
,
mctrld
->
cbDetails
);
return
MMSYSERR_INVALPARAM
;
}
TRACE
(
"%s MIXERCONTROLDETAILS_BOOLEAN[%u]
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
),
mctrld
->
cChannels
);
mcdb
=
(
LPMIXERCONTROLDETAILS_BOOLEAN
)
mctrld
->
paDetails
;
for
(
x
=
i
=
0
;
x
<
mmixer
->
chans
;
++
x
)
if
(
line
!=
x
&&
mmixer
->
lines
[
x
].
dst
==
line
)
{
TRACE
(
"fVal[%i] (%s) = %i
\n
"
,
i
,
debugstr_w
(
mmixer
->
lines
[
x
].
name
),
mcdb
[
i
].
fValue
);
if
(
i
>=
mctrld
->
u
.
cMultipleItems
)
{
TRACE
(
"Too many items to fit, overflowing
\n
"
);
return
MIXERR_INVALVALUE
;
}
if
(
mcdb
[
i
].
fValue
&&
canone
&&
didone
)
{
TRACE
(
"Nice try, but it's not going to work
\n
"
);
elem_callback
(
mmixer
->
lines
[
1
].
elem
,
SND_CTL_EVENT_MASK_VALUE
);
return
MIXERR_INVALVALUE
;
}
if
(
mcdb
[
i
].
fValue
)
didone
=
1
;
++
i
;
}
if
(
canone
&&
!
didone
)
{
TRACE
(
"Nice try, this is not going to work either
\n
"
);
elem_callback
(
mmixer
->
lines
[
1
].
elem
,
SND_CTL_EVENT_MASK_VALUE
);
return
MIXERR_INVALVALUE
;
}
for
(
x
=
i
=
0
;
x
<
mmixer
->
chans
;
++
x
)
if
(
line
!=
x
&&
mmixer
->
lines
[
x
].
dst
==
line
)
{
if
(
mcdb
[
i
].
fValue
)
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
{
if
(
!
snd_mixer_selem_has_capture_channel
(
mmixer
->
lines
[
x
].
elem
,
chn
))
continue
;
snd_mixer_selem_set_capture_switch
(
mmixer
->
lines
[
x
].
elem
,
chn
,
mcdb
[
i
].
fValue
);
}
++
i
;
}
/* If it's a MUX, it means that only 1 channel can be selected
* and the other channels are unselected
*
* For MIXER multiple sources are allowed, so unselect here
*/
if
(
canone
)
break
;
for
(
x
=
i
=
0
;
x
<
mmixer
->
chans
;
++
x
)
if
(
line
!=
x
&&
mmixer
->
lines
[
x
].
dst
==
line
)
{
if
(
!
mcdb
[
i
].
fValue
)
for
(
chn
=
0
;
chn
<=
SND_MIXER_SCHN_LAST
;
++
chn
)
{
if
(
!
snd_mixer_selem_has_capture_channel
(
mmixer
->
lines
[
x
].
elem
,
chn
))
continue
;
snd_mixer_selem_set_capture_switch
(
mmixer
->
lines
[
x
].
elem
,
chn
,
mcdb
[
i
].
fValue
);
}
++
i
;
}
break
;
}
default:
FIXME
(
"Unhandled type %s
\n
"
,
getControlType
(
ct
->
c
.
dwControlType
));
return
MMSYSERR_INVALPARAM
;
}
return
MMSYSERR_NOERROR
;
}
/* Here we give info over the source/dest line given by dwSource+dwDest or dwDest, respectively
* It is also possible that a line is found by componenttype or target type, latter is not implemented yet
* Most important values returned in struct:
...
...
@@ -824,7 +1255,7 @@ static DWORD MIX_GetLineInfo(UINT wDevID, LPMIXERLINEW Ml, DWORD_PTR flags)
if
(
Ml
->
cbStruct
!=
sizeof
(
*
Ml
))
{
WARN
(
"invalid parameter: Ml->cbStruct = %d
!= %d
\n
"
,
Ml
->
cbStruct
,
sizeof
(
*
Ml
)
);
WARN
(
"invalid parameter: Ml->cbStruct = %d
\n
"
,
Ml
->
cbStruct
);
return
MMSYSERR_INVALPARAM
;
}
...
...
@@ -1060,6 +1491,12 @@ DWORD WINAPI ALSA_mxdMessage(UINT wDevID, UINT wMsg, DWORD_PTR dwUser,
case
MXDM_GETLINECONTROLS
:
ret
=
MIX_GetLineControls
(
wDevID
,
(
LPMIXERLINECONTROLSW
)
dwParam1
,
dwParam2
);
break
;
case
MXDM_GETCONTROLDETAILS
:
ret
=
MIX_GetControlDetails
(
wDevID
,
(
LPMIXERCONTROLDETAILS
)
dwParam1
,
dwParam2
);
break
;
case
MXDM_SETCONTROLDETAILS
:
ret
=
MIX_SetControlDetails
(
wDevID
,
(
LPMIXERCONTROLDETAILS
)
dwParam1
,
dwParam2
);
break
;
case
MXDM_GETNUMDEVS
:
ret
=
cards
;
break
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment