[alsa-devel] a52: update to new libavcodec API
Hi, this patchset updates the a52 plugin to work with the upcoming Libav 9 release. In doing that, it drops support for pre-0.8 libavcodec versions. I think it is not unreasonable to demand at least a year old libavcodec to compile latest git of alsa-plugins and keeping compatibility with the old audio encoding API would result in ugly #ifdefery mess.
The plugin compiles and appears to work, but as I have no SPDIF receiver, I cannot properly test it. I would very much appreciate it if someone else could do that.
Please comment.
It is required for the new audio encoding API. --- a52/Makefile.am | 3 +-- a52/pcm_a52.c | 13 +------------ configure.in | 15 +-------------- rate-lavc/Makefile.am | 3 +-- rate-lavc/rate_lavcrate.c | 2 +- 5 files changed, 5 insertions(+), 31 deletions(-)
diff --git a/a52/Makefile.am b/a52/Makefile.am index 48567b4..f8c3663 100644 --- a/a52/Makefile.am +++ b/a52/Makefile.am @@ -2,8 +2,7 @@ asound_module_pcm_a52_LTLIBRARIES = libasound_module_pcm_a52.la
asound_module_pcm_a52dir = @ALSA_PLUGIN_DIR@
-AM_CFLAGS = -Wall -g @ALSA_CFLAGS@ @AVCODEC_CFLAGS@ \ - -DAVCODEC_HEADER="@AVCODEC_HEADER@" +AM_CFLAGS = -Wall -g @ALSA_CFLAGS@ @AVCODEC_CFLAGS@ AM_LDFLAGS = -module -avoid-version -export-dynamic -no-undefined $(LDFLAGS_NOUNDEFINED)
libasound_module_pcm_a52_la_SOURCES = pcm_a52.c diff --git a/a52/pcm_a52.c b/a52/pcm_a52.c index 00c7c59..c1dd96e 100644 --- a/a52/pcm_a52.c +++ b/a52/pcm_a52.c @@ -25,7 +25,7 @@ #include <alsa/asoundlib.h> #include <alsa/pcm_external.h> #include <alsa/pcm_plugin.h> -#include AVCODEC_HEADER +#include <libavcodec/avcodec.h>
struct a52_ctx { snd_pcm_ioplug_t io; @@ -170,13 +170,8 @@ static int fill_data(snd_pcm_ioplug_t *io, static unsigned int ch_index[3][6] = { { 0, 1 }, { 0, 1, 2, 3 }, -#if LIBAVCODEC_VERSION_MAJOR > 52 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 26) /* current libavcodec expects SMPTE order */ { 0, 1, 4, 5, 2, 3 }, -#else - /* libavcodec older than r18540 expects A52 order */ - { 0, 4, 1, 2, 3, 5 }, -#endif }; /* flatten copy to n-channel interleaved */ dst_step = io->channels; @@ -436,12 +431,7 @@ static int a52_prepare(snd_pcm_ioplug_t *io) rec->avctx->bit_rate = rec->bitrate * 1000; rec->avctx->sample_rate = io->rate; rec->avctx->channels = io->channels; -#if LIBAVCODEC_VERSION_MAJOR > 52 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 95) rec->avctx->sample_fmt = AV_SAMPLE_FMT_S16; -#else - rec->avctx->sample_fmt = SAMPLE_FMT_S16; -#endif -#if LIBAVCODEC_VERSION_MAJOR > 52 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 3) switch (io->channels) { case 2: rec->avctx->channel_layout = CH_LAYOUT_STEREO; @@ -455,7 +445,6 @@ static int a52_prepare(snd_pcm_ioplug_t *io) default: break; } -#endif
if (avcodec_open(rec->avctx, rec->codec) < 0) return -EINVAL; diff --git a/configure.in b/configure.in index 0bf0b8e..2b422df 100644 --- a/configure.in +++ b/configure.in @@ -67,7 +67,7 @@ AC_ARG_ENABLE([avcodec], AS_HELP_STRING([--disable-avcodec], [Don't build plugins depending on avcodec (a52)]))
if test "x$enable_avcodec" != "xno"; then - PKG_CHECK_MODULES(AVCODEC, [libavcodec], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) + PKG_CHECK_MODULES(AVCODEC, [libavcodec >= 53.34.0], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) fi
if test "x$HAVE_AVCODEC" = "xno"; then @@ -90,21 +90,9 @@ if test "x$HAVE_AVCODEC" = "xno"; then LDFLAGS="$LDFLAGS_saved" fi
-if test $HAVE_AVCODEC = yes; then - AVCODEC_HEADER="" - AC_CHECK_HEADER([ffmpeg/avcodec.h], [AVCODEC_HEADER='<ffmpeg/avcodec.h>']) - if test -z "$AVCODEC_HEADER"; then - AC_CHECK_HEADER([libavcodec/avcodec.h], [AVCODEC_HEADER='<libavcodec/avcodec.h>']) - fi - if test -z "$AVCODEC_HEADER"; then - HAVE_AVCODEC=no - fi -fi - AM_CONDITIONAL(HAVE_AVCODEC, test x$HAVE_AVCODEC = xyes) AC_SUBST(AVCODEC_CFLAGS) AC_SUBST(AVCODEC_LIBS) -AC_SUBST(AVCODEC_HEADER)
PKG_CHECK_MODULES(speexdsp, [speexdsp >= 1.2], [HAVE_SPEEXDSP="yes"], [HAVE_SPEEXDSP=""]) AM_CONDITIONAL(HAVE_SPEEXDSP, test "$HAVE_SPEEXDSP" = "yes") @@ -214,7 +202,6 @@ echo "A52, lavc plugins: $HAVE_AVCODEC" if test "$HAVE_AVCODEC" = "yes"; then echo " AVCODEC_CFLAGS: $AVCODEC_CFLAGS" echo " AVCODEC_LIBS: $AVCODEC_LIBS" - echo " AVCODEC_HEADER: $AVCODEC_HEADER" fi echo "Speex rate plugin: $PPH" echo "Speex preprocess plugin: $HAVE_SPEEXDSP" diff --git a/rate-lavc/Makefile.am b/rate-lavc/Makefile.am index 5cffd44..e100556 100644 --- a/rate-lavc/Makefile.am +++ b/rate-lavc/Makefile.am @@ -2,8 +2,7 @@ asound_module_rate_lavcrate_LTLIBRARIES = libasound_module_rate_lavcrate.la
asound_module_rate_lavcratedir = @ALSA_PLUGIN_DIR@
-AM_CFLAGS = -Wall -g @ALSA_CFLAGS@ @AVCODEC_CFLAGS@ \ - -DAVCODEC_HEADER="@AVCODEC_HEADER@" +AM_CFLAGS = -Wall -g @ALSA_CFLAGS@ @AVCODEC_CFLAGS@ AM_LDFLAGS = -module -avoid-version -export-dynamic -no-undefined $(LDFLAGS_NOUNDEFINED)
libasound_module_rate_lavcrate_la_SOURCES = rate_lavcrate.c diff --git a/rate-lavc/rate_lavcrate.c b/rate-lavc/rate_lavcrate.c index 14a2198..6f3e74d 100644 --- a/rate-lavc/rate_lavcrate.c +++ b/rate-lavc/rate_lavcrate.c @@ -19,7 +19,7 @@ #include <stdio.h> #include <alsa/asoundlib.h> #include <alsa/pcm_rate.h> -#include AVCODEC_HEADER +#include <libavcodec/avcodec.h> #include "gcd.h"
static int filter_size = 16;
--- a52/pcm_a52.c | 12 ++++++------ configure.in | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/a52/pcm_a52.c b/a52/pcm_a52.c index c1dd96e..9fd326c 100644 --- a/a52/pcm_a52.c +++ b/a52/pcm_a52.c @@ -26,6 +26,7 @@ #include <alsa/pcm_external.h> #include <alsa/pcm_plugin.h> #include <libavcodec/avcodec.h> +#include <libavutil/audioconvert.h>
struct a52_ctx { snd_pcm_ioplug_t io; @@ -424,7 +425,7 @@ static int a52_prepare(snd_pcm_ioplug_t *io)
a52_free(rec);
- rec->avctx = avcodec_alloc_context(); + rec->avctx = avcodec_alloc_context3(rec->codec); if (! rec->avctx) return -ENOMEM;
@@ -434,19 +435,19 @@ static int a52_prepare(snd_pcm_ioplug_t *io) rec->avctx->sample_fmt = AV_SAMPLE_FMT_S16; switch (io->channels) { case 2: - rec->avctx->channel_layout = CH_LAYOUT_STEREO; + rec->avctx->channel_layout = AV_CH_LAYOUT_STEREO; break; case 4: - rec->avctx->channel_layout = CH_LAYOUT_QUAD; + rec->avctx->channel_layout = AV_CH_LAYOUT_QUAD; break; case 6: - rec->avctx->channel_layout = CH_LAYOUT_5POINT1; + rec->avctx->channel_layout = AV_CH_LAYOUT_5POINT1; break; default: break; }
- if (avcodec_open(rec->avctx, rec->codec) < 0) + if (avcodec_open2(rec->avctx, rec->codec, NULL) < 0) return -EINVAL;
rec->inbuf = malloc(rec->avctx->frame_size * 2 * io->channels); @@ -691,7 +692,6 @@ SND_PCM_PLUGIN_DEFINE_FUNC(a52) rec->channels = channels; rec->format = format;
- avcodec_init(); avcodec_register_all();
rec->codec = avcodec_find_encoder_by_name("ac3_fixed"); diff --git a/configure.in b/configure.in index 2b422df..0a3d424 100644 --- a/configure.in +++ b/configure.in @@ -67,7 +67,7 @@ AC_ARG_ENABLE([avcodec], AS_HELP_STRING([--disable-avcodec], [Don't build plugins depending on avcodec (a52)]))
if test "x$enable_avcodec" != "xno"; then - PKG_CHECK_MODULES(AVCODEC, [libavcodec >= 53.34.0], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) + PKG_CHECK_MODULES(AVCODEC, [libavcodec >= 53.34.0 libavutil >= 51.21.0], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) fi
if test "x$HAVE_AVCODEC" = "xno"; then @@ -84,8 +84,8 @@ if test "x$HAVE_AVCODEC" = "xno"; then LDFLAGS_saved="$LDFLAGS" CFLAGS="$CFLAGS $AVCODEC_CFLAGS" LDFLAGS="$LDFLAGS $AVCODEC_LIBS" - AVCODEC_LIBS="$AVCODEC_LIBS -lavcodec" - AC_CHECK_LIB([avcodec], [avcodec_open], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) + AVCODEC_LIBS="$AVCODEC_LIBS -lavutil -lavcodec" + AC_CHECK_LIB([avcodec], [avcodec_open2], [HAVE_AVCODEC=yes], [HAVE_AVCODEC=no]) CFLAGS="$CFLAGS_saved" LDFLAGS="$LDFLAGS_saved" fi
--- a52/pcm_a52.c | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-)
diff --git a/a52/pcm_a52.c b/a52/pcm_a52.c index 9fd326c..b9ffddb 100644 --- a/a52/pcm_a52.c +++ b/a52/pcm_a52.c @@ -27,12 +27,14 @@ #include <alsa/pcm_plugin.h> #include <libavcodec/avcodec.h> #include <libavutil/audioconvert.h> +#include <libavutil/mem.h>
struct a52_ctx { snd_pcm_ioplug_t io; snd_pcm_t *slave; AVCodec *codec; AVCodecContext *avctx; + AVFrame *frame; snd_pcm_format_t format; unsigned int channels; unsigned int rate; @@ -51,24 +53,24 @@ struct a52_ctx { /* convert the PCM data to A52 stream in IEC958 */ static void convert_data(struct a52_ctx *rec) { - int out_bytes; + AVPacket pkt = { .data = rec->outbuf + 8, .size = rec->outbuf_size - 8 }; + int got_frame; + + avcodec_encode_audio2(rec->avctx, &pkt, rec->frame, &got_frame);
- out_bytes = avcodec_encode_audio(rec->avctx, rec->outbuf + 8, - rec->outbuf_size - 8, - rec->inbuf); rec->outbuf[0] = 0xf8; /* sync words */ rec->outbuf[1] = 0x72; rec->outbuf[2] = 0x4e; rec->outbuf[3] = 0x1f; rec->outbuf[4] = rec->outbuf[13] & 7; /* bsmod */ rec->outbuf[5] = 0x01; /* data type */ - rec->outbuf[6] = ((out_bytes * 8) >> 8) & 0xff; - rec->outbuf[7] = (out_bytes * 8) & 0xff; + rec->outbuf[6] = ((pkt.size * 8) >> 8) & 0xff; + rec->outbuf[7] = (pkt.size * 8) & 0xff; /* swap bytes for little-endian 16bit */ if (rec->format == SND_PCM_FORMAT_S16_LE) - swab(rec->outbuf, rec->outbuf, out_bytes + 8); - memset(rec->outbuf + 8 + out_bytes, 0, - rec->outbuf_size - 8 - out_bytes); + swab(rec->outbuf, rec->outbuf, pkt.size + 8); + memset(rec->outbuf + 8 + pkt.size, 0, + rec->outbuf_size - 8 - pkt.size); rec->remain = rec->outbuf_size / 4; rec->filled = 0; } @@ -408,6 +410,12 @@ static void a52_free(struct a52_ctx *rec) av_free(rec->avctx); rec->avctx = NULL; } +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(54, 28, 0) + avcodec_free_frame(&rec->frame); +#else + av_freep(&rec->frame); +#endif + free(rec->inbuf); rec->inbuf = NULL; free(rec->outbuf); @@ -429,6 +437,10 @@ static int a52_prepare(snd_pcm_ioplug_t *io) if (! rec->avctx) return -ENOMEM;
+ rec->frame = avcodec_alloc_frame(); + if (!rec->frame) + return -ENOMEM; + rec->avctx->bit_rate = rec->bitrate * 1000; rec->avctx->sample_rate = io->rate; rec->avctx->channels = io->channels; @@ -458,6 +470,10 @@ static int a52_prepare(snd_pcm_ioplug_t *io) if (! rec->outbuf) return -ENOMEM;
+ rec->frame->data[0] = (uint8_t*)rec->inbuf; + rec->frame->linesize[0] = rec->avctx->frame_size * 2 * io->channels; + rec->frame->nb_samples = rec->avctx->frame_size; + rec->transfer = 0; rec->remain = 0; rec->filled = 0;
AC3 encoder in recent libavcodec versions requires planar (non-interleaved) input. --- a52/pcm_a52.c | 120 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 38 deletions(-)
diff --git a/a52/pcm_a52.c b/a52/pcm_a52.c index b9ffddb..b60ec2e 100644 --- a/a52/pcm_a52.c +++ b/a52/pcm_a52.c @@ -28,6 +28,9 @@ #include <libavcodec/avcodec.h> #include <libavutil/audioconvert.h> #include <libavutil/mem.h> +#include <libavutil/samplefmt.h> + +#define MAX_CHANNELS 6
struct a52_ctx { snd_pcm_ioplug_t io; @@ -35,11 +38,11 @@ struct a52_ctx { AVCodec *codec; AVCodecContext *avctx; AVFrame *frame; + int planar; snd_pcm_format_t format; unsigned int channels; unsigned int rate; unsigned int bitrate; - short *inbuf; unsigned char *outbuf; int outbuf_size; snd_pcm_uframes_t transfer; @@ -109,11 +112,16 @@ static int a52_drain(snd_pcm_ioplug_t *io) int err;
if (rec->filled) { + int planes = rec->planar ? io->channels : 1; + int blocksize = 2 * (rec->planar ? 1 : io->channels); + int i; + if ((err = write_out_pending(io, rec)) < 0) return err; /* remaining data must be converted and sent out */ - memset(rec->inbuf + rec->filled * io->channels, 0, - (rec->avctx->frame_size - rec->filled) * io->channels * 2); + for (i = 0; i < planes; i++) + memset(rec->frame->data[i] + rec->filled * blocksize, 0, + (rec->avctx->frame_size - rec->filled) * blocksize); convert_data(rec); } err = write_out_pending(io, rec); @@ -151,6 +159,12 @@ static int fill_data(snd_pcm_ioplug_t *io, unsigned int offset, unsigned int size, int interleaved) { + static unsigned int ch_index[3][6] = { + { 0, 1 }, + { 0, 1, 2, 3 }, + /* current libavcodec expects SMPTE order */ + { 0, 1, 4, 5, 2, 3 }, + }; struct a52_ctx *rec = io->private_data; unsigned int len = rec->avctx->frame_size - rec->filled; short *src, *dst; @@ -163,35 +177,42 @@ static int fill_data(snd_pcm_ioplug_t *io, if (size > len) size = len;
- dst = rec->inbuf + rec->filled * io->channels; - if (interleaved) { - memcpy(dst, areas->addr + offset * io->channels * 2, - size * io->channels * 2); - } else { - unsigned int i, ch, dst_step; - short *dst1; - static unsigned int ch_index[3][6] = { - { 0, 1 }, - { 0, 1, 2, 3 }, - /* current libavcodec expects SMPTE order */ - { 0, 1, 4, 5, 2, 3 }, - }; - /* flatten copy to n-channel interleaved */ - dst_step = io->channels; - for (ch = 0; ch < io->channels; ch++, dst++) { + if (rec->planar) { + unsigned int i; + + for (i = 0; i < io->channels; i++) { const snd_pcm_channel_area_t *ap; - ap = &areas[ch_index[io->channels / 2 - 1][ch]]; - dst1 = dst; - src = (short *)(ap->addr + - (ap->first + offset * ap->step) / 8); - src_step = ap->step / 16; /* in word */ - for (i = 0; i < size; i++) { - *dst1 = *src; - src += src_step; - dst1 += dst_step; + ap = &areas[ch_index[io->channels / 2 - 1][i]]; + memcpy(rec->frame->data[i], ap->addr + + (ap->first + offset * ap->step) / 8, size * 2); + } + } else { + dst = (short*)(rec->frame->data[0] + rec->filled * io->channels * 2); + if (interleaved) { + memcpy(dst, areas->addr + offset * io->channels * 2, + size * io->channels * 2); + } else { + unsigned int i, ch, dst_step; + short *dst1; + + /* flatten copy to n-channel interleaved */ + dst_step = io->channels; + for (ch = 0; ch < io->channels; ch++, dst++) { + const snd_pcm_channel_area_t *ap; + ap = &areas[ch_index[io->channels / 2 - 1][ch]]; + dst1 = dst; + src = (short *)(ap->addr + + (ap->first + offset * ap->step) / 8); + src_step = ap->step / 16; /* in word */ + for (i = 0; i < size; i++) { + *dst1 = *src; + src += src_step; + dst1 += dst_step; + } } } } + rec->filled += size; if (rec->filled == rec->avctx->frame_size) { convert_data(rec); @@ -410,14 +431,14 @@ static void a52_free(struct a52_ctx *rec) av_free(rec->avctx); rec->avctx = NULL; } + if (rec->frame) + av_freep(&rec->frame->data[0]); #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(54, 28, 0) avcodec_free_frame(&rec->frame); #else av_freep(&rec->frame); #endif
- free(rec->inbuf); - rec->inbuf = NULL; free(rec->outbuf); rec->outbuf = NULL; } @@ -430,6 +451,7 @@ static void a52_free(struct a52_ctx *rec) static int a52_prepare(snd_pcm_ioplug_t *io) { struct a52_ctx *rec = io->private_data; + int err;
a52_free(rec);
@@ -444,7 +466,7 @@ static int a52_prepare(snd_pcm_ioplug_t *io) rec->avctx->bit_rate = rec->bitrate * 1000; rec->avctx->sample_rate = io->rate; rec->avctx->channels = io->channels; - rec->avctx->sample_fmt = AV_SAMPLE_FMT_S16; + rec->avctx->sample_fmt = rec->codec->sample_fmts[0]; switch (io->channels) { case 2: rec->avctx->channel_layout = AV_CH_LAYOUT_STEREO; @@ -462,16 +484,18 @@ static int a52_prepare(snd_pcm_ioplug_t *io) if (avcodec_open2(rec->avctx, rec->codec, NULL) < 0) return -EINVAL;
- rec->inbuf = malloc(rec->avctx->frame_size * 2 * io->channels); - if (! rec->inbuf) - return -ENOMEM; + err = av_samples_alloc(rec->frame->data, rec->frame->linesize, + io->channels, rec->avctx->frame_size, + rec->avctx->sample_fmt, 32); + if (err < 0) + return - ENOMEM; + rec->outbuf_size = rec->avctx->frame_size * 4; rec->outbuf = malloc(rec->outbuf_size); if (! rec->outbuf) return -ENOMEM;
- rec->frame->data[0] = (uint8_t*)rec->inbuf; - rec->frame->linesize[0] = rec->avctx->frame_size * 2 * io->channels; + rec->planar = av_sample_fmt_is_planar(rec->avctx->sample_fmt); rec->frame->nb_samples = rec->avctx->frame_size;
rec->transfer = 0; @@ -550,19 +574,31 @@ static snd_pcm_ioplug_callback_t a52_ops = {
static int a52_set_hw_constraint(struct a52_ctx *rec) { - unsigned int accesses[] = { + unsigned int accesses_all[] = { SND_PCM_ACCESS_MMAP_INTERLEAVED, SND_PCM_ACCESS_MMAP_NONINTERLEAVED, SND_PCM_ACCESS_RW_INTERLEAVED, SND_PCM_ACCESS_RW_NONINTERLEAVED }; + unsigned int accesses_planar[] = { + SND_PCM_ACCESS_MMAP_NONINTERLEAVED, + SND_PCM_ACCESS_RW_NONINTERLEAVED, + }; + unsigned int *accesses = accesses_all; + int accesses_size = ARRAY_SIZE(accesses_all); unsigned int formats[] = { SND_PCM_FORMAT_S16 }; int err; snd_pcm_uframes_t buffer_max; unsigned int period_bytes, max_periods;
+ + if (av_sample_fmt_is_planar(rec->codec->sample_fmts[0])) { + accesses = accesses_planar; + accesses_size = ARRAY_SIZE(accesses_planar); + } + if ((err = snd_pcm_ioplug_set_param_list(&rec->io, SND_PCM_IOPLUG_HW_ACCESS, - ARRAY_SIZE(accesses), accesses)) < 0 || + accesses_size, accesses)) < 0 || (err = snd_pcm_ioplug_set_param_list(&rec->io, SND_PCM_IOPLUG_HW_FORMAT, ARRAY_SIZE(formats), formats)) < 0 || (err = snd_pcm_ioplug_set_param_minmax(&rec->io, SND_PCM_IOPLUG_HW_CHANNELS, @@ -721,6 +757,14 @@ SND_PCM_PLUGIN_DEFINE_FUNC(a52) goto error; }
+ if (!rec->codec->sample_fmts || + (rec->codec->sample_fmts[0] != AV_SAMPLE_FMT_S16 && + rec->codec->sample_fmts[0] != AV_SAMPLE_FMT_S16P)) { + SNDERR("AC3 encoder does not support s16 audio."); + err = -EINVAL; + goto error; + } + if (! pcm_string) { snprintf(devstr, sizeof(devstr), "iec958:{AES0 0x%x AES1 0x%x AES2 0x%x AES3 0x%x %s%s}",
At Sun, 11 Nov 2012 11:30:00 +0100, Anton Khirnov wrote:
Hi, this patchset updates the a52 plugin to work with the upcoming Libav 9 release. In doing that, it drops support for pre-0.8 libavcodec versions. I think it is not unreasonable to demand at least a year old libavcodec to compile latest git of alsa-plugins and keeping compatibility with the old audio encoding API would result in ugly #ifdefery mess.
Thanks for the patches.
But, no, we still need the compatibility. One year old is just a baby; you can't guarantee that all users have already switched to it.
If #ifdef's are problems, you can write a cleaner code by splitting files, too.
Takashi
participants (2)
-
Anton Khirnov
-
Takashi Iwai