diff --git a/Changelog b/Changelog
index 711c843b99..85df371b55 100644
--- a/Changelog
+++ b/Changelog
@@ -75,6 +75,7 @@ version <next>:
 - PFM decoder
 - dblur video filter
 - Real War KVAG muxer
+- basicplay filter source
 
 
 version 4.2:
diff --git a/doc/filters.texi b/doc/filters.texi
index f76604c51e..08b0707a01 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -6021,6 +6021,56 @@ anullsrc=r=48000:cl=mono
 
 All the parameters need to be explicitly defined.
 
+@section basicplay
+
+An audio source that reimplments the command PLAY from Microsoft BASIC 1.0.
+It generates the samples for a tune described in a very simple text
+description, with electronic-sounding notes.
+
+@table @command
+
+@item DO, RE, MI, FA, SO, LA, SI
+Note, named in fixed-do solfège. The note can possibly be followed by "#" or
+"B".
+Note: the "B" suffix will not change octave: "DOB" is a half-tone below the
+next "DO", not the current one.
+
+@item P
+Silent pause.
+
+@item O@var{o} (1-5; default 4)
+Set the octave for the following notes. "O1DO" is 33 Hz, "O1SI" is 62 Hz,
+"O5DO" is 523 Hz, "O5SI" is 988 Hz.
+
+@item A@var{a} (0-255; default 0)
+Set the "attack" for the following notes: 0 means continuous sound, @var{a}
+values mean the duration of the sound is bounded by the duration of a whole
+note divided by exp(1)×@var{a}.
+The default is 0.
+
+@item L@var{l} (1-96; default 24)
+Set the note duration for the following notes: 96 is a whole note /
+semibreve / ronde, other values give a proportional duration.
+The default is 24, meaning a quarter note / crochet / noire.
+
+@item T@var{t} (1-255; default 5)
+Set the tempo period for the following notes. The default 5 causes 5 quarter
+notes per second, and everything else is proportional.
+Put it simply, T@var{t}L@var{l} sets the duration of the notes to
+@var{t}×@var{l}/600 second.
+
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Generate the beginning of Für Elise:
+@example
+basicplay=T20L6O4MIRE#MIRE#MIO3SIO4REDOL12O3LA
+@end example
+@end itemize
+
 @section flite
 
 Synthesize a voice utterance using the libflite library.
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 83d939f0b1..849aad05ba 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -148,6 +148,7 @@ OBJS-$(CONFIG_AEVALSRC_FILTER)               += aeval.o
 OBJS-$(CONFIG_AFIRSRC_FILTER)                += asrc_afirsrc.o
 OBJS-$(CONFIG_ANOISESRC_FILTER)              += asrc_anoisesrc.o
 OBJS-$(CONFIG_ANULLSRC_FILTER)               += asrc_anullsrc.o
+OBJS-$(CONFIG_BASICPLAY_FILTER)              += asrc_basicplay.o intsine.o
 OBJS-$(CONFIG_FLITE_FILTER)                  += asrc_flite.o
 OBJS-$(CONFIG_HILBERT_FILTER)                += asrc_hilbert.o
 OBJS-$(CONFIG_SINC_FILTER)                   += asrc_sinc.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 1183e40267..daddddf159 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -142,6 +142,7 @@ extern AVFilter ff_asrc_aevalsrc;
 extern AVFilter ff_asrc_afirsrc;
 extern AVFilter ff_asrc_anoisesrc;
 extern AVFilter ff_asrc_anullsrc;
+extern AVFilter ff_asrc_basicplay;
 extern AVFilter ff_asrc_flite;
 extern AVFilter ff_asrc_hilbert;
 extern AVFilter ff_asrc_sinc;
diff --git a/libavfilter/asrc_basicplay.c b/libavfilter/asrc_basicplay.c
new file mode 100644
index 0000000000..979a07e028
--- /dev/null
+++ b/libavfilter/asrc_basicplay.c
@@ -0,0 +1,309 @@
+/*
+ * Copyright (c) 2013 Nicolas George
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with FFmpeg; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
+#include "libavutil/opt.h"
+#include "audio.h"
+#include "avfilter.h"
+#include "filters.h"
+#include "internal.h"
+#include "intsine.h"
+
+static const unsigned time_base = 960000;
+
+typedef struct Note {
+    unsigned duration;
+    uint8_t note;
+    uint8_t octave;
+} Note;
+
+typedef struct PlayContext {
+    const AVClass *class;
+    const char *tune;
+    Note *notes;
+    int64_t pts;
+    uint32_t phi;
+    unsigned nb_notes;
+    unsigned cur_note;
+    int sample_rate;
+    int16_t sin[SINE_PERIOD];
+} PlayContext;
+
+#define OFFSET(x) offsetof(PlayContext, x)
+#define FLAGS AV_OPT_FLAG_AUDIO_PARAM|AV_OPT_FLAG_FILTERING_PARAM
+
+static const AVOption basicplay_options[] = {
+    { "tune", "tune description", OFFSET(tune), AV_OPT_TYPE_STRING,
+        {.str = ""}, 0, 0, FLAGS },
+    { "sample_rate", "sample rate", OFFSET(sample_rate), AV_OPT_TYPE_INT,
+        {.i64 = 44100}, 1, INT_MAX, FLAGS },
+    { "r", "sample rate", OFFSET(sample_rate), AV_OPT_TYPE_INT,
+        {.i64 = 44100}, 1, INT_MAX, FLAGS },
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(basicplay);
+
+static void
+add_note(Note **notes, unsigned *nb_notes, uint64_t *pos,
+         unsigned note, unsigned octave, unsigned period, unsigned length, unsigned attack)
+{
+    unsigned whole_note, duration, sound;
+
+    octave += note / 12;
+    note %= 12;
+    whole_note = time_base / 600 * 96 * period;
+    duration = time_base / 600 * period * length;
+    sound = !attack ? duration : /* 325368125/119696244 =~ exp(1) */
+        av_rescale(whole_note, 119696244, (uint64_t)325368125 * attack);
+    sound = FFMIN(sound, duration);
+    if (sound > 0) {
+        if (*notes) {
+            (*notes)->duration = sound;
+            (*notes)->note = note;
+            (*notes)->octave = octave;
+            (*notes)++;
+        }
+        (*nb_notes)++;
+    }
+    if (sound < duration) {
+        (*nb_notes)++;
+        if (*notes) {
+            (*notes)->duration = duration - sound;
+            (*notes)->note = 0;
+            (*notes)->octave = 0;
+            (*notes)++;
+        }
+    }
+    (*pos) += duration;
+}
+
+#define LC(c) ((c) | ('a' - 'A'))
+
+static int parse_letter_number(const char **rt, const char *end, char letter,
+                               unsigned min, unsigned max, unsigned *rv)
+{
+    const char *t = *rt;
+    unsigned v = 0;
+
+    if (LC(*t) != letter)
+        return 0;
+    t++;
+    if (!av_isdigit(*t))
+        return 0;
+    while (av_isdigit(*t)) {
+        v = v * 10 + (*(t++) - '0');
+        if (v > max)
+            return 0;
+    }
+    if (v < min)
+        return 0;
+    *rt = t;
+    *rv = v;
+    return 1;
+}
+
+static int parse_tune(AVFilterContext *ctx, Note *notes)
+{
+    PlayContext *s = ctx->priv;
+    const char *t = s->tune;
+    const char *end = t + strlen(t);
+    unsigned nb_notes = 0;
+    unsigned octave = 4, period = 5, length = 24, attack = 0;
+    uint64_t pos = 0;
+    int note;
+
+    while (t < end) {
+        if (av_isspace(*t)) {
+            t++;
+            continue;
+
+        }
+        note = end - t < 2 ? -1 :
+            LC(t[0]) == 'd' && LC(t[1]) == 'o' ?  0 :
+            LC(t[0]) == 'r' && LC(t[1]) == 'e' ?  2 :
+            LC(t[0]) == 'm' && LC(t[1]) == 'i' ?  4 :
+            LC(t[0]) == 'f' && LC(t[1]) == 'a' ?  5 :
+            LC(t[0]) == 's' && LC(t[1]) == 'o' ?  7 :
+            LC(t[0]) == 'l' && LC(t[1]) == 'a' ?  9 :
+            LC(t[0]) == 's' && LC(t[1]) == 'i' ? 11 : -1;
+        if (note >= 0) {
+            t += 2;
+            if (t < end && *t == '#') {
+                t++;
+                note++;
+            }
+            if (t < end && *t == 'b') {
+                t++;
+                note = note ? note - 1 : 11;
+            }
+            add_note(&notes, &nb_notes, &pos,
+                     note, octave, period, length, attack);
+            continue;
+        }
+        if (LC(*t) == 'p') {
+            t++;
+            add_note(&notes, &nb_notes, &pos, 0, 0, period, length, 0);
+            continue;
+        }
+        if (parse_letter_number(&t, end, 'o', 1, 5, &octave))
+            continue;
+        if (parse_letter_number(&t, end, 'l', 1, 96, &length))
+            continue;
+        if (parse_letter_number(&t, end, 't', 1, 255, &period))
+            continue;
+        if (parse_letter_number(&t, end, 'a', 0, 255, &attack))
+            continue;
+        av_log(ctx, AV_LOG_ERROR, "Syntax error in tune, near \"%s\"\n", t);
+        return AVERROR(EINVAL);
+    }
+    return nb_notes;
+}
+
+static av_cold int init(AVFilterContext *ctx)
+{
+    PlayContext *s = ctx->priv;
+    int ret;
+
+    ff_make_sin_table(s->sin);
+    ret = parse_tune(ctx, NULL);
+    if (ret < 0)
+        return ret;
+    s->nb_notes = ret;
+    s->notes = av_malloc_array(sizeof(*s->notes), s->nb_notes);
+    if (!s->notes)
+        return AVERROR(ENOMEM);
+    ret = parse_tune(ctx, s->notes);
+    av_assert0(ret == s->nb_notes);
+    return 0;
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    PlayContext *s = ctx->priv;
+
+    av_freep(&s->notes);
+}
+
+static av_cold int query_formats(AVFilterContext *ctx)
+{
+    PlayContext *s = ctx->priv;
+    static const int64_t chlayouts[] = { AV_CH_LAYOUT_MONO, -1 };
+    int sample_rates[] = { s->sample_rate, -1 };
+    static const enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_S16,
+                                                       AV_SAMPLE_FMT_NONE };
+    AVFilterFormats *formats;
+    AVFilterChannelLayouts *layouts;
+    int ret;
+
+    formats = ff_make_format_list(sample_fmts);
+    if (!formats)
+        return AVERROR(ENOMEM);
+    ret = ff_set_common_formats (ctx, formats);
+    if (ret < 0)
+        return ret;
+
+    layouts = avfilter_make_format64_list(chlayouts);
+    if (!layouts)
+        return AVERROR(ENOMEM);
+    ret = ff_set_common_channel_layouts(ctx, layouts);
+    if (ret < 0)
+        return ret;
+
+    formats = ff_make_format_list(sample_rates);
+    if (!formats)
+        return AVERROR(ENOMEM);
+    ret = ff_set_common_samplerates(ctx, formats);
+    if (ret < 0)
+        return ret;
+
+    return 0;
+}
+
+static av_cold int config_props(AVFilterLink *outlink)
+{
+    outlink->time_base = av_make_q(1, outlink->sample_rate);
+    return 0;
+}
+
+static int activate(AVFilterContext *ctx)
+{
+    PlayContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+    AVFrame *frame;
+    Note *note;
+    unsigned nb_samples, i;
+    uint32_t dphi;
+    int16_t *samples;
+    const uint32_t freq[12] = { /* octave 27, from DO to SI, in Hz */
+        2194674310, 2325176436, 2463438621, 2609922305,
+        2765116361, 2929538736, 3103738174, 3288296050,
+        3483828309, 3690987520, 3910465059, 4142993412,
+    };
+
+    if (!ff_outlink_frame_wanted(outlink))
+        return FFERROR_NOT_READY;
+    if (s->cur_note == s->nb_notes) {
+        ff_outlink_set_status(outlink, AVERROR_EOF, s->pts);
+        return 0;
+    }
+    note = &s->notes[s->cur_note++];
+    nb_samples = av_rescale(note->duration, outlink->sample_rate, time_base);
+    frame = ff_get_audio_buffer(outlink, nb_samples);
+    if (!frame)
+        return AVERROR(ENOMEM);
+    frame->pts = s->pts;
+    samples = (int16_t *)frame->data[0];
+    if (note->octave) {
+        dphi = ((uint64_t)freq[note->note] << (note->octave + 5)) / outlink->sample_rate;
+    } else {
+        dphi = 0;
+        s->phi = 0;
+    }
+    for (i = 0; i < nb_samples; i++) {
+        samples[i] = s->sin[s->phi >> SINE_SHIFT_PHI];
+        s->phi += dphi;
+    }
+    s->pts += nb_samples;
+    return ff_filter_frame(outlink, frame);
+}
+
+static const AVFilterPad basicplay_outputs[] = {
+    {
+        .name          = "default",
+        .type          = AVMEDIA_TYPE_AUDIO,
+        .config_props  = config_props,
+    },
+    { NULL }
+};
+
+AVFilter ff_asrc_basicplay = {
+    .name          = "basicplay",
+    .description   = NULL_IF_CONFIG_SMALL("Generate a tune like PLAY in Microsoft BASIC."),
+    .query_formats = query_formats,
+    .init          = init,
+    .uninit        = uninit,
+    .activate      = activate,
+    .priv_size     = sizeof(PlayContext),
+    .inputs        = NULL,
+    .outputs       = basicplay_outputs,
+    .priv_class    = &basicplay_class,
+};
diff --git a/libavfilter/version.h b/libavfilter/version.h
index 980d9baca3..96b14d6794 100644
--- a/libavfilter/version.h
+++ b/libavfilter/version.h
@@ -30,7 +30,7 @@
 #include "libavutil/version.h"
 
 #define LIBAVFILTER_VERSION_MAJOR   7
-#define LIBAVFILTER_VERSION_MINOR  84
+#define LIBAVFILTER_VERSION_MINOR  85
 #define LIBAVFILTER_VERSION_MICRO 100
 
 
diff --git a/tests/fate/filter-audio.mak b/tests/fate/filter-audio.mak
index 79b1536df0..da754af9be 100644
--- a/tests/fate/filter-audio.mak
+++ b/tests/fate/filter-audio.mak
@@ -99,6 +99,9 @@ fate-filter-asetrate: tests/data/asynth-44100-2.wav
 fate-filter-asetrate: SRC = $(TARGET_PATH)/tests/data/asynth-44100-2.wav
 fate-filter-asetrate: CMD = framecrc -i $(SRC) -frames:a 20 -af asetrate=20000
 
+FATE_AFILTER-$(call ALLYES, BASICPLAY_FILTER) += fate-filter-basicplay
+fate-filter-basicplay: CMD = framecrc -lavfi basicplay="DOREbSIPO1DOREbSIO5DOREbSIO4A2DOREbSIT2DOREbSIL48DOREbSI"
+
 FATE_AFILTER-$(call FILTERDEMDECENCMUX, CHORUS, WAV, PCM_S16LE, PCM_S16LE, WAV) += fate-filter-chorus
 fate-filter-chorus: tests/data/asynth-22050-1.wav
 fate-filter-chorus: SRC = $(TARGET_PATH)/tests/data/asynth-22050-1.wav
diff --git a/tests/ref/fate/filter-basicplay b/tests/ref/fate/filter-basicplay
new file mode 100644
index 0000000000..d00323ff1c
--- /dev/null
+++ b/tests/ref/fate/filter-basicplay
@@ -0,0 +1,34 @@
+#tb 0: 1/44100
+#media_type 0: audio
+#codec_id 0: pcm_s16le
+#sample_rate 0: 44100
+#channel_layout 0: 4
+#channel_layout_name 0: mono
+0,          0,          0,     8820,    17640, 0x42b3494a
+0,       8820,       8820,     8820,    17640, 0x93a75587
+0,      17640,      17640,     8820,    17640, 0xef3b4efa
+0,      26460,      26460,     8820,    17640, 0x00000000
+0,      35280,      35280,     8820,    17640, 0xc0064c32
+0,      44100,      44100,     8820,    17640, 0x8c7e4cb4
+0,      52920,      52920,     8820,    17640, 0x35d2a2ff
+0,      61740,      61740,     8820,    17640, 0x9b354e12
+0,      70560,      70560,     8820,    17640, 0x35125b2d
+0,      79380,      79380,     8820,    17640, 0xc4a352b4
+0,      88200,      88200,     6489,    12978, 0x7415347d
+0,      94689,      94689,     2331,     4662, 0x00000000
+0,      97020,      97020,     6489,    12978, 0x74494189
+0,     103509,     103509,     2331,     4662, 0x00000000
+0,     105840,     105840,     6489,    12978, 0xb8b13337
+0,     112329,     112329,     2331,     4662, 0x00000000
+0,     114660,     114660,     2596,     5192, 0xc39a0594
+0,     117256,     117256,      932,     1864, 0x00000000
+0,     118188,     118188,     2596,     5192, 0x19851628
+0,     120784,     120784,      932,     1864, 0x00000000
+0,     121716,     121716,     2596,     5192, 0x54fa0d97
+0,     124312,     124312,      932,     1864, 0x00000000
+0,     125244,     125244,     2596,     5192, 0xc39a0594
+0,     127840,     127840,     4460,     8920, 0x00000000
+0,     132300,     132300,     2596,     5192, 0x19851628
+0,     134896,     134896,     4460,     8920, 0x00000000
+0,     139356,     139356,     2596,     5192, 0x54fa0d97
+0,     141952,     141952,     4460,     8920, 0x00000000
