Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
M
miniaudio
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
MyCard
miniaudio
Commits
3bee97a9
Commit
3bee97a9
authored
Dec 12, 2019
by
David Reid
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove sigvis.
This will be replaced with a more complete solution later. Public issue #105.
parent
cd44057a
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
2 additions
and
540 deletions
+2
-540
.gitmodules
.gitmodules
+0
-3
tests/ma_resampling.c
tests/ma_resampling.c
+2
-70
tools/external/dred
tools/external/dred
+0
-1
tools/mini_sigvis/README.md
tools/mini_sigvis/README.md
+0
-1
tools/mini_sigvis/mini_sigvis.h
tools/mini_sigvis/mini_sigvis.h
+0
-465
No files found.
.gitmodules
View file @
3bee97a9
[submodule "tools/external/dred"]
path = tools/external/dred
url = https://github.com/dr-soft/dred
tests/ma_resampling.c
View file @
3bee97a9
// We're using sigvis for visualizations. This will include miniaudio for us, so no need to include miniaudio in this file.
#define NO_SIGVIS
#define MA_NO_SSE2
#define MA_NO_AVX2
#ifdef NO_SIGVIS
#define MINIAUDIO_IMPLEMENTATION
#include "../miniaudio.h"
#else
#define MINI_SIGVIS_IMPLEMENTATION
#include "../tools/mini_sigvis/mini_sigvis.h" // <-- Includes miniaudio.
#endif
#define MINIAUDIO_IMPLEMENTATION
#include "../miniaudio.h"
// There is a usage pattern for resampling that miniaudio does not properly support which is where the client continuously
// reads samples until ma_src_read() returns 0. The problem with this pattern is that is consumes the samples sitting
...
...
@@ -137,66 +130,6 @@ int main(int argc, char** argv)
return
-
1
;
}
#ifndef NO_SIGVIS
msigvis_context
sigvis
;
result
=
msigvis_init
(
&
sigvis
);
if
(
result
!=
MA_SUCCESS
)
{
printf
(
"Failed to initialize mini_sigvis context.
\n
"
);
return
-
1
;
}
msigvis_screen
screen
;
result
=
msigvis_screen_init
(
&
sigvis
,
1280
,
720
,
&
screen
);
if
(
result
!=
MA_SUCCESS
)
{
printf
(
"Failed to initialize mini_sigvis screen.
\n
"
);
return
-
2
;
}
msigvis_screen_show
(
&
screen
);
msigvis_channel
channelSineWave
;
result
=
msigvis_channel_init
(
&
sigvis
,
ma_format_f32
,
sampleRateOut
,
&
channelSineWave
);
if
(
result
!=
MA_SUCCESS
)
{
printf
(
"Failed to initialize mini_sigvis channel.
\n
"
);
return
-
3
;
}
float
testSamples
[
40960
];
float
*
pFramesF32
=
testSamples
;
// To reproduce the case we are needing to test, we need to read from the SRC in a very specific way. We keep looping
// until we've read the requested frame count, however we have an inner loop that keeps running until ma_src_read()
// returns 0, in which case we need to reload the SRC's input data and keep going.
ma_uint32
totalFramesRead
=
0
;
while
(
totalFramesRead
<
ma_countof
(
testSamples
))
{
ma_uint32
maxFramesToRead
=
128
;
ma_uint32
framesToRead
=
ma_countof
(
testSamples
);
if
(
framesToRead
>
maxFramesToRead
)
{
framesToRead
=
maxFramesToRead
;
}
ma_uint32
framesRead
=
(
ma_uint32
)
ma_src_read_deinterleaved
(
&
src
,
framesToRead
,
(
void
**
)
&
pFramesF32
,
NULL
);
if
(
framesRead
==
0
)
{
reload_src_input
();
}
totalFramesRead
+=
framesRead
;
pFramesF32
+=
framesRead
;
}
msigvis_channel_push_samples
(
&
channelSineWave
,
ma_countof
(
testSamples
),
testSamples
);
msigvis_screen_add_channel
(
&
screen
,
&
channelSineWave
);
int
exitCode
=
msigvis_run
(
&
sigvis
);
msigvis_screen_uninit
(
&
screen
);
msigvis_uninit
(
&
sigvis
);
#else
result
=
ma_device_start
(
&
device
);
if
(
result
!=
MA_SUCCESS
)
{
return
-
2
;
...
...
@@ -205,7 +138,6 @@ int main(int argc, char** argv)
printf
(
"Press Enter to quit...
\n
"
);
getchar
();
ma_device_uninit
(
&
device
);
#endif
return
0
;
}
dred
@
f18b0c5d
Subproject commit f18b0c5d9245560d529af9ba6fc9691ec7cad34e
tools/mini_sigvis/README.md
deleted
100644 → 0
View file @
cd44057a
This is a simple library for visualizing signals. This readme will be updated later when the library stabilizes.
\ No newline at end of file
tools/mini_sigvis/mini_sigvis.h
deleted
100644 → 0
View file @
cd44057a
// Signal visualization library. Public domain. See "unlicense" statement at the end of this file.
// mini_sigvis - v0.x - 2018-xx-xx
//
// David Reid - davidreidsoftware@gmail.com
#ifndef mini_sigvis_h
#define mini_sigvis_h
#include "../../miniaudio.h"
#include "../external/dred/source/dred/dtk/dtk.h"
#ifdef __cplusplus
extern
"C"
{
#endif
typedef
struct
msigvis_context
msigvis_context
;
typedef
struct
msigvis_screen
msigvis_screen
;
typedef
struct
msigvis_channel
msigvis_channel
;
struct
msigvis_context
{
dtk_context
tk
;
};
struct
msigvis_screen
{
dtk_window
window
;
dtk_uint32
sampleRate
;
float
zoomX
;
float
zoomY
;
dtk_color
bgColor
;
dtk_uint32
channelCount
;
dtk_uint32
channelCap
;
msigvis_channel
**
ppChannels
;
};
struct
msigvis_channel
{
ma_format
format
;
ma_uint32
sampleRate
;
dtk_color
color
;
ma_uint32
sampleCount
;
ma_uint32
bufferCapInSamples
;
ma_uint8
*
pBuffer
;
// The buffer containing the sample to visualize.
};
// Context
ma_result
msigvis_init
(
msigvis_context
*
pContext
);
void
msigvis_uninit
(
msigvis_context
*
pContext
);
int
msigvis_run
(
msigvis_context
*
pContext
);
// Screen
ma_result
msigvis_screen_init
(
msigvis_context
*
pContext
,
ma_uint32
screenWidth
,
ma_uint32
screenHeight
,
msigvis_screen
*
pScreen
);
void
msigvis_screen_uninit
(
msigvis_screen
*
pScreen
);
ma_result
msigvis_screen_show
(
msigvis_screen
*
pScreen
);
ma_result
msigvis_screen_hide
(
msigvis_screen
*
pScreen
);
ma_result
msigvis_screen_add_channel
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
);
ma_result
msigvis_screen_remove_channel
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
);
ma_result
msigvis_screen_remove_channel_by_index
(
msigvis_screen
*
pScreen
,
ma_uint32
iChannel
);
ma_result
msigvis_screen_find_channel_index
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
,
ma_uint32
*
pIndex
);
ma_result
msigvis_screen_redraw
(
msigvis_screen
*
pScreen
);
// Channel
ma_result
msigvis_channel_init
(
msigvis_context
*
pContext
,
ma_format
format
,
ma_uint32
sampleRate
,
msigvis_channel
*
pChannel
);
void
msigvis_channel_uninit
(
msigvis_channel
*
pChannel
);
ma_result
msigvis_channel_push_samples
(
msigvis_channel
*
pChannel
,
ma_uint32
sampleCount
,
const
void
*
pSamples
);
ma_result
msigvis_channel_pop_samples
(
msigvis_channel
*
pChannel
,
ma_uint32
sampleCount
);
float
msigvis_channel_get_sample_f32
(
msigvis_channel
*
pChannel
,
ma_uint32
iSample
);
#ifdef __cplusplus
}
#endif
#endif // mini_sigvis_h
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#ifdef MINI_SIGVIS_IMPLEMENTATION
#define MINIAUDIO_IMPLEMENTATION
#include "../../miniaudio.h"
#include "../external/dred/source/dred/dtk/dtk.c"
ma_result
msigvis_result_from_dtk
(
dtk_result
resultDTK
)
{
return
(
ma_result
)
resultDTK
;
}
ma_result
msigvis_init
(
msigvis_context
*
pContext
)
{
if
(
pContext
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
ma_zero_object
(
pContext
);
// DTK context.
dtk_result
resultDTK
=
dtk_init
(
&
pContext
->
tk
,
NULL
,
pContext
);
if
(
resultDTK
!=
DTK_SUCCESS
)
{
return
msigvis_result_from_dtk
(
resultDTK
);
}
return
MA_SUCCESS
;
}
void
msigvis_uninit
(
msigvis_context
*
pContext
)
{
if
(
pContext
==
NULL
)
{
return
;
}
dtk_uninit
(
&
pContext
->
tk
);
}
int
msigvis_run
(
msigvis_context
*
pContext
)
{
int
exitCode
=
0
;
for
(;;)
{
dtk_result
result
=
dtk_next_event
(
&
pContext
->
tk
,
DTK_TRUE
,
&
exitCode
);
// <-- DTK_TRUE = blocking.
if
(
result
!=
DTK_SUCCESS
)
{
break
;
}
}
return
exitCode
;
}
///////////////////////////////////////////////////////////////////////////////
//
// Screen
//
///////////////////////////////////////////////////////////////////////////////
dtk_bool32
msigvis_window_event_handler
(
dtk_event
*
pEvent
)
{
dtk_window
*
pWindow
=
DTK_WINDOW
(
pEvent
->
pControl
);
msigvis_screen
*
pScreen
=
(
msigvis_screen
*
)
DTK_CONTROL
(
pWindow
)
->
pUserData
;
dtk_assert
(
pScreen
!=
NULL
);
switch
(
pEvent
->
type
)
{
case
DTK_EVENT_CLOSE
:
{
dtk_post_quit_event
(
pEvent
->
pTK
,
0
);
}
break
;
case
DTK_EVENT_SIZE
:
{
dtk_window_scheduled_redraw
(
pWindow
,
dtk_window_get_client_rect
(
pWindow
));
}
break
;
case
DTK_EVENT_MOUSE_WHEEL
:
{
if
(
pEvent
->
mouseWheel
.
delta
>
0
)
{
pScreen
->
zoomX
=
pScreen
->
zoomX
*
(
2
*
pEvent
->
mouseWheel
.
delta
);
if
(
pScreen
->
zoomX
>
10000
.
0
f
)
{
pScreen
->
zoomX
=
10000
.
0
f
;
}
}
else
{
pScreen
->
zoomX
=
pScreen
->
zoomX
/
(
-
2
*
pEvent
->
mouseWheel
.
delta
);
if
(
pScreen
->
zoomX
<
0
.
000001
f
)
{
pScreen
->
zoomX
=
0
.
000001
f
;
}
}
dtk_window_scheduled_redraw
(
pWindow
,
dtk_window_get_client_rect
(
pWindow
));
}
break
;
case
DTK_EVENT_PAINT
:
{
dtk_surface
*
pSurface
=
pEvent
->
paint
.
pSurface
;
dtk_assert
(
pSurface
!=
NULL
);
dtk_surface_clear
(
pSurface
,
dtk_rgb
(
0
,
32
,
16
));
// At zoom level 1 we draw one tenth of a second worth of samples to the screen at the screens sample rate.
dtk_int32
screenSizeX
;
dtk_int32
screenSizeY
;
dtk_window_get_size
(
&
pScreen
->
window
,
&
screenSizeX
,
&
screenSizeY
);
float
baseSampleSpacingX
=
(
screenSizeX
/
(
float
)(
pScreen
->
sampleRate
/
10
))
*
pScreen
->
zoomX
;
float
baseSampleSpacingY
=
((
screenSizeY
/
1
)
/
2
.
0
f
)
*
pScreen
->
zoomY
;
for
(
ma_uint32
iChannel
=
0
;
iChannel
<
pScreen
->
channelCount
;
++
iChannel
)
{
msigvis_channel
*
pChannel
=
pScreen
->
ppChannels
[
iChannel
];
float
spacingFactorX
=
pScreen
->
sampleRate
/
(
float
)
pChannel
->
sampleRate
;
float
sampleSpacingX
=
baseSampleSpacingX
*
spacingFactorX
;
float
sampleSpacingY
=
baseSampleSpacingY
;
ma_uint32
sampleInterval
=
1
;
if
(
sampleSpacingX
<
1
)
{
sampleInterval
=
(
ma_uint32
)(
1
/
sampleSpacingX
);
}
if
(
sampleInterval
==
0
)
{
sampleInterval
=
1
;
// Safety.
}
ma_uint32
iFirstSample
=
0
;
for
(
ma_uint32
iSample
=
iFirstSample
;
iSample
<
pChannel
->
sampleCount
;
iSample
+=
sampleInterval
)
{
float
samplePosX
=
iSample
*
sampleSpacingX
;
float
samplePosY
=
msigvis_channel_get_sample_f32
(
pChannel
,
iSample
)
*
sampleSpacingY
*
-
1
;
// Invert the Y axis for graphics output.
dtk_rect
pointRect
;
pointRect
.
left
=
(
dtk_int32
)
samplePosX
;
pointRect
.
right
=
pointRect
.
left
+
2
;
pointRect
.
top
=
(
dtk_int32
)
samplePosY
+
(
screenSizeY
/
2
);
pointRect
.
bottom
=
pointRect
.
top
-
2
;
dtk_surface_draw_rect
(
pSurface
,
pointRect
,
pChannel
->
color
);
if
(
pointRect
.
right
>
screenSizeX
)
{
break
;
}
}
}
}
break
;
}
return
dtk_window_default_event_handler
(
pEvent
);
}
ma_result
msigvis_screen_init
(
msigvis_context
*
pContext
,
ma_uint32
screenWidth
,
ma_uint32
screenHeight
,
msigvis_screen
*
pScreen
)
{
if
(
pScreen
==
NULL
)
{
return
DTK_INVALID_ARGS
;
}
ma_zero_object
(
pScreen
);
dtk_result
resultDTK
=
dtk_window_init
(
&
pContext
->
tk
,
msigvis_window_event_handler
,
NULL
,
dtk_window_type_toplevel
,
"mini_sigvis"
,
screenWidth
,
screenHeight
,
&
pScreen
->
window
);
if
(
resultDTK
!=
DTK_SUCCESS
)
{
return
msigvis_result_from_dtk
(
resultDTK
);
}
pScreen
->
window
.
control
.
pUserData
=
pScreen
;
pScreen
->
sampleRate
=
48000
;
pScreen
->
zoomX
=
1
;
pScreen
->
zoomY
=
1
;
pScreen
->
bgColor
=
dtk_rgb
(
0
,
32
,
16
);
return
DTK_SUCCESS
;
}
void
msigvis_screen_uninit
(
msigvis_screen
*
pScreen
)
{
if
(
pScreen
==
NULL
)
{
return
;
}
dtk_window_uninit
(
&
pScreen
->
window
);
}
ma_result
msigvis_screen_show
(
msigvis_screen
*
pScreen
)
{
if
(
pScreen
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
return
msigvis_result_from_dtk
(
dtk_window_show
(
&
pScreen
->
window
,
DTK_SHOW_NORMAL
));
}
ma_result
msigvis_screen_hide
(
msigvis_screen
*
pScreen
)
{
if
(
pScreen
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
return
msigvis_result_from_dtk
(
dtk_window_hide
(
&
pScreen
->
window
));
}
ma_result
msigvis_screen_add_channel
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
)
{
if
(
pScreen
==
NULL
||
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
// Expand if necessary.
if
(
pScreen
->
channelCap
==
pScreen
->
channelCount
)
{
ma_uint32
newCap
=
pScreen
->
channelCap
*
2
;
if
(
newCap
==
0
)
{
newCap
=
1
;
}
msigvis_channel
**
ppNewBuffer
=
(
msigvis_channel
**
)
ma_realloc
(
pScreen
->
ppChannels
,
sizeof
(
*
pScreen
->
ppChannels
)
*
newCap
);
if
(
ppNewBuffer
==
NULL
)
{
return
MA_OUT_OF_MEMORY
;
}
pScreen
->
channelCap
=
newCap
;
pScreen
->
ppChannels
=
ppNewBuffer
;
}
pScreen
->
ppChannels
[
pScreen
->
channelCount
]
=
pChannel
;
pScreen
->
channelCount
+=
1
;
msigvis_screen_redraw
(
pScreen
);
return
MA_SUCCESS
;
}
ma_result
msigvis_screen_remove_channel
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
)
{
if
(
pScreen
==
NULL
||
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
ma_uint32
iChannel
;
ma_result
result
=
msigvis_screen_find_channel_index
(
pScreen
,
pChannel
,
&
iChannel
);
if
(
result
!=
MA_SUCCESS
)
{
return
result
;
}
return
msigvis_screen_remove_channel_by_index
(
pScreen
,
iChannel
);
}
ma_result
msigvis_screen_remove_channel_by_index
(
msigvis_screen
*
pScreen
,
ma_uint32
iChannel
)
{
if
(
pScreen
==
NULL
||
iChannel
>
pScreen
->
channelCount
)
{
return
MA_INVALID_ARGS
;
}
if
(
pScreen
->
channelCount
==
0
)
{
return
MA_INVALID_OPERATION
;
}
if
(
iChannel
<
pScreen
->
channelCount
-
1
)
{
memmove
(
pScreen
->
ppChannels
+
iChannel
,
pScreen
->
ppChannels
+
iChannel
+
1
,
sizeof
(
*
pScreen
->
ppChannels
)
*
(
pScreen
->
channelCount
-
iChannel
-
1
));
}
pScreen
->
channelCount
-=
1
;
msigvis_screen_redraw
(
pScreen
);
return
MA_SUCCESS
;
}
ma_result
msigvis_screen_find_channel_index
(
msigvis_screen
*
pScreen
,
msigvis_channel
*
pChannel
,
ma_uint32
*
pIndex
)
{
if
(
pScreen
==
NULL
||
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
for
(
ma_uint32
iChannel
=
0
;
iChannel
<
pScreen
->
channelCount
;
++
iChannel
)
{
if
(
pScreen
->
ppChannels
[
iChannel
]
==
pChannel
)
{
*
pIndex
=
iChannel
;
return
MA_SUCCESS
;
}
}
return
MA_ERROR
;
}
ma_result
msigvis_screen_redraw
(
msigvis_screen
*
pScreen
)
{
if
(
pScreen
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
return
msigvis_result_from_dtk
(
dtk_window_scheduled_redraw
(
&
pScreen
->
window
,
dtk_window_get_client_rect
(
&
pScreen
->
window
)));
}
///////////////////////////////////////////////////////////////////////////////
//
// Channel
//
///////////////////////////////////////////////////////////////////////////////
ma_result
msigvis_channel_init
(
msigvis_context
*
pContext
,
ma_format
format
,
ma_uint32
sampleRate
,
msigvis_channel
*
pChannel
)
{
(
void
)
pContext
;
if
(
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
ma_zero_object
(
pChannel
);
if
(
format
==
ma_format_unknown
||
sampleRate
==
0
)
{
return
MA_INVALID_ARGS
;
}
pChannel
->
format
=
format
;
pChannel
->
sampleRate
=
sampleRate
;
pChannel
->
color
=
dtk_rgb
(
255
,
255
,
255
);
return
MA_SUCCESS
;
}
void
msigvis_channel_uninit
(
msigvis_channel
*
pChannel
)
{
if
(
pChannel
==
NULL
)
{
return
;
}
ma_free
(
pChannel
->
pBuffer
);
}
ma_result
msigvis_channel_push_samples
(
msigvis_channel
*
pChannel
,
ma_uint32
sampleCount
,
const
void
*
pSamples
)
{
if
(
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
ma_uint32
bps
=
ma_get_bytes_per_sample
(
pChannel
->
format
);
// Resize the buffer if necessary.
if
(
pChannel
->
sampleCount
+
sampleCount
>=
pChannel
->
bufferCapInSamples
)
{
ma_uint32
newBufferCapInSamples
=
ma_max
(
pChannel
->
sampleCount
+
sampleCount
,
pChannel
->
bufferCapInSamples
*
2
);
if
(
newBufferCapInSamples
==
0
)
{
newBufferCapInSamples
=
32
;
}
ma_uint8
*
pNewBuffer
=
(
ma_uint8
*
)
ma_realloc
(
pChannel
->
pBuffer
,
newBufferCapInSamples
*
bps
);
if
(
pNewBuffer
==
NULL
)
{
return
MA_OUT_OF_MEMORY
;
}
pChannel
->
pBuffer
=
pNewBuffer
;
pChannel
->
bufferCapInSamples
=
newBufferCapInSamples
;
}
ma_copy_memory
(
pChannel
->
pBuffer
+
pChannel
->
sampleCount
*
bps
,
pSamples
,
sampleCount
*
bps
);
pChannel
->
sampleCount
+=
sampleCount
;
return
MA_SUCCESS
;
}
ma_result
msigvis_channel_pop_samples
(
msigvis_channel
*
pChannel
,
ma_uint32
sampleCount
)
{
if
(
pChannel
==
NULL
)
{
return
MA_INVALID_ARGS
;
}
if
(
sampleCount
>
pChannel
->
sampleCount
)
{
sampleCount
=
pChannel
->
sampleCount
;
}
ma_uint32
bps
=
ma_get_bytes_per_sample
(
pChannel
->
format
);
// This is just a dumb "move everything down" type of data movement. Need to change this to a circular buffer to make this more efficient.
ma_uint32
bytesToRemove
=
sampleCount
*
bps
;
ma_assert
(
bytesToRemove
>
0
);
memmove
(
pChannel
->
pBuffer
,
pChannel
->
pBuffer
+
bytesToRemove
,
pChannel
->
sampleCount
*
bps
-
bytesToRemove
);
pChannel
->
sampleCount
-=
sampleCount
;
return
MA_SUCCESS
;
}
float
msigvis_channel_get_sample_f32
(
msigvis_channel
*
pChannel
,
ma_uint32
iSample
)
{
switch
(
pChannel
->
format
)
{
case
ma_format_f32
:
return
*
((
float
*
)
pChannel
->
pBuffer
+
iSample
);
default:
return
0
;
}
}
#endif
\ No newline at end of file
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