# HG changeset patch # User Bram Moolenaar # Date 1665234005 -7200 # Node ID 71137f73c94dafbfe9cafa6e8aaafa8409f2fd40 # Parent 118268e202510a703d0dee7a7e6dfe8f9d7339bb patch 9.0.0694: no native sound support on Mac OS Commit: https://github.com/vim/vim/commit/4314e4f7da4db5d85f63cdf43b73be3689502c93 Author: Yee Cheng Chin 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) diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt --- 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 diff --git a/src/configure.ac b/src/configure.ac --- 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]) diff --git a/src/feature.h b/src/feature.h --- 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 diff --git a/src/getchar.c b/src/getchar.c --- 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(); diff --git a/src/os_macosx.m b/src/os_macosx.m --- 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 *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; + +- (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 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 diff --git a/src/os_unix.c b/src/os_unix.c --- 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()) diff --git a/src/proto.h b/src/proto.h --- 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); diff --git a/src/proto/os_macosx.pro b/src/proto/os_macosx.pro 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 : */ diff --git a/src/proto/sound.pro b/src/proto/sound.pro --- 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); diff --git a/src/sound.c b/src/sound.c --- 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 @@ -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 diff --git a/src/testdir/test_sound.vim b/src/testdir/test_sound.vim --- 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 diff --git a/src/ui.c b/src/ui.c --- 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 )) diff --git a/src/version.c b/src/version.c --- 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, diff --git a/src/vim.h b/src/vim.h --- 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.