diff --git a/apps/app_playback.c b/apps/app_playback.c
index fc5dcf20a906478253fbd9d97e82abcfd80ae65c..2816d53df07d863ea27031d85e454698dff01ce3 100644
--- a/apps/app_playback.c
+++ b/apps/app_playback.c
@@ -44,6 +44,10 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 #include "asterisk/options.h"
 #include "asterisk/app.h"
 
+#include "asterisk/cli.h"
+#include "asterisk/localtime.h"
+#include "asterisk/say.h"
+
 static char *tdesc = "Sound File Playback Application";
 
 static char *app = "Playback";
@@ -68,12 +72,322 @@ static char *descrip =
 
 LOCAL_USER_DECL;
 
+static struct ast_config *say_cfg;
+/* save the say' api calls.
+ * The first entry is NULL if we have the standard source,
+ * otherwise we are sourcing from here.
+ * 'say load [new|old]' will enable the new or old method, or report status
+ */
+static const void * say_api_buf[40];
+static const char *say_old = "old";
+static const char *say_new = "new";
+
+static void save_say_mode(const void *arg)
+{
+	int i = 0;
+	say_api_buf[i++] = arg;
+
+        say_api_buf[i++] = ast_say_number_full;
+        say_api_buf[i++] = ast_say_enumeration_full;
+        say_api_buf[i++] = ast_say_digit_str_full;
+        say_api_buf[i++] = ast_say_character_str_full;
+        say_api_buf[i++] = ast_say_phonetic_str_full;
+        say_api_buf[i++] = ast_say_datetime;
+        say_api_buf[i++] = ast_say_time;
+        say_api_buf[i++] = ast_say_date;
+        say_api_buf[i++] = ast_say_datetime_from_now;
+        say_api_buf[i++] = ast_say_date_with_format;
+}
+
+static void restore_say_mode(void *arg)
+{
+	int i = 0;
+	say_api_buf[i++] = arg;
+
+        ast_say_number_full = say_api_buf[i++];
+        ast_say_enumeration_full = say_api_buf[i++];
+        ast_say_digit_str_full = say_api_buf[i++];
+        ast_say_character_str_full = say_api_buf[i++];
+        ast_say_phonetic_str_full = say_api_buf[i++];
+        ast_say_datetime = say_api_buf[i++];
+        ast_say_time = say_api_buf[i++];
+        ast_say_date = say_api_buf[i++];
+        ast_say_datetime_from_now = say_api_buf[i++];
+        ast_say_date_with_format = say_api_buf[i++];
+}
+
+/* 
+ * Typical 'say' arguments in addition to the date or number or string
+ * to say. We do not include 'options' because they may be different
+ * in recursive calls, and so they are better left as an external
+ * parameter.
+ */
+typedef struct {
+        struct ast_channel *chan;
+        const char *ints;
+        const char *language;
+        int audiofd;
+        int ctrlfd;
+} say_args_t;
+
+static int s_streamwait3(const say_args_t *a, const char *fn)
+{
+        int res = ast_streamfile(a->chan, fn, a->language);
+        if (res) {
+                ast_log(LOG_WARNING, "Unable to play message %s\n", fn);
+                return res;
+        }
+        res = (a->audiofd  > -1 && a->ctrlfd > -1) ?
+                ast_waitstream_full(a->chan, a->ints, a->audiofd, a->ctrlfd) :
+                ast_waitstream(a->chan, a->ints);
+        ast_stopstream(a->chan);
+        return res;  
+}
+
+/*
+ * the string is 'prefix:data' or prefix:fmt:data'
+ * with ':' being invalid in strings.
+ */
+static int do_say(say_args_t *a, const char *s, const char *options, int depth)
+{
+	struct ast_variable *v;
+	char *lang, *x, *rule = NULL;
+	int ret = 0;   
+	struct varshead head = { .first = NULL, .last = NULL };
+	struct ast_var_t *n;
+
+	ast_log(LOG_WARNING, "string <%s> depth <%d>\n", s, depth);
+	if (depth++ > 10) {
+		ast_log(LOG_WARNING, "recursion too deep, exiting\n");
+		return -1;
+	}
+
+	/* scan languages same as in file.c */
+	if (a->language == NULL)
+		a->language = "en";     /* default */
+	ast_log(LOG_WARNING, "try <%s> in <%s>\n", s, a->language);
+	lang = ast_strdupa(a->language);
+	if (!lang)      /* no memory! */
+		return -1;
+	for (;;) {
+		for (v = ast_variable_browse(say_cfg, lang); v ; v = v->next) {
+			if (ast_extension_match(v->name, s)) {
+				rule = ast_strdupa(v->value);
+				break;
+			}
+		}
+		if (rule)
+			break;
+		if ( (x = strchr(lang, '_')) )
+			*x = '\0';      /* try without suffix */
+		else if (strcmp(lang, "en"))
+			lang = "en";    /* last resort, try 'en' if not done yet */
+		else
+			break;
+	}
+	if (!rule)
+		return 0;
+
+	/* skip up to two prefixes to get the value */
+	if ( (x = strchr(s, ':')) )
+		s = x + 1;
+	if ( (x = strchr(s, ':')) )
+		s = x + 1;
+	ast_log(LOG_WARNING, "value is <%s>\n", s);
+	n = ast_var_assign("SAY", s);
+	AST_LIST_INSERT_HEAD(&head, n, entries);
+
+	/* scan the body, one piece at a time */
+	while ( ret <= 0 && (x = strsep(&rule, ",")) ) { /* exit on key */
+		char fn[128];
+		const char *p, *fmt, *data; /* format and data pointers */
+
+		/* prepare a decent file name */
+		x = ast_skip_blanks(x);
+		ast_trim_blanks(x);
+
+		/* replace variables */
+		memset(fn, 0, sizeof(fn)); /* XXX why isn't done in pbx_substitute_variables_helper! */
+		pbx_substitute_variables_varshead(&head, x, fn, sizeof(fn));
+		ast_log(LOG_WARNING, "doing [%s]\n", fn);
+
+		/* locate prefix and data, if any */
+		fmt = index(fn, ':');
+		if (!fmt || fmt == fn)	{	/* regular filename */
+			ret = s_streamwait3(a, fn);
+			continue;
+		}
+		fmt++;
+		data = index(fmt, ':');	/* colon before data */
+		if (!data || data == fmt) {	/* simple prefix-fmt */
+			ret = do_say(a, fn, options, depth);
+			continue;
+		}
+		/* prefix:fmt:data */
+		for (p = fmt; p < data && ret <= 0; p++) {
+			char fn2[128];
+			if (*p == ' ' || *p == '\t')	/* skip blanks */
+				continue;
+			if (*p == '\'') {/* file name - we trim them */
+				char *y;
+				strcpy(fn2, ast_skip_blanks(p+1));	/* make a full copy */
+				y = index(fn2, '\'');
+				if (!y) {
+					p = data;	/* invalid. prepare to end */
+					break;
+				}
+				*y = '\0';
+				ast_trim_blanks(fn2);
+				p = index(p+1, '\'');
+				ret = s_streamwait3(a, fn2);
+			} else {
+				int l = fmt-fn;
+				strcpy(fn2, fn); /* copy everything */
+				/* after prefix, append the format */
+				fn2[l++] = *p;
+				strcpy(fn2 + l, data);
+				ret = do_say(a, fn2, options, depth);
+			}
+		}
+	}
+	ast_var_delete(n);
+	return ret;
+}
+
+static int say_full(struct ast_channel *chan, const char *string,
+        const char *ints, const char *lang, const char *options,
+        int audiofd, int ctrlfd)
+{
+        say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
+  
+        if (!say_cfg) {
+                ast_log(LOG_WARNING, "no say.conf, cannot spell '%s'\n", string);
+                return -1;   
+        }
+        return do_say(&a, string, options, 0);
+}
+
+static int say_number_full(struct ast_channel *chan, int num,
+	const char *ints, const char *lang, const char *options,
+	int audiofd, int ctrlfd)
+{
+	char buf[64];
+        say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
+	snprintf(buf, sizeof(buf), "num:%d", num);
+        return do_say(&a, buf, options, 0);
+}
+
+static int say_enumeration_full(struct ast_channel *chan, int num,
+	const char *ints, const char *lang, const char *options,
+	int audiofd, int ctrlfd)
+{
+	char buf[64];
+        say_args_t a = { chan, ints, lang, audiofd, ctrlfd };
+	snprintf(buf, sizeof(buf), "enum:%d", num);
+        return do_say(&a, buf, options, 0);
+}
+
+static int say_date_generic(struct ast_channel *chan, time_t t,
+	const char *ints, const char *lang, const char *format, const char *timezone, const char *prefix)
+{
+	char buf[128];
+	struct tm tm;
+        say_args_t a = { chan, ints, lang, -1, -1 };
+	if (format == NULL)
+		format = "";
+
+	ast_localtime(&t, &tm, NULL);
+	snprintf(buf, sizeof(buf), "%s:%s:%04d%02d%02d%02d%02d.%02d-%d-%3d",
+		prefix,
+		format,
+		tm.tm_year+1900,
+		tm.tm_mon+1,
+		tm.tm_mday,
+		tm.tm_hour,
+		tm.tm_min,
+		tm.tm_sec,
+		tm.tm_wday,
+		tm.tm_yday);
+        return do_say(&a, buf, NULL, 0);
+}
+
+static int say_date_with_format(struct ast_channel *chan, time_t t,
+	const char *ints, const char *lang, const char *format, const char *timezone)
+{
+	return say_date_generic(chan, t, ints, lang, format, timezone, "datetime");
+}
+
+static int say_date(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
+{
+	return say_date_generic(chan, t, ints, lang, "", NULL, "date");
+}
+
+static int say_time(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
+{
+	return say_date_generic(chan, t, ints, lang, "", NULL, "time");
+}
+
+static int say_datetime(struct ast_channel *chan, time_t t, const char *ints, const char *lang)
+{
+	return say_date_generic(chan, t, ints, lang, "", NULL, "datetime");
+}
+
+/*
+ * remap the 'say' functions to use those in this file
+ */
+static int __say_init(int fd, int argc, char *argv[])
+{
+	const char *old_mode = say_api_buf[0] ? say_new : say_old;
+	char *mode;
+
+	if (argc == 2) {
+		ast_cli(fd, "say mode is [%s]\n", old_mode);
+		return RESULT_SUCCESS;
+        } else if (argc != 3)
+                return RESULT_SHOWUSAGE;
+        mode = argv[2];
+
+	ast_log(LOG_WARNING, "init say.c from %s to %s", old_mode, mode);
+
+	if (!strcmp(mode, old_mode)) {
+		ast_log(LOG_WARNING, "say mode is %s already\n", mode);
+	} else if (!strcmp(mode, say_new)) {
+		if (say_cfg == NULL)
+			say_cfg = ast_config_load("say.conf");
+		save_say_mode(say_new);
+		ast_say_number_full = say_number_full;
+
+		ast_say_enumeration_full = say_enumeration_full;
+#if 0
+		ast_say_digits_full = say_digits_full;
+		ast_say_digit_str_full = say_digit_str_full;
+		ast_say_character_str_full = say_character_str_full;
+		ast_say_phonetic_str_full = say_phonetic_str_full;
+		ast_say_datetime_from_now = say_datetime_from_now;
+#endif
+		ast_say_datetime = say_datetime;
+		ast_say_time = say_time;
+		ast_say_date = say_date;
+		ast_say_date_with_format = say_date_with_format;
+	} else if (!strcmp(mode, say_old) && say_api_buf[0] == say_new) {
+		restore_say_mode(NULL);
+	} else {
+		ast_log(LOG_WARNING, "unrecognized mode %s\n", mode);
+	}
+	return RESULT_SUCCESS;
+}
+
+static struct ast_cli_entry myclis[] = {
+        { { "say", "load", NULL }, __say_init, "set/show the say mode", "say load new|old" },
+};
+
 static int playback_exec(struct ast_channel *chan, void *data)
 {
 	int res = 0;
 	struct localuser *u;
 	char *tmp;
 	int option_skip=0;
+	int option_say=0;
 	int option_noanswer = 0;
 	int priority_jump = 0;
 
@@ -96,6 +410,8 @@ static int playback_exec(struct ast_channel *chan, void *data)
 	if (args.options) {
 		if (strcasestr(args.options, "skip"))
 			option_skip = 1;
+		if (strcasestr(args.options, "say"))
+			option_say = 1;
 		if (strcasestr(args.options, "noanswer"))
 			option_noanswer = 1;
 		if (strchr(args.options, 'j'))
@@ -105,8 +421,7 @@ static int playback_exec(struct ast_channel *chan, void *data)
 	if (chan->_state != AST_STATE_UP) {
 		if (option_skip) {
 			/* At the user's option, skip if the line is not up */
-			LOCAL_USER_REMOVE(u);
-			return 0;
+			goto done;
 		} else if (!option_noanswer)
 			/* Otherwise answer unless we're supposed to send this while on-hook */
 			res = ast_answer(chan);
@@ -117,7 +432,10 @@ static int playback_exec(struct ast_channel *chan, void *data)
 
 		ast_stopstream(chan);
 		while (!res && (front = strsep(&tmp, "&"))) {
-			res = ast_streamfile(chan, front, chan->language);
+			if (option_say)
+				res = say_full(chan, front, "", chan->language, NULL, -1, -1);
+			else
+				res = ast_streamfile(chan, front, chan->language);
 			if (!res) { 
 				res = ast_waitstream(chan, "");	
 				ast_stopstream(chan);
@@ -131,6 +449,7 @@ static int playback_exec(struct ast_channel *chan, void *data)
 		}
 		pbx_builtin_setvar_helper(chan, "PLAYBACKSTATUS", mres ? "FAILED" : "SUCCESS");
 	}
+done:
 	LOCAL_USER_REMOVE(u);
 	return res;
 }
@@ -148,6 +467,7 @@ static int unload_module(void *mod)
 
 static int load_module(void *mod)
 {
+        ast_cli_register_multiple(myclis, sizeof(myclis)/sizeof(struct ast_cli_entry));
 	return ast_register_application(app, playback_exec, synopsis, descrip);
 }