Problem: No native sound support on Mac OS.
Solution: Add sound support for Mac OS. (Yee Cheng Chin, closes #11274)
< On MS-Windows, {name} can be SystemAsterisk, SystemDefault,
SystemExclamation, SystemExit, SystemHand, SystemQuestion,
SystemStart, SystemWelcome, etc.
+ On macOS, {name} refers to files located in
+ /System/Library/Sounds (e.g. "Tink"). It will also work for
+ custom installed sounds in folders like ~/Library/Sounds.
When {callback} is specified it is invoked when the sound is
finished. The first argument is the sound ID, the second
AC_MSG_CHECKING([whether we need macOS frameworks])
if test "$MACOS_X_DARWIN" = "yes"; then
if test "$features" = "tiny"; then
- dnl Since no FEAT_CLIPBOARD, no longer need for os_macosx.m.
+ dnl Since no FEAT_CLIPBOARD or FEAT_SOUND, no need for os_macosx.m.
OS_EXTRA_SRC=`echo "$OS_EXTRA_SRC" | sed -e 's+os_macosx.m++'`
OS_EXTRA_OBJ=`echo "$OS_EXTRA_OBJ" | sed -e 's+objects/os_macosx.o++'`
AC_MSG_RESULT([yes, we need CoreServices])
#endif
/*
- * sound - currently only with libcanberra
+ * sound
*/
#if !defined(FEAT_SOUND) && defined(HAVE_CANBERRA)
# define FEAT_SOUND
# ifdef FEAT_TERMINAL
free_unused_terminals();
# endif
+
+# ifdef FEAT_SOUND_MACOSX
+ process_cfrunloop();
+# endif
# ifdef FEAT_SOUND_CANBERRA
if (has_sound_callback_in_queue())
invoke_sound_callback();
#endif /* FEAT_RELTIME */
+#ifdef FEAT_SOUND
+
+static NSMutableDictionary<NSNumber*, NSSound*> *sounds_list = nil;
+
+/// A delegate for handling when a sound has stopped playing, in
+/// order to clean up the sound and to send a callback.
+@interface SoundDelegate : NSObject<NSSoundDelegate>;
+
+- (id) init:(long) sound_id callback:(soundcb_T*) callback;
+- (void) sound:(NSSound *)sound didFinishPlaying:(BOOL)flag;
+
+@property (readonly) long sound_id;
+@property (readonly) soundcb_T *callback;
+
+@end
+
+@implementation SoundDelegate
+- (id) init:(long) sound_id callback:(soundcb_T*) callback
+{
+ if ([super init])
+ {
+ _sound_id = sound_id;
+ _callback = callback;
+ }
+ return self;
+}
+
+- (void) sound:(NSSound *)sound didFinishPlaying:(BOOL)flag
+{
+ if (sounds_list != nil)
+ {
+ if (_callback)
+ {
+ call_sound_callback(_callback, _sound_id, flag ? 0 : 1);
+ delete_sound_callback(_callback);
+ _callback = NULL;
+ }
+ [sounds_list removeObjectForKey:[NSNumber numberWithLong:_sound_id]];
+ }
+ // Release itself. Do that here instead of earlier because NSSound only
+ // holds weak reference to this object.
+ [self release];
+}
+@end
+
+ void
+process_cfrunloop()
+{
+ if (sounds_list != nil && [sounds_list count] > 0)
+ {
+ // Continually drain the run loop of events. Currently, this
+ // is only used for processing sound callbacks, because
+ // NSSound relies of this runloop to call back to the
+ // delegate.
+ @autoreleasepool
+ {
+ while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true)
+ == kCFRunLoopRunHandledSource)
+ ; // do nothing
+ }
+ }
+}
+
+ bool
+sound_mch_play(const char_u* sound_name, long sound_id, soundcb_T *callback, bool playfile)
+{
+ @autoreleasepool
+ {
+ NSString *sound_name_ns = [[[NSString alloc] initWithUTF8String:(const char*)sound_name] autorelease];
+ NSSound* sound = playfile ?
+ [[[NSSound alloc] initWithContentsOfFile:sound_name_ns byReference:YES] autorelease] :
+ [NSSound soundNamed:sound_name_ns];
+ if (!sound)
+ {
+ return false;
+ }
+
+ if (sounds_list == nil)
+ {
+ sounds_list = [[NSMutableDictionary<NSNumber*, NSSound*> alloc] init];
+ }
+ sounds_list[[NSNumber numberWithLong:sound_id]] = sound;
+
+ // Make a delegate to handle when the sound stops. No need to call
+ // autorelease because NSSound only holds a weak reference to it.
+ SoundDelegate *delegate = [[SoundDelegate alloc] init:sound_id callback:callback];
+
+ [sound setDelegate:delegate];
+ [sound play];
+ }
+ return true;
+}
+
+ void
+sound_mch_stop(long sound_id)
+{
+ @autoreleasepool
+ {
+ NSSound *sound = sounds_list[[NSNumber numberWithLong:sound_id]];
+ if (sound != nil)
+ {
+ // Stop the sound. No need to release it because the delegate will do
+ // it for us.
+ [sound stop];
+ }
+ }
+}
+
+ void
+sound_mch_clear()
+{
+ if (sounds_list != nil)
+ {
+ @autoreleasepool
+ {
+ for (NSSound *sound in [sounds_list allValues])
+ {
+ [sound stop];
+ }
+ [sounds_list release];
+ sounds_list = nil;
+ }
+ }
+}
+
+ void
+sound_mch_free()
+{
+ sound_mch_clear();
+}
+
+#endif // FEAT_SOUND
+
/* Lift the compiler warning suppression. */
#if defined(__clang__) && defined(__STRICT_ANSI__)
# pragma clang diagnostic pop
rest -= msec;
}
# endif
+# ifdef FEAT_SOUND_MACOSX
+ // Invoke any pending sound callbacks.
+ process_cfrunloop();
+# endif
# ifdef FEAT_SOUND_CANBERRA
// Invoke any pending sound callbacks.
if (has_sound_callback_in_queue())
# ifdef MACOS_CONVERT
# include "os_mac_conv.pro"
# endif
+# ifdef MACOS_X
+# include "os_macosx.pro"
+# endif
# if defined(MACOS_X_DARWIN) && defined(FEAT_CLIPBOARD) && !defined(FEAT_GUI)
// functions in os_macosx.m
void clip_mch_lose_selection(Clipboard_T *cbd);
--- /dev/null
+/* os_macosx.m */
+void process_cfrunloop();
+bool sound_mch_play(const char_u* event, long sound_id, soundcb_T *callback, bool playfile);
+void sound_mch_stop(long sound_id);
+void sound_mch_clear();
+void sound_mch_free();
+/* vim: set ft=c : */
/* sound.c */
+typedef struct soundcb_S soundcb_T;
+
int has_any_sound_callback(void);
int has_sound_callback_in_queue(void);
+void call_sound_callback(soundcb_T *soundcb, long sound_id, int result);
+void delete_sound_callback(soundcb_T *soundcb);
void invoke_sound_callback(void);
void f_sound_playevent(typval_T *argvars, typval_T *rettv);
void f_sound_playfile(typval_T *argvars, typval_T *rettv);
return soundcb;
}
+/*
+ * Call "soundcb" with proper parameters.
+ */
+ void
+call_sound_callback(soundcb_T *soundcb, long snd_id, int result)
+{
+ typval_T argv[3];
+ typval_T rettv;
+
+ argv[0].v_type = VAR_NUMBER;
+ argv[0].vval.v_number = snd_id;
+ argv[1].v_type = VAR_NUMBER;
+ argv[1].vval.v_number = result;
+ argv[2].v_type = VAR_UNKNOWN;
+
+ call_callback(&soundcb->snd_callback, -1, &rettv, 2, argv);
+ clear_tv(&rettv);
+}
+
/*
* Delete "soundcb" from the list of pending callbacks.
*/
- static void
+ void
delete_sound_callback(soundcb_T *soundcb)
{
soundcb_T *p;
#if defined(HAVE_CANBERRA) || defined(PROTO)
/*
- * Sound implementation for Linux/Unix/Mac using libcanberra.
+ * Sound implementation for Linux/Unix using libcanberra.
*/
# include <canberra.h>
invoke_sound_callback(void)
{
soundcb_queue_T *scb;
- typval_T argv[3];
- typval_T rettv;
-
while (callback_queue != NULL)
{
scb = callback_queue;
callback_queue = scb->scb_next;
- argv[0].v_type = VAR_NUMBER;
- argv[0].vval.v_number = scb->scb_id;
- argv[1].v_type = VAR_NUMBER;
- argv[1].vval.v_number = scb->scb_result;
- argv[2].v_type = VAR_UNKNOWN;
-
- call_callback(&scb->scb_callback->snd_callback, -1, &rettv, 2, argv);
- clear_tv(&rettv);
+ call_sound_callback(scb->scb_callback, scb->scb_id, scb->scb_result);
delete_sound_callback(scb->scb_callback);
vim_free(scb);
for (p = first_callback; p != NULL; p = p->snd_next)
if (p->snd_device_id == (MCIDEVICEID) lParam)
{
- typval_T argv[3];
- typval_T rettv;
char buf[32];
vim_snprintf(buf, sizeof(buf), "close sound%06ld",
p->snd_id);
mciSendString(buf, NULL, 0, 0);
- argv[0].v_type = VAR_NUMBER;
- argv[0].vval.v_number = p->snd_id;
- argv[1].v_type = VAR_NUMBER;
- argv[1].vval.v_number =
- wParam == MCI_NOTIFY_SUCCESSFUL ? 0
- : wParam == MCI_NOTIFY_ABORTED ? 1 : 2;
- argv[2].v_type = VAR_UNKNOWN;
-
- call_callback(&p->snd_callback, -1, &rettv, 2, argv);
- clear_tv(&rettv);
+ long result = wParam == MCI_NOTIFY_SUCCESSFUL ? 0
+ : wParam == MCI_NOTIFY_ABORTED ? 1 : 2;
+ call_sound_callback(p, p->snd_id, result);
delete_sound_callback(p);
redraw_after_callback(TRUE, FALSE);
}
# endif
-#endif // MSWIN
+#elif defined(MACOS_X_DARWIN)
+
+// Sound implementation for macOS.
+ static void
+sound_play_common(typval_T *argvars, typval_T *rettv, bool playfile)
+{
+ if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
+ return;
+
+ char_u *sound_name = tv_get_string(&argvars[0]);
+ soundcb_T *soundcb = get_sound_callback(&argvars[1]);
+
+ ++sound_id;
+
+ bool play_success = sound_mch_play(sound_name, sound_id, soundcb, playfile);
+ if (!play_success && soundcb)
+ {
+ delete_sound_callback(soundcb);
+ }
+ rettv->vval.v_number = play_success ? sound_id : 0;
+}
+
+ void
+f_sound_playevent(typval_T *argvars, typval_T *rettv)
+{
+ sound_play_common(argvars, rettv, false);
+}
+
+ void
+f_sound_playfile(typval_T *argvars, typval_T *rettv)
+{
+ sound_play_common(argvars, rettv, true);
+}
+
+ void
+f_sound_stop(typval_T *argvars, typval_T *rettv UNUSED)
+{
+ if (in_vim9script() && check_for_number_arg(argvars, 0) == FAIL)
+ return;
+ sound_mch_stop(tv_get_number(&argvars[0]));
+}
+
+ void
+f_sound_clear(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
+{
+ sound_mch_clear();
+}
+
+#if defined(EXITFREE) || defined(PROTO)
+ void
+sound_free(void)
+{
+ sound_mch_free();
+ while (first_callback != NULL)
+ delete_sound_callback(first_callback);
+}
+#endif
+
+#endif // MACOS_X_DARWIN
#endif // FEAT_SOUND
endif
let g:playcallback_count = 0
let g:id = 0
- let id = 'bell'->sound_playevent('PlayCallback')
+ let event_name = 'bell'
+ if has('osx')
+ let event_name = 'Tink'
+ endif
+ let id = event_name->sound_playevent('PlayCallback')
if id == 0
throw 'Skipped: bell event not available'
endif
}
if (due_time <= 0 || (wtime > 0 && due_time > remaining))
due_time = remaining;
-# if defined(FEAT_JOB_CHANNEL) || defined(FEAT_SOUND_CANBERRA)
+# if defined(FEAT_JOB_CHANNEL) || defined(FEAT_SOUND_CANBERRA) || defined(FEAT_SOUND_MACOSX)
if ((due_time < 0 || due_time > 10L) && (
# if defined(FEAT_JOB_CHANNEL)
(
!gui.in_use &&
# endif
(has_pending_job() || channel_any_readahead()))
-# ifdef FEAT_SOUND_CANBERRA
+# if defined(FEAT_SOUND_CANBERRA) || defined(FEAT_SOUND_MACOSX)
||
# endif
# endif
-# ifdef FEAT_SOUND_CANBERRA
+# if defined(FEAT_SOUND_CANBERRA) || defined(FEAT_SOUND_MACOSX)
has_any_sound_callback()
# endif
))
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 694,
/**/
693,
/**/
*/
#include "feature.h"
-#if defined(MACOS_X_DARWIN) && defined(FEAT_NORMAL) \
- && !defined(FEAT_CLIPBOARD)
-# define FEAT_CLIPBOARD
+#if defined(MACOS_X_DARWIN)
+# if defined(FEAT_NORMAL) && !defined(FEAT_CLIPBOARD)
+# define FEAT_CLIPBOARD
+# endif
+# if defined(FEAT_BIG) && !defined(FEAT_SOUND)
+# define FEAT_SOUND
+# endif
+# if defined(FEAT_SOUND)
+# define FEAT_SOUND_MACOSX
+# endif
#endif
// +x11 is only enabled when it's both available and wanted.