changeset 30719:71137f73c94d v9.0.0694

patch 9.0.0694: no native sound support on Mac OS Commit: https://github.com/vim/vim/commit/4314e4f7da4db5d85f63cdf43b73be3689502c93 Author: Yee Cheng Chin <ychin.git@gmail.com> Date: Sat Oct 8 13:50:05 2022 +0100 patch 9.0.0694: no native sound support on Mac OS Problem: No native sound support on Mac OS. Solution: Add sound support for Mac OS. (Yee Cheng Chin, closes https://github.com/vim/vim/issues/11274)
author Bram Moolenaar <Bram@vim.org>
date Sat, 08 Oct 2022 15:00:05 +0200
parents 118268e20251
children 763b6d18b527
files runtime/doc/builtin.txt src/configure.ac src/feature.h src/getchar.c src/os_macosx.m src/os_unix.c src/proto.h src/proto/os_macosx.pro src/proto/sound.pro src/sound.c src/testdir/test_sound.vim src/ui.c src/version.c src/vim.h
diffstat 14 files changed, 264 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -8631,6 +8631,9 @@ sound_playevent({name} [, {callback}])
 <		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
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -4553,7 +4553,7 @@ if test "$MACOS_X" = "yes"; then
   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])
--- a/src/feature.h
+++ b/src/feature.h
@@ -484,7 +484,7 @@
 #endif
 
 /*
- * sound - currently only with libcanberra
+ * sound
  */
 #if !defined(FEAT_SOUND) && defined(HAVE_CANBERRA)
 # define FEAT_SOUND
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -2326,6 +2326,10 @@ parse_queued_messages(void)
 # 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();
--- a/src/os_macosx.m
+++ b/src/os_macosx.m
@@ -384,6 +384,139 @@ timer_delete(timer_t timerid)
 
 #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
--- a/src/os_unix.c
+++ b/src/os_unix.c
@@ -6125,6 +6125,10 @@ WaitForCharOrMouse(long msec, int *inter
 		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())
--- a/src/proto.h
+++ b/src/proto.h
@@ -327,6 +327,9 @@ extern char_u *vimpty_getenv(const char_
 # 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);
new file mode 100644
--- /dev/null
+++ b/src/proto/os_macosx.pro
@@ -0,0 +1,7 @@
+/* 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 : */
--- a/src/proto/sound.pro
+++ b/src/proto/sound.pro
@@ -1,6 +1,10 @@
 /* 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);
--- a/src/sound.c
+++ b/src/sound.c
@@ -65,9 +65,28 @@ get_sound_callback(typval_T *arg)
 }
 
 /*
+ * 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;
@@ -89,7 +108,7 @@ delete_sound_callback(soundcb_T *soundcb
 #if defined(HAVE_CANBERRA) || defined(PROTO)
 
 /*
- * Sound implementation for Linux/Unix/Mac using libcanberra.
+ * Sound implementation for Linux/Unix using libcanberra.
  */
 # include <canberra.h>
 
@@ -152,23 +171,13 @@ has_sound_callback_in_queue(void)
 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);
@@ -307,24 +316,15 @@ sound_wndproc(HWND hwnd, UINT message, W
 	    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);
@@ -459,6 +459,64 @@ sound_free(void)
 }
 # 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
--- a/src/testdir/test_sound.vim
+++ b/src/testdir/test_sound.vim
@@ -17,7 +17,11 @@ func Test_play_event()
   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
--- a/src/ui.c
+++ b/src/ui.c
@@ -460,7 +460,7 @@ ui_wait_for_chars_or_timer(
 	}
 	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)
 		(
@@ -468,11 +468,11 @@ ui_wait_for_chars_or_timer(
 		!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
 		    ))
--- a/src/version.c
+++ b/src/version.c
@@ -700,6 +700,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    694,
+/**/
     693,
 /**/
     692,
--- a/src/vim.h
+++ b/src/vim.h
@@ -163,9 +163,16 @@
  */
 #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.