hammerspoon/extensions/noises/libnoises.m

291 lines
8.4 KiB
Objective-C

#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
#import <Foundation/Foundation.h>
#import <LuaSkin/LuaSkin.h>
#import <AudioToolbox/AudioQueue.h>
#import <AudioToolbox/AudioFile.h>
#include "detectors.h"
// This warning doesn't make sense for this application, where a system API needs direct pointers
// into a struct, and the hacks and heap allocation to use properties would be a performance hit
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#define NUM_BUFFERS 1
static const int kSampleRate = 44100;
#define USERDATA_TAG "hs.noises"
static LSRefTable refTable;
#define get_listener_arg(L, idx) (__bridge Listener*)*((void**)luaL_checkudata(L, idx, USERDATA_TAG))
typedef struct
{
AudioStreamBasicDescription dataFormat;
AudioQueueRef queue;
AudioQueueBufferRef buffers[NUM_BUFFERS];
AudioFileID audioFile;
UInt64 currentFrame;
bool recording;
}RecordState;
@interface Listener : NSObject
- (Listener*)initPlugins;
- (void)setupAudioFormat:(AudioStreamBasicDescription*)format;
- (void)startRecording;
- (void)stopRecording;
- (void)feedSamplesToEngine:(UInt32)audioDataBytesCapacity audioData:(void *)audioData;
- (RecordState*)recordState;
- (void)runCallbackWithEvent: (NSNumber*)evNumber;
- (void)mainThreadCallback: (NSUInteger)evNumber;
@property lua_State* L;
@property int fn;
@end
void AudioInputCallback(void * inUserData, // Custom audio metadata
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription * inPacketDescs) {
Listener *rec = (__bridge Listener *)inUserData;
RecordState * recordState = [rec recordState];
if(!recordState->recording) return;
AudioQueueEnqueueBuffer(recordState->queue, inBuffer, 0, NULL);
[rec feedSamplesToEngine:inBuffer->mAudioDataBytesCapacity audioData:inBuffer->mAudioData];
}
@implementation Listener {
RecordState recordState;
detectors_t *detectors;
}
- (Listener*)initPlugins {
self = [super init];
if (self) {
self.fn = LUA_NOREF ;
recordState.recording = false;
detectors = detectors_new();
}
return self;
}
- (void)dealloc {
[self stopRecording]; // remove callbacks if not already stopped before deallocating
detectors_free(detectors);
}
- (RecordState*)recordState {
return &recordState;
}
- (void)setupAudioFormat:(AudioStreamBasicDescription*)format {
format->mSampleRate = kSampleRate;
format->mFormatID = kAudioFormatLinearPCM;
format->mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
format->mFramesPerPacket = 1;
format->mChannelsPerFrame = 1;
format->mBytesPerFrame = sizeof(float);
format->mBytesPerPacket = sizeof(float);
format->mBitsPerChannel = sizeof(float) * 8;
}
- (void)startRecording {
if(recordState.recording) return;
[self setupAudioFormat:&recordState.dataFormat];
recordState.currentFrame = 0;
OSStatus status;
status = AudioQueueNewInput(&recordState.dataFormat,
AudioInputCallback,
(__bridge void *)self,
NULL, // seems more responsive than CFRunLoopGetCurrent(),
kCFRunLoopCommonModes,
0,
&recordState.queue);
if (status == 0) {
for (int i = 0; i < NUM_BUFFERS; i++) {
AudioQueueAllocateBuffer(recordState.queue, DETECTORS_BLOCK_SIZE*sizeof(float), &recordState.buffers[i]);
AudioQueueEnqueueBuffer(recordState.queue, recordState.buffers[i], 0, nil);
}
recordState.recording = true;
status = AudioQueueStart(recordState.queue, NULL);
} else {
NSLog(@"Error: Couldn't open audio queue.");
}
}
- (void)stopRecording {
if(!recordState.recording) return;
recordState.recording = false;
AudioQueueStop(recordState.queue, true);
for (int i = 0; i < NUM_BUFFERS; i++) {
AudioQueueFreeBuffer(recordState.queue, recordState.buffers[i]);
}
AudioQueueDispose(recordState.queue, true);
AudioFileClose(recordState.audioFile);
}
- (void)mainThreadCallback: (NSUInteger)evNumber {
[self performSelectorOnMainThread:@selector(runCallbackWithEvent:)
withObject:[NSNumber numberWithLong: evNumber] waitUntilDone:NO];
}
- (void)feedSamplesToEngine:(UInt32)audioDataBytesCapacity audioData:(void *)audioData {
int sampleCount = audioDataBytesCapacity / sizeof(float);
float *samples = (float*)audioData;
NSAssert(sampleCount == DETECTORS_BLOCK_SIZE, @"Incorrect buffer size %i", sampleCount);
int result = detectors_process(detectors, samples);
if((result & TSS_START_CODE) == TSS_START_CODE) {
[self mainThreadCallback: 1]; // Tss on
}
if((result & TSS_STOP_CODE) == TSS_STOP_CODE) {
[self mainThreadCallback: 2]; // Tss off
}
if((result & POP_CODE) == POP_CODE) {
[self mainThreadCallback: 3]; // Pop
}
recordState.currentFrame += sampleCount;
}
- (void)runCallbackWithEvent: (NSNumber*)evNumber {
if (self.fn != LUA_NOREF) {
LuaSkin *skin = [LuaSkin sharedWithState:NULL];
lua_State* L = self.L;
_lua_stackguard_entry(L);
[skin pushLuaRef:refTable ref:self.fn];
lua_pushinteger(L, [evNumber intValue]);
[skin protectedCallAndError:@"hs.noises callback" nargs:1 nresults:0];
_lua_stackguard_exit(L);
}
}
@end
static int listener_gc(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
// Have to some contortions to make sure ARC properly frees the Listener
void **userdata = (void**)luaL_checkudata(L, 1, USERDATA_TAG);
Listener *listener = (__bridge_transfer Listener*)(*userdata);
[listener stopRecording];
listener.fn = [skin luaUnref:refTable ref:listener.fn];
*userdata = nil;
listener = nil;
return 0;
}
/// hs.noises:stop() -> self
/// Method
/// Stops the listener from recording and analyzing microphone input.
///
/// Parameters:
/// * None
///
/// Returns:
/// * The `hs.noises` object
static int listener_stop(lua_State* L) {
Listener* listener = get_listener_arg(L, 1);
[listener stopRecording];
lua_settop(L,1);
return 1;
}
/// hs.noises:start() -> self
/// Method
/// Starts listening to the microphone and passing the audio to the recognizer.
///
/// Parameters:
/// * None
///
/// Returns:
/// * The `hs.noises` object
static int listener_start(lua_State* L) {
Listener* listener = get_listener_arg(L, 1);
[listener startRecording];
lua_settop(L,1);
return 1;
}
static int listener_eq(lua_State* L) {
Listener* listenA = get_listener_arg(L, 1);
Listener* listenB = get_listener_arg(L, 2);
lua_pushboolean(L, listenA == listenB);
return 1;
}
void new_listener(lua_State* L, Listener* listener) {
void** listenptr = lua_newuserdata(L, sizeof(Listener**));
*listenptr = (__bridge_retained void*)listener;
luaL_getmetatable(L, USERDATA_TAG);
lua_setmetatable(L, -2);
}
/// hs.noises.new(fn) -> listener
/// Constructor
/// Creates a new listener for mouth noise recognition
///
/// Parameters:
/// * A function that is called when a mouth noise is recognized. It should accept a single parameter which will be a number representing the event type (see module docs).
///
/// Returns:
/// * An `hs.noises` object
static int listener_new(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TFUNCTION, LS_TBREAK];
Listener *listener = [[Listener alloc] initPlugins];
lua_pushvalue(L, 1);
listener.fn = [skin luaRef:refTable];
listener.L = L;
new_listener(L, listener);
return 1;
}
static int meta_gc(lua_State* __unused L) {
return 0;
}
// Metatable for created objects when _new invoked
static const luaL_Reg noises_metalib[] = {
{"start", listener_start},
{"stop", listener_stop},
{"__gc", listener_gc},
{"__eq", listener_eq},
{NULL, NULL}
};
// Functions for returned object when module loads
static const luaL_Reg noisesLib[] = {
{"new", listener_new},
{NULL, NULL}
};
// Metatable for returned object when module loads
static const luaL_Reg meta_gcLib[] = {
{"__gc", meta_gc},
{NULL, NULL}
};
int luaopen_hs_libnoises(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
refTable = [skin registerLibraryWithObject:USERDATA_TAG functions:noisesLib metaFunctions:meta_gcLib objectFunctions:noises_metalib];
return 1;
}
#pragma clang diagnostic pop