diff --git a/Patchfield/.classpath b/Patchfield/.classpath new file mode 100644 index 0000000..5cc5eb9 --- /dev/null +++ b/Patchfield/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Patchfield/.project b/Patchfield/.project new file mode 100644 index 0000000..d58ccdb --- /dev/null +++ b/Patchfield/.project @@ -0,0 +1,33 @@ + + + Patchfield + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/Patchfield/.settings/org.eclipse.jdt.core.prefs b/Patchfield/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/Patchfield/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/Patchfield/AndroidManifest.xml b/Patchfield/AndroidManifest.xml new file mode 100644 index 0000000..2f67be7 --- /dev/null +++ b/Patchfield/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/Patchfield/Makefile b/Patchfield/Makefile new file mode 100644 index 0000000..eb116e7 --- /dev/null +++ b/Patchfield/Makefile @@ -0,0 +1,10 @@ +all: + javah -classpath bin/classes -o jni/internal/audio_module_java.h \ + com.noisepages.nettoyeur.patchfield.AudioModule + javah -classpath bin/classes -o jni/internal/patchfield.h \ + com.noisepages.nettoyeur.patchfield.Patchfield + javah -classpath bin/classes -o jni/internal/shared_memory_utils.h \ + com.noisepages.nettoyeur.patchfield.internal.SharedMemoryUtils + javah -classpath bin/classes -o jni/modules/javamodule.h \ + com.noisepages.nettoyeur.patchfield.modules.JavaModule + ndk-build diff --git a/Patchfield/jni/Android.mk b/Patchfield/jni/Android.mk new file mode 100644 index 0000000..b328f78 --- /dev/null +++ b/Patchfield/jni/Android.mk @@ -0,0 +1,66 @@ +LOCAL_PATH := $(call my-dir) + +# Public libraries. + +include $(CLEAR_VARS) + +LOCAL_MODULE := audiomodule +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) +LOCAL_EXPORT_CFLAGS := -Wno-int-to-pointer-cast -Wno-pointer-to-int-cast +LOCAL_SRC_FILES := internal/audio_module.c +include $(BUILD_STATIC_LIBRARY) + +include $(CLEAR_VARS) + +LOCAL_MODULE := buffersizeadapter +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) +LOCAL_SRC_FILES := utils/buffer_size_adapter.c +LOCAL_STATIC_LIBRARIES := audiomodule +include $(BUILD_STATIC_LIBRARY) + + +# Java Audio module. + +include $(CLEAR_VARS) + +LOCAL_MODULE := javamodule +LOCAL_LDLIBS := -llog +LOCAL_SRC_FILES := modules/javamodule.c internal/simple_barrier.c +LOCAL_STATIC_LIBRARIES := audiomodule buffersizeadapter +include $(BUILD_SHARED_LIBRARY) + + +# Internal libraries. + +include $(CLEAR_VARS) + +LOCAL_MODULE := audiomoduleinternal +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) +LOCAL_EXPORT_CFLAGS := -Wno-int-to-pointer-cast -Wno-pointer-to-int-cast +LOCAL_EXPORT_LDLIBS := -lOpenSLES -llog +LOCAL_SRC_FILES := internal/audio_module_internal.c \ + internal/simple_barrier.c internal/shared_memory_internal.c \ + opensl_stream/opensl_stream.c +include $(BUILD_STATIC_LIBRARY) + +include $(CLEAR_VARS) + +LOCAL_MODULE := audiomodulejava +LOCAL_STATIC_LIBRARIES := audiomoduleinternal +LOCAL_SRC_FILES := internal/audio_module_java.c +include $(BUILD_SHARED_LIBRARY) + +include $(CLEAR_VARS) + +LOCAL_MODULE := patchfield +LOCAL_STATIC_LIBRARIES := audiomoduleinternal +LOCAL_SRC_FILES := internal/patchfield.c +include $(BUILD_SHARED_LIBRARY) + +include $(CLEAR_VARS) + +LOCAL_MODULE := shared_memory_utils +LOCAL_LDLIBS := -llog +LOCAL_SRC_FILES := internal/shared_memory_utils.c \ + internal/shared_memory_internal.c +include $(BUILD_SHARED_LIBRARY) diff --git a/Patchfield/jni/Application.mk b/Patchfield/jni/Application.mk new file mode 100644 index 0000000..84a096d --- /dev/null +++ b/Patchfield/jni/Application.mk @@ -0,0 +1,2 @@ +APP_OPTIM := release +APP_ABI := armeabi armeabi-v7a x86 diff --git a/Patchfield/jni/audio_module.h b/Patchfield/jni/audio_module.h new file mode 100644 index 0000000..15f313b --- /dev/null +++ b/Patchfield/jni/audio_module.h @@ -0,0 +1,58 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Native library for use with the native components of subclasses of + * AudioModule.java. Audio modules must implement a process callback of type + * audio_module_process_t and hook up the am_configure function in this library + * to the corresponding configure method in the Java class. See + * LowpassModule.java and lowpass.{h,c} for an example of this interaction. + */ + +#ifndef __AUDIO_MODULE_H__ +#define __AUDIO_MODULE_H__ +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Processing callback; takes a processing context (which is just a pointer to + * whatever data you want to pass to the callback), the sample rate, the buffer + * size in frames, the number of input and output channels, as well as input + * and output buffers whose size is the number of channels times the number of + * frames per buffer. Buffers are non-interleaved. + * + * This function will be invoked on a dedicated audio thread, and so any data + * in the context that may be modified concurrently must be protected (e.g., by + * gcc atomics) to prevent race conditions. + */ +typedef void (*audio_module_process_t) + (void *context, int sample_rate, int buffer_frames, + int input_channels, const float *input_buffer, + int output_channels, float *output_buffer); + +/* + * Configures the audio module with a native audio processing method and + * its context. The handle parameter is the handle that the AudioModule class + * passes to the protected configure method. On the Java side, this handle is + * of type long, and so in C it must be cast from jlong to void*. + */ +void am_configure(void *handle, audio_module_process_t process, void *context); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Patchfield/jni/internal/audio_module.c b/Patchfield/jni/internal/audio_module.c new file mode 100644 index 0000000..443d0d5 --- /dev/null +++ b/Patchfield/jni/internal/audio_module.c @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "audio_module.h" + +#include "internal/audio_module_internal.h" + +void am_configure(void *handle, audio_module_process_t process, void *context) { + audio_module_runner *amr = (audio_module_runner *) handle; + amr->process = process; + amr->context = context; +} diff --git a/Patchfield/jni/internal/audio_module_internal.c b/Patchfield/jni/internal/audio_module_internal.c new file mode 100644 index 0000000..5efe10d --- /dev/null +++ b/Patchfield/jni/internal/audio_module_internal.c @@ -0,0 +1,206 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "audio_module_internal.h" + +#include "opensl_stream/opensl_stream.h" +#include "audio_module_internal.h" +#include "shared_memory_internal.h" +#include "simple_barrier.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOGI(...) \ + __android_log_print(ANDROID_LOG_INFO, "audio_module_internal", __VA_ARGS__) +#define LOGW(...) \ + __android_log_print(ANDROID_LOG_WARN, "audio_module_internal", __VA_ARGS__) + +audio_module *ami_get_audio_module(void *p, int index) { + return ((audio_module *) p) + index; +} + +float *ami_get_audio_buffer(void *p, ptrdiff_t offset) { + return ((float *) p) + offset; +} + +simple_barrier_t *ami_get_barrier(void *p, ptrdiff_t offset) { + return ((simple_barrier_t *) p) + offset; +} + +void ami_collect_input(void *p, int index) { + audio_module *module = ami_get_audio_module(p, index); + float *input_buffer = ami_get_audio_buffer(p, module->input_buffer); + memset(input_buffer, 0, + module->buffer_frames * module->input_channels * sizeof(float)); + int i, j; + for (i = 0; i < MAX_CONNECTIONS; ++i) { + connection *conn = module->input_connections + i; + if (conn->in_use) { + audio_module *source = ami_get_audio_module(p, conn->source_index); + if (source->in_use) { + float *input_channel = + input_buffer + conn->sink_port * module->buffer_frames; + float *source_channel = ami_get_audio_buffer(p, + source->output_buffer) + conn->source_port * module->buffer_frames; + if (!sb_wait(ami_get_barrier(p, source->ready), &source->deadline)) { + for (j = 0; j < module->buffer_frames; ++j) { + input_channel[j] += source_channel[j]; + } + } + } + } + } +} + +#define AM_SIG_ALRM SIGRTMAX + +static __thread sigjmp_buf sig_env; + +static void signal_handler(int sig, siginfo_t *info, void *context) { + LOGI("Received signal %d.", sig); + siglongjmp(sig_env, 1); +} + +static void *run_module(void *arg) { + LOGI("Entering run_module."); + audio_module_runner *amr = (audio_module_runner *) arg; + sb_wake(&amr->launched); + audio_module *module = ami_get_audio_module(amr->shm_ptr, amr->index); + + timer_t timer; + struct sigevent evp; + evp.sigev_notify = SIGEV_THREAD_ID; + evp.sigev_signo = AM_SIG_ALRM; + evp.sigev_value.sival_ptr = module; + evp.sigev_notify_thread_id = gettid(); + timer_create(CLOCK_MONOTONIC, &evp, &timer); + + struct itimerspec timeout; + timeout.it_interval.tv_sec = 0; + timeout.it_interval.tv_nsec = 0; + timeout.it_value.tv_sec = 1; // One-second timeout. + timeout.it_value.tv_nsec = 0; + + struct itimerspec cancel; + cancel.it_interval.tv_sec = 0; + cancel.it_interval.tv_nsec = 0; + cancel.it_value.tv_sec = 0; + cancel.it_value.tv_nsec = 0; + + if (!sigsetjmp(sig_env, 1)) { + while (1) { + sb_wake(ami_get_barrier(amr->shm_ptr, module->report)); + sb_wait_and_clear(ami_get_barrier(amr->shm_ptr, module->wake), NULL); + if (amr->done) { + break; + } + ami_collect_input(amr->shm_ptr, amr->index); + timer_settime(timer, 0, &timeout, NULL); // Arm timer. + amr->process(amr->context, module->sample_rate, module->buffer_frames, + module->input_channels, + ami_get_audio_buffer(amr->shm_ptr, module->input_buffer), + module->output_channels, + ami_get_audio_buffer(amr->shm_ptr, module->output_buffer)); + timer_settime(timer, 0, &cancel, NULL); // Disarm timer. + sb_wake(ami_get_barrier(amr->shm_ptr, module->ready)); + } + } else { + __sync_bool_compare_and_swap(&amr->timed_out, 0, 1); + // We can safely log now because we have already left the processing chain. + LOGW("Process callback interrupted after timeout; terminating thread."); + } + + timer_delete(timer); + LOGI("Leaving run_module."); + return NULL; +} + +static void launch_thread(void *context, int sample_rate, int buffer_frames, + int input_channels, const short *input_buffer, + int output_channels, short *output_buffer) { + audio_module_runner *amr = (audio_module_runner *) context; + if (!--amr->launch_counter) { + if (pthread_create(&amr->thread, NULL, run_module, amr)) { + LOGW("Thread creation failed: %s", strerror(errno)); + } + } +} + +static size_t get_protected_size() { + return BARRIER_OFFSET * MEM_PAGE_SIZE; +} + +audio_module_runner *ami_create(int version, int token, int index) { + if (version != PATCHFIELD_PROTOCOL_VERSION) { + LOGW("Protocol version mismatch."); + return NULL; + } + audio_module_runner *amr = malloc(sizeof(audio_module_runner)); + if (amr) { + amr->shm_fd = token; + amr->shm_ptr = smi_map(token); + smi_protect(amr->shm_ptr, get_protected_size()); + amr->index = index; + amr->done = 0; + amr->timed_out = 0; + amr->process = NULL; + amr->context = NULL; + amr->launch_counter = 3; // Make sure that this number stays current. + + audio_module *module = ami_get_audio_module(amr->shm_ptr, amr->index); + // Clear barriers, just in case. + sb_clobber(ami_get_barrier(amr->shm_ptr, module->report)); + sb_clobber(ami_get_barrier(amr->shm_ptr, module->wake)); + sb_clobber(ami_get_barrier(amr->shm_ptr, module->ready)); + + OPENSL_STREAM *os = opensl_open(module->sample_rate, 0, 2, + module->buffer_frames, launch_thread, amr); + sb_clobber(&amr->launched); + opensl_start(os); + sb_wait(&amr->launched, NULL); + opensl_close(os); + + struct sigaction act; + act.sa_sigaction = signal_handler; + act.sa_flags = SA_SIGINFO; + sigfillset(&act.sa_mask); + sigaction(AM_SIG_ALRM, &act, NULL); + } + return (audio_module_runner *) amr; +} + +void ami_release(audio_module_runner *amr) { + audio_module *module = ami_get_audio_module(amr->shm_ptr, amr->index); + + amr->done = 1; + sb_wake(ami_get_barrier(amr->shm_ptr, module->wake)); + pthread_join(amr->thread, NULL); + + smi_unmap(amr->shm_ptr); + free(amr); +} + +int ami_has_timed_out(audio_module_runner *amr) { + return __sync_or_and_fetch(&amr->timed_out, 0); +} diff --git a/Patchfield/jni/internal/audio_module_internal.h b/Patchfield/jni/internal/audio_module_internal.h new file mode 100644 index 0000000..63f3107 --- /dev/null +++ b/Patchfield/jni/internal/audio_module_internal.h @@ -0,0 +1,95 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Internal data structures representing audio modules and their connections, + * as well as some functions that operate on them. + */ + +#ifndef __AUDIO_MODULE_INTERNAL_H__ +#define __AUDIO_MODULE_INTERNAL_H__ + +#include "audio_module.h" + +#include "simple_barrier.h" + +#include +#include +#include +#include + +#define PATCHFIELD_PROTOCOL_VERSION 6 + +#define MAX_MODULES 32 +#define MAX_CONNECTIONS 16 + +#define MEM_PAGE_SIZE sysconf(_SC_PAGESIZE) +#define BARRIER_OFFSET (MAX_MODULES * sizeof(audio_module) / MEM_PAGE_SIZE + 1) +#define BUFFER_OFFSET \ + (BARRIER_OFFSET + MAX_MODULES * 3 * sizeof(int) / MEM_PAGE_SIZE + 1) + +typedef struct { + int status; // 0: none; 1: current; 2: slated for deletion + int in_use; + + int source_index; + int source_port; + int sink_port; +} connection; + +typedef struct { + int status; // 0: none; 1: current; 2: slated for deletion + int active; + int in_use; + + int sample_rate; + int buffer_frames; + + int input_channels; + ptrdiff_t input_buffer; // Storing buffers as offsets of type ptrdiff_t + int output_channels; // rather than pointers of type float* to render + ptrdiff_t output_buffer; // them independent of the shared memory location. + + connection input_connections[MAX_CONNECTIONS]; + + struct timespec deadline; + ptrdiff_t report; + ptrdiff_t wake; + ptrdiff_t ready; +} audio_module; + +typedef struct { + int shm_fd; + void *shm_ptr; + int index; + pthread_t thread; + simple_barrier_t launched; + int launch_counter; + int done; + int timed_out; + audio_module_process_t process; + void *context; +} audio_module_runner; + +audio_module *ami_get_audio_module(void *p, int index); +float *ami_get_audio_buffer(void *p, ptrdiff_t offset); +simple_barrier_t *ami_get_barrier(void *p, ptrdiff_t offset); +void ami_collect_input(void *p, int index); +audio_module_runner *ami_create(int version, int token, int index); +void ami_release(audio_module_runner *p); +int ami_has_timed_out(audio_module_runner *p); + +#endif diff --git a/Patchfield/jni/internal/audio_module_java.c b/Patchfield/jni/internal/audio_module_java.c new file mode 100644 index 0000000..0bc4f39 --- /dev/null +++ b/Patchfield/jni/internal/audio_module_java.c @@ -0,0 +1,45 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "audio_module_java.h" + +#include "audio_module_internal.h" + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_AudioModule_getProtocolVersion +(JNIEnv *env, jclass cls) { + return PATCHFIELD_PROTOCOL_VERSION; +} + +JNIEXPORT jlong JNICALL +Java_com_noisepages_nettoyeur_patchfield_AudioModule_createRunner +(JNIEnv *env, jobject obj, jint version, jint token, jint index) { + return (jlong) ami_create(version, token, index); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_AudioModule_release +(JNIEnv *env, jobject obj, jlong p) { + audio_module_runner *amr = (audio_module_runner *) p; + ami_release(amr); +} + +JNIEXPORT jboolean JNICALL +Java_com_noisepages_nettoyeur_patchfield_AudioModule_hasTimedOut +(JNIEnv *env, jobject obj, jlong p) { + audio_module_runner *amr = (audio_module_runner *) p; + return ami_has_timed_out(amr); +} diff --git a/Patchfield/jni/internal/audio_module_java.h b/Patchfield/jni/internal/audio_module_java.h new file mode 100644 index 0000000..c30929c --- /dev/null +++ b/Patchfield/jni/internal/audio_module_java.h @@ -0,0 +1,62 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_noisepages_nettoyeur_patchfield_AudioModule */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_AudioModule +#define _Included_com_noisepages_nettoyeur_patchfield_AudioModule +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_noisepages_nettoyeur_patchfield_AudioModule + * Method: getProtocolVersion + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_AudioModule_getProtocolVersion + (JNIEnv *, jclass); + +/* + * Class: com_noisepages_nettoyeur_patchfield_AudioModule + * Method: createRunner + * Signature: (III)J + */ +JNIEXPORT jlong JNICALL Java_com_noisepages_nettoyeur_patchfield_AudioModule_createRunner + (JNIEnv *, jobject, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_AudioModule + * Method: release + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_AudioModule_release + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_AudioModule + * Method: hasTimedOut + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_com_noisepages_nettoyeur_patchfield_AudioModule_hasTimedOut + (JNIEnv *, jobject, jlong); + +#ifdef __cplusplus +} +#endif +#endif +/* Header for class com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread +#define _Included_com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread +#ifdef __cplusplus +extern "C" { +#endif +#undef com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_MIN_PRIORITY +#define com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_MIN_PRIORITY 1L +#undef com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_NORM_PRIORITY +#define com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_NORM_PRIORITY 5L +#undef com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_MAX_PRIORITY +#define com_noisepages_nettoyeur_patchfield_AudioModule_FdReceiverThread_MAX_PRIORITY 10L +#ifdef __cplusplus +} +#endif +#endif diff --git a/Patchfield/jni/internal/patchfield.c b/Patchfield/jni/internal/patchfield.c new file mode 100644 index 0000000..22b6299 --- /dev/null +++ b/Patchfield/jni/internal/patchfield.c @@ -0,0 +1,464 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "patchfield.h" + +#include "audio_module_internal.h" +#include "opensl_stream/opensl_stream.h" +#include "shared_memory_internal.h" +#include "simple_barrier.h" + +#include +#include +#include +#include +#include +#include + +#define LOGI(...) \ + __android_log_print(ANDROID_LOG_INFO, "patchfield", __VA_ARGS__) +#define LOGW(...) \ + __android_log_print(ANDROID_LOG_WARN, "patchfield", __VA_ARGS__) + +typedef struct { + OPENSL_STREAM *os; + int sample_rate; + int buffer_frames; + int shm_fd; + void *shm_ptr; + ptrdiff_t next_buffer; +} patchfield; + +static void perform_cleanup(patchfield *pb) { + int i, j, k; + for (i = 0; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + if (__sync_or_and_fetch(&module->status, 0) == 2) { + int buffer_frames = (module->input_channels + module->output_channels) * + pb->buffer_frames; + pb->next_buffer -= buffer_frames; + for (j = 0; j < MAX_MODULES; ++j) { + audio_module *other = ami_get_audio_module(pb->shm_ptr, j); + if (other->input_buffer > module->input_buffer) { + other->input_buffer -= buffer_frames; + other->output_buffer -= buffer_frames; + } + for (k = 0; k < MAX_CONNECTIONS; ++k) { + connection *conn = other->input_connections + k; + if (conn->source_index == i && + __sync_or_and_fetch(&conn->status, 0)) { + int val = 1; + while (val = __sync_val_compare_and_swap(&conn->status, val, 0)); + } + } + } + __sync_bool_compare_and_swap(&module->status, 2, 0); + } else { + for (j = 0; j < MAX_CONNECTIONS; ++j) { + connection *conn = module->input_connections + j; + __sync_bool_compare_and_swap(&conn->status, 2, 0); + } + } + } +} + +static int is_running(patchfield *pb) { + return opensl_is_running(pb->os); +} + +static int add_module(patchfield *pb, + int input_channels, int output_channels) { + if (!is_running(pb)) { + perform_cleanup(pb); + } + if ((pb->next_buffer + (input_channels + output_channels) * + pb->buffer_frames) * sizeof(float) > smi_get_size()) { + return -9; // PatchfieldException.OUT_OF_BUFFER_SPACE + } + int i; + for (i = 0; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + if (__sync_or_and_fetch(&module->status, 0) == 0) { + module->active = 0; + module->in_use = 0; + module->sample_rate = pb->sample_rate; + module->buffer_frames = pb->buffer_frames; + module->input_channels = input_channels; + module->input_buffer = pb->next_buffer; + pb->next_buffer += input_channels * pb->buffer_frames; + module->output_channels = output_channels; + module->output_buffer = pb->next_buffer; + pb->next_buffer += output_channels * pb->buffer_frames; + module->report = + BARRIER_OFFSET * MEM_PAGE_SIZE / sizeof(simple_barrier_t) + i * 3; + sb_clobber(ami_get_barrier(pb->shm_ptr, module->report)); + module->wake = module->report + 1; + sb_clobber(ami_get_barrier(pb->shm_ptr, module->wake)); + module->ready = module->report + 2; + sb_clobber(ami_get_barrier(pb->shm_ptr, module->ready)); + memset(module->input_connections, 0, + MAX_CONNECTIONS * sizeof(connection)); + __sync_bool_compare_and_swap(&module->status, 0, 1); + return i; + } + } + return -5; // PatchfieldException.TOO_MANY_MODULES +} + +static int delete_module(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + __sync_bool_compare_and_swap(&module->status, 1, 2); + return 0; +} + +static int activate_module(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + __sync_bool_compare_and_swap(&module->active, 0, 1); + return 0; +} +static int deactivate_module(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + __sync_bool_compare_and_swap(&module->active, 1, 0); + return 0; +} + +static int is_active(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + return __sync_or_and_fetch(&module->active, 0); +} + +static int get_input_channels(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + return module->input_channels; +} + +static int get_output_channels(patchfield *pb, int index) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, index); + return module->output_channels; +} + +static int is_connected(patchfield *pb, int source_index, int source_port, + int sink_index, int sink_port) { + audio_module *sink = ami_get_audio_module(pb->shm_ptr, sink_index); + int i; + for (i = 0; i < MAX_CONNECTIONS; ++i) { + connection *input = sink->input_connections + i; + if (input->source_index == source_index && + input->source_port == source_port && + input->sink_port == sink_port && + __sync_or_and_fetch(&input->status, 0) == 1) { + return 1; + } + } + return 0; +} + +static int connect_modules(patchfield *pb, int source_index, int source_port, + int sink_index, int sink_port) { + if (!is_running(pb)) { + perform_cleanup(pb); + } + audio_module *sink = ami_get_audio_module(pb->shm_ptr, sink_index); + int i; + for (i = 0; i < MAX_CONNECTIONS; ++i) { + connection *input = sink->input_connections + i; + if (__sync_or_and_fetch(&input->status, 0) == 0) { + input->sink_port = sink_port; + input->source_index = source_index; + input->source_port = source_port; + __sync_bool_compare_and_swap(&input->status, 0, 1); + return 0; + } + } + return -7; // PatchfieldException.TOO_MANY_CONNECTIONS +} + +static int disconnect_modules(patchfield *pb, int source_index, int source_port, + int sink_index, int sink_port) { + audio_module *sink = ami_get_audio_module(pb->shm_ptr, sink_index); + int i; + for (i = 0; i < MAX_CONNECTIONS; ++i) { + connection *input = sink->input_connections + i; + if (input->source_index == source_index && + input->source_port == source_port && + input->sink_port == sink_port && + __sync_bool_compare_and_swap(&input->status, 1, 2)) { + break; + } + } + return 0; +} + +static void release(patchfield *pb) { + int i; + opensl_close(pb->os); + smi_unlock(pb->shm_ptr); + smi_unmap(pb->shm_ptr); + close(pb->shm_fd); + free(pb); +} + +#define ONE_BILLION 1000000000 + +static void add_nsecs(struct timespec *t, int dt) { + t->tv_nsec += dt; + if (t->tv_nsec >= ONE_BILLION) { + ++t->tv_sec; + t->tv_nsec -= ONE_BILLION; + } +} + +static const float float_to_short = SHRT_MAX; +static const float short_to_float = 1 / (1 + (float) SHRT_MAX); + +static void process(void *context, int sample_rate, int buffer_frames, + int input_channels, const short *input_buffer, + int output_channels, short *output_buffer) { + patchfield *pb = (patchfield *) context; + struct timespec deadline; + clock_gettime(CLOCK_MONOTONIC, &deadline); + add_nsecs(&deadline, 100000); // 0.1ms deadline for clients to report. + int i, j; + for (i = 0; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + module->in_use = + __sync_or_and_fetch(&module->status, 0) == 1 && + __sync_or_and_fetch(&module->active, 0) && + ((i < 2) || sb_wait_and_clear( + ami_get_barrier(pb->shm_ptr, module->report), &deadline) == 0); + if (module->in_use) { + sb_clobber(ami_get_barrier(pb->shm_ptr, module->ready)); + for (j = 0; j < MAX_CONNECTIONS; ++j) { + connection *conn = module->input_connections + j; + conn->in_use = (__sync_or_and_fetch(&conn->status, 0) == 1); + } + } + } + audio_module *input = ami_get_audio_module(pb->shm_ptr, 0); + if (input->in_use) { + float *b = ami_get_audio_buffer(pb->shm_ptr, input->output_buffer); + for (i = 0; i < input_channels; ++i) { + for (j = 0; j < buffer_frames; ++j) { + b[j] = input_buffer[i + j * input_channels] * short_to_float; + } + b += buffer_frames; + } + sb_wake(ami_get_barrier(pb->shm_ptr, input->ready)); + } + int dt = (ONE_BILLION / sample_rate + 1) * buffer_frames; + clock_gettime(CLOCK_MONOTONIC, &deadline); + add_nsecs(&deadline, 2 * dt); // Two-buffer-period processing deadline. + for (i = 2; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + if (module->in_use) { + module->deadline.tv_sec = deadline.tv_sec; + module->deadline.tv_nsec = deadline.tv_nsec; + sb_wake(ami_get_barrier(pb->shm_ptr, module->wake)); + } + } + audio_module *output = ami_get_audio_module(pb->shm_ptr, 1); + if (output->in_use) { + ami_collect_input(pb->shm_ptr, 1); + float *b = ami_get_audio_buffer(pb->shm_ptr, output->input_buffer); + for (i = 0; i < output_channels; ++i) { + for (j = 0; j < buffer_frames; ++j) { + float v = b[j]; + output_buffer[i + j * output_channels] = (short) (float_to_short * + (isnan(v) ? 0 : (v < -1.0f ? -1.0f : (v > 1.0f ? 1.0f : v)))); + } + b += buffer_frames; + } + } + for (i = 2; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + if (module->in_use) { + sb_wait(ami_get_barrier(pb->shm_ptr, module->ready), + &module->deadline); + } + } + perform_cleanup(pb); +} + +static patchfield *create_instance(int sample_rate, int buffer_frames, + int input_channels, int output_channels) { + patchfield *pb = malloc(sizeof(patchfield)); + if (pb) { + pb->sample_rate = sample_rate; + pb->buffer_frames = buffer_frames; + pb->next_buffer = BUFFER_OFFSET * MEM_PAGE_SIZE / sizeof(float); + + pb->shm_fd = smi_create(); + if (pb->shm_fd < 0) { + LOGW("Unable to create shared memory."); + free(pb); + return NULL; + } + pb->shm_ptr = smi_map(pb->shm_fd); + if (!pb->shm_ptr) { + LOGW("Unable to map shared memory."); + close(pb->shm_fd); + free(pb); + return NULL; + } + smi_lock(pb->shm_ptr); + + // Create OpenSL stream. + pb->os = opensl_open(sample_rate, + input_channels, output_channels, buffer_frames, process, pb); + if (!pb->os) { + smi_unlock(pb->shm_ptr); + smi_unmap(pb->shm_ptr); + close(pb->shm_fd); + free(pb); + return NULL; + } + + int i; + for (i = 0; i < MAX_MODULES; ++i) { + audio_module *module = ami_get_audio_module(pb->shm_ptr, i); + memset(module, 0, sizeof(audio_module)); + } + activate_module(pb, add_module(pb, 0, input_channels)); + activate_module(pb, add_module(pb, output_channels, 0)); + } + return pb; +} + +JNIEXPORT jlong JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_createInstance +(JNIEnv *env, jobject obj, jint sample_rate, jint buffer_frames, + int input_channels, int output_channels) { + return (jlong) create_instance(sample_rate, buffer_frames, + input_channels, output_channels); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_releaseInstance +(JNIEnv *env, jobject obj, jlong p) { + patchfield *pb = (patchfield *) p; + release(pb); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_sendSharedMemoryFileDescriptor +(JNIEnv *env, jobject obj, jlong p) { + patchfield *pb = (patchfield *) p; + if (smi_send(pb->shm_fd) < 0) { + LOGW("Failed to send file descriptor."); + return -1; // PatchfieldException.FAILURE + } + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_start +(JNIEnv *env, jobject obj, jlong p) { + patchfield *pb = (patchfield *) p; + return opensl_start(pb->os); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_stop +(JNIEnv *env, jobject obj, jlong p) { + patchfield *pb = (patchfield *) p; + opensl_pause(pb->os); +} + +JNIEXPORT jboolean JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_isRunning +(JNIEnv *env, jobject obj, jlong p) { + patchfield *pb = (patchfield *) p; + return is_running(pb); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_createModule +(JNIEnv *env, jobject obj, jlong p, jint input_channels, jint output_channels) { + patchfield *pb = (patchfield *) p; + return add_module(pb, input_channels, output_channels); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_connectPorts +(JNIEnv *env, jobject obj, jlong p, + jint source_index, jint source_port, jint sink_index, jint sink_port) { + patchfield *pb = (patchfield *) p; + return connect_modules(pb, source_index, source_port, sink_index, sink_port); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_disconnectPorts +(JNIEnv *env, jobject obj, jlong p, + jint source_index, jint source_port, jint sink_index, jint sink_port) { + patchfield *pb = (patchfield *) p; + return disconnect_modules(pb, source_index, source_port, sink_index, sink_port); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_deleteModule +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return delete_module(pb, index); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_activateModule +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return activate_module(pb, index); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_deactivateModule +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return deactivate_module(pb, index); +} + +JNIEXPORT jboolean JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_isActive +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return is_active(pb, index); +} + +JNIEXPORT jboolean JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_isConnected +(JNIEnv *env, jobject obj, jlong p, jint sourceIndex, jint sourcePort, + jint sinkIndex, jint sinkPort) { + patchfield *pb = (patchfield *) p; + return is_connected(pb, sourceIndex, sourcePort, sinkIndex, sinkPort); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_getInputChannels +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return get_input_channels(pb, index); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_getOutputChannels +(JNIEnv *env, jobject obj, jlong p, jint index) { + patchfield *pb = (patchfield *) p; + return get_output_channels(pb, index); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_Patchfield_getProtocolVersion +(JNIEnv *env, jobject obj, jlong p) { + return PATCHFIELD_PROTOCOL_VERSION; +} diff --git a/Patchfield/jni/internal/patchfield.h b/Patchfield/jni/internal/patchfield.h new file mode 100644 index 0000000..94f38bf --- /dev/null +++ b/Patchfield/jni/internal/patchfield.h @@ -0,0 +1,149 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_noisepages_nettoyeur_patchfield_Patchfield */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_Patchfield +#define _Included_com_noisepages_nettoyeur_patchfield_Patchfield +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: createInstance + * Signature: (IIII)J + */ +JNIEXPORT jlong JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_createInstance + (JNIEnv *, jobject, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: sendSharedMemoryFileDescriptor + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_sendSharedMemoryFileDescriptor + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: releaseInstance + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_releaseInstance + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: createModule + * Signature: (JII)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_createModule + (JNIEnv *, jobject, jlong, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: deleteModule + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_deleteModule + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: connectPorts + * Signature: (JIIII)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_connectPorts + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: disconnectPorts + * Signature: (JIIII)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_disconnectPorts + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: activateModule + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_activateModule + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: deactivateModule + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_deactivateModule + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: start + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_start + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: stop + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_stop + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: isActive + * Signature: (JI)Z + */ +JNIEXPORT jboolean JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_isActive + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: isRunning + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_isRunning + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: isConnected + * Signature: (JIIII)Z + */ +JNIEXPORT jboolean JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_isConnected + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: getInputChannels + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_getInputChannels + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: getOutputChannels + * Signature: (JI)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_getOutputChannels + (JNIEnv *, jobject, jlong, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_Patchfield + * Method: getProtocolVersion + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_Patchfield_getProtocolVersion + (JNIEnv *, jobject, jlong); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Patchfield/jni/internal/shared_memory_internal.c b/Patchfield/jni/internal/shared_memory_internal.c new file mode 100644 index 0000000..76c686c --- /dev/null +++ b/Patchfield/jni/internal/shared_memory_internal.c @@ -0,0 +1,162 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "shared_memory_internal.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOGI(...) \ + __android_log_print(ANDROID_LOG_INFO, "shared_memory_internal", __VA_ARGS__) +#define LOGW(...) \ + __android_log_print(ANDROID_LOG_WARN, "shared_memory_internal", __VA_ARGS__) + +#define ASHMEM_MODULE "/dev/ashmem" +#define SHARED_MEM_SIZE 262144 +#define SOCK_NAME "patchfield_shm_socket" + +int smi_create() { + int fd = open(ASHMEM_MODULE, O_RDWR); + if (fd < 0) { + LOGW("Failed to open ashmem: %s", strerror(errno)); + return -1; + } + if (ioctl(fd, ASHMEM_SET_SIZE, SHARED_MEM_SIZE) < 0) { + LOGW("Failed to allocate shared memory: %s", strerror(errno)); + close(fd); + return -1; + } + return fd; +} + +void *smi_map(int fd) { + return mmap(NULL, SHARED_MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); +} + +int smi_unmap(void *p) { + return munmap(p, SHARED_MEM_SIZE); +} + +int smi_lock(void *p) { + int result = mlock(p, SHARED_MEM_SIZE); + if (result) { + LOGW("Failed to lock shared memory: %s", strerror(errno)); + } else { + LOGI("Locked shared memory."); + } + return result; +} + +int smi_unlock(void *p) { + return munlock(p, SHARED_MEM_SIZE); +} + +int smi_protect(void *p, size_t n) { + mprotect(p, n, PROT_READ); +} + +static int smi_transmit(int fd) { + // Boilerplate for passing file descriptors across processes. + int x = 1; + struct iovec v; + v.iov_base = &x; + v.iov_len = sizeof(int); + struct msghdr hdr; + hdr.msg_name = NULL; + hdr.msg_namelen = 0; + hdr.msg_iov = &v; + hdr.msg_iovlen = 1; + union { + struct cmsghdr chdr; + char control[CMSG_SPACE(sizeof(int))]; + } ctrl; + hdr.msg_control = ctrl.control; + hdr.msg_controllen = sizeof(ctrl.control); + ctrl.chdr.cmsg_type = SCM_RIGHTS; + ctrl.chdr.cmsg_level = SOL_SOCKET; + ctrl.chdr.cmsg_len = CMSG_LEN(sizeof(int)); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = 0; + char *path = SOCK_NAME; + strncpy(&addr.sun_path[1], path, strlen(path)); + int sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (sock_fd < 0) { + LOGW("Failed to open socket: %s", strerror(errno)); + return -1; + } + + if (fd >= 0) { + if (connect(sock_fd, (struct sockaddr *) &addr, + sizeof(struct sockaddr_un)) < 0) { + close(sock_fd); + LOGW("Failed to connect socket: %s", strerror(errno)); + return -1; + } + *((int *) CMSG_DATA(CMSG_FIRSTHDR(&hdr))) = fd; + if (sendmsg(sock_fd, &hdr, 0) < 0) { + close(sock_fd); + LOGW("Failed to pass file descriptor: %s", strerror(errno)); + return -1; + } + if (close(sock_fd) < 0) { + LOGW("Failed to close socket: %s", strerror(errno)); + } + return 0; + } else { + if (bind(sock_fd, (struct sockaddr *) &addr, + sizeof(struct sockaddr_un)) < 0) { + close(sock_fd); + LOGW("Failed to bind socket: %s", strerror(errno)); + return -1; + } + if (recvmsg(sock_fd, &hdr, 0) < 0) { + close(sock_fd); + LOGW("Failed to receive file descriptor: %s", strerror(errno)); + return -1; + } + if (close(sock_fd) < 0) { + LOGW("Failed to close socket: %s", strerror(errno)); + } + return *((int *) CMSG_DATA(CMSG_FIRSTHDR(&hdr))); + } +} + +int smi_send(int fd) { + if (fd < 0) { + LOGW("Negative file descriptor."); + return -1; + } + return smi_transmit(fd); +} + +int smi_receive() { + return smi_transmit(-1); +} + +long smi_get_size() { + return SHARED_MEM_SIZE; +} diff --git a/Patchfield/jni/internal/shared_memory_internal.h b/Patchfield/jni/internal/shared_memory_internal.h new file mode 100644 index 0000000..07f2054 --- /dev/null +++ b/Patchfield/jni/internal/shared_memory_internal.h @@ -0,0 +1,38 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Tools for handling shared memory with ashmem. Most functions simply wrap + * basic system calls, except for smi_{send,receive}, which serve to pass the + * ashmem file descriptor across process boundaries using Unix domain sockets. + */ + +#ifndef __SHARED_MEMORY_INTERNAL_H__ +#define __SHARED_MEMORY_INTERNAL_H__ + +#include + +int smi_create(); +void *smi_map(int fd); +int smi_unmap(void *p); +int smi_lock(void *p); +int smi_unlock(void *p); +int smi_protect(void *p, size_t n); +int smi_send(int fd); +int smi_receive(); +long smi_get_size(); + +#endif diff --git a/Patchfield/jni/internal/shared_memory_utils.c b/Patchfield/jni/internal/shared_memory_utils.c new file mode 100644 index 0000000..73f8137 --- /dev/null +++ b/Patchfield/jni/internal/shared_memory_utils.c @@ -0,0 +1,33 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "shared_memory_utils.h" + +#include "internal/shared_memory_internal.h" + +#include + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils_receiveSharedMemoryFileDescriptor +(JNIEnv *env, jclass cls) { + return smi_receive(); +} + +JNIEXPORT jint JNICALL +Java_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils_closeSharedMemoryFileDescriptor +(JNIEnv *env, jclass cls, jint fd) { + return close(fd); +} diff --git a/Patchfield/jni/internal/shared_memory_utils.h b/Patchfield/jni/internal/shared_memory_utils.h new file mode 100644 index 0000000..1fb60df --- /dev/null +++ b/Patchfield/jni/internal/shared_memory_utils.h @@ -0,0 +1,29 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils +#define _Included_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils + * Method: receiveSharedMemoryFileDescriptor + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils_receiveSharedMemoryFileDescriptor + (JNIEnv *, jclass); + +/* + * Class: com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils + * Method: closeSharedMemoryFileDescriptor + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_com_noisepages_nettoyeur_patchfield_internal_SharedMemoryUtils_closeSharedMemoryFileDescriptor + (JNIEnv *, jclass, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Patchfield/jni/internal/simple_barrier.c b/Patchfield/jni/internal/simple_barrier.c new file mode 100644 index 0000000..dbc1cdb --- /dev/null +++ b/Patchfield/jni/internal/simple_barrier.c @@ -0,0 +1,108 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "simple_barrier.h" + +#include +#include +#include +#include + +#define ONE_BILLION 1000000000 + +static void get_relative_deadline( + const struct timespec *abstime, struct timespec *reltime) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec < abstime->tv_sec || + (now.tv_sec == abstime->tv_sec && now.tv_nsec < abstime->tv_nsec)) { + reltime->tv_sec = abstime->tv_sec - now.tv_sec; + if (abstime->tv_nsec >= now.tv_nsec) { + reltime->tv_nsec = abstime->tv_nsec - now.tv_nsec; + } else { + --reltime->tv_sec; + reltime->tv_nsec = (ONE_BILLION + abstime->tv_nsec) - now.tv_nsec; + } + } else { + reltime->tv_sec = 0; + reltime->tv_nsec = 0; + } +} + +static void futex_wait(simple_barrier_t *p, struct timespec *abstime) { + if (abstime == NULL) { + syscall(__NR_futex, p, FUTEX_WAIT, 0, NULL, NULL, 0, 0); + } else { + struct timespec reltime; + get_relative_deadline(abstime, &reltime); + syscall(__NR_futex, p, FUTEX_WAIT, 0, &reltime, NULL, 0, 0); + // Note: Passing struct timespec as ktime_t works for now but may need + // further consideration when we move to 64bit. + } +} + +int sb_wait(simple_barrier_t *p, struct timespec *abstime) { + switch (__sync_or_and_fetch(p, 0)) { + case 0: + futex_wait(p, abstime); + break; + case 1: + return 0; // Success! + default: + return -2; // Error; the futex has been tampered with. + } + switch (__sync_or_and_fetch(p, 0)) { + case 0: + return -1; // Failure; __futex_wait probably timed out. + case 1: + return 0; + default: + return -2; + } +} + +int sb_wait_and_clear(simple_barrier_t *p, struct timespec *abstime) { + switch (__sync_or_and_fetch(p, 0)) { + case 0: + futex_wait(p, abstime); + case 1: + break; + default: + return -2; + } + switch (__sync_val_compare_and_swap(p, 1, 0)) { + case 0: + return -1; + case 1: + return 0; + default: + return -2; + } +} + +int sb_wake(simple_barrier_t *p) { + if (__sync_bool_compare_and_swap(p, 0, 1)) { + syscall(__NR_futex, p, FUTEX_WAKE, INT_MAX, NULL, NULL, 0, 0); + return 0; + } else { + return -2; + } +} + +void sb_clobber(simple_barrier_t *p) { + int val = 1; + while (val = __sync_val_compare_and_swap(p, val, 0)); +} diff --git a/Patchfield/jni/internal/simple_barrier.h b/Patchfield/jni/internal/simple_barrier.h new file mode 100644 index 0000000..595f589 --- /dev/null +++ b/Patchfield/jni/internal/simple_barrier.h @@ -0,0 +1,61 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * An atomic blocking boolean, designed to be robust when the underlying memory + * location is overwritten by buggy or malicious code. Integer return values + * are 0 for success, -1 for failure (due to timeouts), and -2 for errors (if + * the memory location has been tampered with). + */ + +#ifndef __SIMPLE_BARRIER_H__ +#define __SIMPLE_BARRIER_H__ + +#include + +/* + * Abstract data type representing a barrier. + */ +typedef int simple_barrier_t; + +/* + * If *p == 0, wait for another thread to invoke sb_wake(p). If *p == 1, return + * immediately. The deadline is either NULL (i.e., no deadline) or an absolute + * time measured with CLOCK_MONOTONIC. + * + * Use this function if multiple threads may be waiting on p. + */ +int sb_wait(simple_barrier_t *p, struct timespec *abstime); + +/* + * Like sb_wait, except it clears *p right after *p has become 1. + * + * Use this function if the current thread is the only thread waiting on p. + */ +int sb_wait_and_clear(simple_barrier_t *p, struct timespec *abstime); + +/* + * If *p == 0, set it to 1 and wake all threads waiting on p. If *p == 1, do + * nothing. + */ +int sb_wake(simple_barrier_t *p); + +/* + * Clears *p, regardless of whether any threads are waiting on p. + */ +void sb_clobber(simple_barrier_t *p); + +#endif diff --git a/Patchfield/jni/modules/javamodule.c b/Patchfield/jni/modules/javamodule.c new file mode 100644 index 0000000..08b942c --- /dev/null +++ b/Patchfield/jni/modules/javamodule.c @@ -0,0 +1,108 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "javamodule.h" + +#include "audio_module.h" +#include "utils/buffer_size_adapter.h" +#include "internal/simple_barrier.h" + +#include +#include +#include + +typedef struct { + buffer_size_adapter *bsa; + simple_barrier_t wake; + simple_barrier_t ready; + const float *input_buffer; + float *output_buffer; + int done; +} jmodule; + +static void process_jm(void *context, int sample_rate, int buffer_frames, + int input_channels, const float *input_buffer, + int output_channels, float *output_buffer) { + jmodule *jm = (jmodule *) context; + jm->input_buffer = input_buffer; + jm->output_buffer = output_buffer; + sb_wake(&jm->wake); + sb_wait_and_clear(&jm->ready, NULL); +} + +JNIEXPORT jlong JNICALL +Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_configure +(JNIEnv *env, jobject obj, jlong p, jint host_buffer_size, + jint user_buffer_size, jint input_channels, jint output_channels) { + jmodule *jm = malloc(sizeof(jmodule)); + if (jm) { + sb_clobber(&jm->wake); + sb_clobber(&jm->ready); + jm->input_buffer = NULL; + jm->output_buffer = NULL; + jm->done = 0; + jm->bsa = bsa_create((void *) p, host_buffer_size, user_buffer_size, + input_channels, output_channels, process_jm, jm); + if (!jm->bsa) { + free(jm); + jm = NULL; + } + } + return (jlong) jm; +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_release +(JNIEnv *env, jobject obj, jlong p) { + jmodule *jm = (jmodule *) p; + bsa_release(jm->bsa); + free(jm); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_fillInputBuffer +(JNIEnv *env, jobject obj, jlong p, jfloatArray buffer) { + jmodule *jm = (jmodule *) p; + sb_wait_and_clear(&jm->wake, NULL); + if (!jm->done) { + int n = (*env)->GetArrayLength(env, buffer); + float *b = (*env)->GetFloatArrayElements(env, buffer, NULL); + memcpy(b, jm->input_buffer, n * sizeof(float)); + (*env)->ReleaseFloatArrayElements(env, buffer, b, 0); + } +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_sendOutputBuffer +(JNIEnv *env, jobject obj, jlong p, jfloatArray buffer) { + jmodule *jm = (jmodule *) p; + if (!jm->done) { + int n = (*env)->GetArrayLength(env, buffer); + float *b = (*env)->GetFloatArrayElements(env, buffer, NULL); + memcpy(jm->output_buffer, b, n * sizeof(float)); + (*env)->ReleaseFloatArrayElements(env, buffer, b, 0); + sb_wake(&jm->ready); + } +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_signalThread +(JNIEnv *env, jobject obj, jlong p) { + jmodule *jm = (jmodule *) p; + jm->done = 1; + sb_wake(&jm->wake); +} + diff --git a/Patchfield/jni/modules/javamodule.h b/Patchfield/jni/modules/javamodule.h new file mode 100644 index 0000000..f4303a2 --- /dev/null +++ b/Patchfield/jni/modules/javamodule.h @@ -0,0 +1,53 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_noisepages_nettoyeur_patchfield_modules_JavaModule */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_modules_JavaModule +#define _Included_com_noisepages_nettoyeur_patchfield_modules_JavaModule +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_noisepages_nettoyeur_patchfield_modules_JavaModule + * Method: configure + * Signature: (JIIII)J + */ +JNIEXPORT jlong JNICALL Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_configure + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_modules_JavaModule + * Method: release + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_release + (JNIEnv *, jobject, jlong); + +/* + * Class: com_noisepages_nettoyeur_patchfield_modules_JavaModule + * Method: fillInputBuffer + * Signature: (J[F)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_fillInputBuffer + (JNIEnv *, jobject, jlong, jfloatArray); + +/* + * Class: com_noisepages_nettoyeur_patchfield_modules_JavaModule + * Method: sendOutputBuffer + * Signature: (J[F)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_sendOutputBuffer + (JNIEnv *, jobject, jlong, jfloatArray); + +/* + * Class: com_noisepages_nettoyeur_patchfield_modules_JavaModule + * Method: signalThread + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_modules_JavaModule_signalThread + (JNIEnv *, jobject, jlong); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Patchfield/jni/utils/buffer_size_adapter.c b/Patchfield/jni/utils/buffer_size_adapter.c new file mode 100644 index 0000000..0711464 --- /dev/null +++ b/Patchfield/jni/utils/buffer_size_adapter.c @@ -0,0 +1,195 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "buffer_size_adapter.h" + +#include +#include + +typedef struct { + int buffer_frames; + float *v; + int write_index; + int read_index; +} bsa_ring_buffer; + +struct _buffer_size_adapter { + int host_buffer_frames; + int user_buffer_frames; + void *user_context; + audio_module_process_t user_process; + bsa_ring_buffer *input_buffer; + bsa_ring_buffer *output_buffer; +}; + +static int lcm(int a, int b) { + int m = a; + while (m % b) { + m += a; + } + return m; +} + +static int frames_available(bsa_ring_buffer *rb) { + return + (rb->buffer_frames + rb->write_index - rb->read_index) % rb->buffer_frames; +} + +static void transfer_buffers(int nframes, int nchannels, + const float *source, int source_index, int source_frames, + float *sink, int sink_index, int sink_frames) { + int c, n, b; + while (nframes > 0) { + n = nframes; + b = source_frames - source_index % source_frames; + if (b < n) { + n = b; + } + b = sink_frames - sink_index % sink_frames; + if (b < n) { + n = b; + } + for (c = 0; c < nchannels; ++c) { + memcpy( + sink + (sink_index / sink_frames) * sink_frames * nchannels + + c * sink_frames + sink_index % sink_frames, + source + (source_index / source_frames) * source_frames * nchannels + + c * source_frames + source_index % source_frames, + n * sizeof(float)); + } + nframes -= n; + source_index += n; + sink_index += n; + } +} + +static void bsa_process(void *context, int sample_rate, int buffer_frames, + int input_channels, const float *input_buffer, + int output_channels, float *output_buffer) { + buffer_size_adapter *adapter = (buffer_size_adapter *) context; + if (adapter->host_buffer_frames != adapter->user_buffer_frames) { + bsa_ring_buffer *ib = adapter->input_buffer; + bsa_ring_buffer *ob = adapter->output_buffer; + transfer_buffers(buffer_frames, input_channels, + input_buffer, 0, buffer_frames, + ib->v, ib->write_index, adapter->user_buffer_frames); + ib->write_index = (ib->write_index + buffer_frames) % ib->buffer_frames; + while (frames_available(ib) >= adapter->user_buffer_frames) { + adapter->user_process(adapter->user_context, sample_rate, + adapter->user_buffer_frames, + input_channels, ib->v + ib->read_index * input_channels, + output_channels, ob->v + ob->write_index * output_channels); + ib->read_index = + (ib->read_index + adapter->user_buffer_frames) % ib->buffer_frames; + ob->write_index = + (ob->write_index + adapter->user_buffer_frames) % ob->buffer_frames; + } + if (frames_available(ob) >= buffer_frames) { + transfer_buffers(buffer_frames, output_channels, + ob->v, ob->read_index, adapter->user_buffer_frames, + output_buffer, 0, buffer_frames); + ob->read_index = (ob->read_index + buffer_frames) % ob->buffer_frames; + } else { + memset(output_buffer, 0, buffer_frames * output_channels * sizeof(float)); + } + } else { + adapter->user_process(adapter->user_context, sample_rate, buffer_frames, + input_channels, input_buffer, output_channels, output_buffer); + } +} + +static bsa_ring_buffer *create_buffer( + int host_buffer_frames, int user_buffer_frames, int nchannels) { + bsa_ring_buffer *rb = malloc(sizeof(bsa_ring_buffer)); + if (rb) { + rb->read_index = 0; + rb->write_index = 0; + int m = lcm(host_buffer_frames, user_buffer_frames); + if (m == host_buffer_frames || m == user_buffer_frames) { + m *= 2; + } + rb->buffer_frames = m; + rb->v = calloc(rb->buffer_frames * nchannels, sizeof(float)); + if (!rb->v) { + free(rb); + rb = NULL; + } + } + return rb; +} + +static void release_buffer(bsa_ring_buffer *rb) { + if (rb) { + free(rb->v); + } + free(rb); +} + +buffer_size_adapter *bsa_create( + void *handle, int host_buffer_frames, int user_buffer_frames, + int input_channels, int output_channels, + audio_module_process_t user_process, void *user_context) { + buffer_size_adapter *adapter = malloc(sizeof(buffer_size_adapter)); + if (adapter) { + adapter->host_buffer_frames = host_buffer_frames; + adapter->user_buffer_frames = user_buffer_frames; + adapter->user_process = user_process; + adapter->user_context = user_context; + if (host_buffer_frames != user_buffer_frames) { + adapter->input_buffer = + create_buffer(host_buffer_frames, user_buffer_frames, input_channels); + if (adapter->input_buffer) { + adapter->output_buffer = create_buffer(host_buffer_frames, + user_buffer_frames, output_channels); + if (adapter->output_buffer) { + // Optimizing initial indices according to St\'ephane Letz, "Callback + // adaptation techniques" + // (see www.grame.fr/ressources/publications/CallbackAdaptation.pdf). + int r, w = 0; + int m = lcm(host_buffer_frames, user_buffer_frames); + int dmax = 0; + for (r = 0; r < m; r += host_buffer_frames) { + for (; w < r; w += user_buffer_frames); + int d = w - r; + if (d > dmax) { + dmax = d; + adapter->output_buffer->read_index = r; + adapter->output_buffer->write_index = w; + } + } + } else { + release_buffer(adapter->input_buffer); + free(adapter); + adapter = NULL; + } + } else { + free(adapter); + adapter = NULL; + } + } else { + adapter->input_buffer = NULL; + adapter->output_buffer = NULL; + } + } + am_configure(handle, bsa_process, adapter); + return adapter; +} + +void bsa_release(buffer_size_adapter *adapter) { + release_buffer(adapter->input_buffer); + release_buffer(adapter->output_buffer); + free(adapter); +} diff --git a/Patchfield/jni/utils/buffer_size_adapter.h b/Patchfield/jni/utils/buffer_size_adapter.h new file mode 100644 index 0000000..5c9d0a3 --- /dev/null +++ b/Patchfield/jni/utils/buffer_size_adapter.h @@ -0,0 +1,45 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * An adapter that allows audio modules to operate at buffer sizes that differ + * from the buffer size of the Patchfield service. + */ +#ifndef __BUFFER_SIZE_ADAPTER_H__ +#define __BUFFER_SIZE_ADAPTER_H__ + +#include "audio_module.h" + +/* + * Abstract data type representing a buffer size adapter. + */ +typedef struct _buffer_size_adapter buffer_size_adapter; + +/* + * Constructor; calls am_configure internally, and so audio modules using + * this utility will not need to call am_configure themselves. + */ +buffer_size_adapter *bsa_create( + void *handle, int host_buffer_frames, int user_buffer_frames, + int input_channels, int output_channels, + audio_module_process_t user_process, void *user_context); + +/* + * Releases all resources associated with this adapter. + */ +void bsa_release(buffer_size_adapter *adapter); + +#endif diff --git a/Patchfield/libs/android-support-v4.jar b/Patchfield/libs/android-support-v4.jar new file mode 100644 index 0000000..428bdbc Binary files /dev/null and b/Patchfield/libs/android-support-v4.jar differ diff --git a/Patchfield/proguard-project.txt b/Patchfield/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/Patchfield/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/Patchfield/project.properties b/Patchfield/project.properties new file mode 100644 index 0000000..484dab0 --- /dev/null +++ b/Patchfield/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/Patchfield/res/drawable-hdpi/.!43333!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!43333!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/.!43353!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!43353!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/.!43416!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!43416!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/.!43465!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!43465!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/.!43520!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!43520!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/.!44022!ic_launcher.png b/Patchfield/res/drawable-hdpi/.!44022!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-hdpi/ic_launcher.png b/Patchfield/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..776f508 Binary files /dev/null and b/Patchfield/res/drawable-hdpi/ic_launcher.png differ diff --git a/Patchfield/res/drawable-mdpi/.!43417!emo_im_happy.png b/Patchfield/res/drawable-mdpi/.!43417!emo_im_happy.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43418!ic_launcher.png b/Patchfield/res/drawable-mdpi/.!43418!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43419!perm_group_audio_settings.png b/Patchfield/res/drawable-mdpi/.!43419!perm_group_audio_settings.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43420!perm_group_microphone.png b/Patchfield/res/drawable-mdpi/.!43420!perm_group_microphone.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43470!emo_im_happy.png b/Patchfield/res/drawable-mdpi/.!43470!emo_im_happy.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43471!ic_launcher.png b/Patchfield/res/drawable-mdpi/.!43471!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43472!perm_group_audio_settings.png b/Patchfield/res/drawable-mdpi/.!43472!perm_group_audio_settings.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43473!perm_group_microphone.png b/Patchfield/res/drawable-mdpi/.!43473!perm_group_microphone.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43529!emo_im_happy.png b/Patchfield/res/drawable-mdpi/.!43529!emo_im_happy.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43530!ic_launcher.png b/Patchfield/res/drawable-mdpi/.!43530!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43531!perm_group_audio_settings.png b/Patchfield/res/drawable-mdpi/.!43531!perm_group_audio_settings.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!43532!perm_group_microphone.png b/Patchfield/res/drawable-mdpi/.!43532!perm_group_microphone.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!44035!emo_im_happy.png b/Patchfield/res/drawable-mdpi/.!44035!emo_im_happy.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!44036!ic_launcher.png b/Patchfield/res/drawable-mdpi/.!44036!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!44037!perm_group_audio_settings.png b/Patchfield/res/drawable-mdpi/.!44037!perm_group_audio_settings.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/.!44038!perm_group_microphone.png b/Patchfield/res/drawable-mdpi/.!44038!perm_group_microphone.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-mdpi/emo_im_happy.png b/Patchfield/res/drawable-mdpi/emo_im_happy.png new file mode 100644 index 0000000..a324720 Binary files /dev/null and b/Patchfield/res/drawable-mdpi/emo_im_happy.png differ diff --git a/Patchfield/res/drawable-mdpi/ic_launcher.png b/Patchfield/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..c432b83 Binary files /dev/null and b/Patchfield/res/drawable-mdpi/ic_launcher.png differ diff --git a/Patchfield/res/drawable-mdpi/perm_group_audio_settings.png b/Patchfield/res/drawable-mdpi/perm_group_audio_settings.png new file mode 100644 index 0000000..f2f461b Binary files /dev/null and b/Patchfield/res/drawable-mdpi/perm_group_audio_settings.png differ diff --git a/Patchfield/res/drawable-mdpi/perm_group_microphone.png b/Patchfield/res/drawable-mdpi/perm_group_microphone.png new file mode 100644 index 0000000..9bab315 Binary files /dev/null and b/Patchfield/res/drawable-mdpi/perm_group_microphone.png differ diff --git a/Patchfield/res/drawable-xhdpi/.!43421!ic_launcher.png b/Patchfield/res/drawable-xhdpi/.!43421!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-xhdpi/.!43475!ic_launcher.png b/Patchfield/res/drawable-xhdpi/.!43475!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-xhdpi/.!43535!ic_launcher.png b/Patchfield/res/drawable-xhdpi/.!43535!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-xhdpi/.!44042!ic_launcher.png b/Patchfield/res/drawable-xhdpi/.!44042!ic_launcher.png new file mode 100644 index 0000000..e69de29 diff --git a/Patchfield/res/drawable-xhdpi/ic_launcher.png b/Patchfield/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..7664569 Binary files /dev/null and b/Patchfield/res/drawable-xhdpi/ic_launcher.png differ diff --git a/Patchfield/res/values-v11/styles.xml b/Patchfield/res/values-v11/styles.xml new file mode 100644 index 0000000..3c02242 --- /dev/null +++ b/Patchfield/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/Patchfield/res/values-v14/styles.xml b/Patchfield/res/values-v14/styles.xml new file mode 100644 index 0000000..a91fd03 --- /dev/null +++ b/Patchfield/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Patchfield/res/values/strings.xml b/Patchfield/res/values/strings.xml new file mode 100644 index 0000000..045b0a4 --- /dev/null +++ b/Patchfield/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Patchfield + + diff --git a/Patchfield/res/values/styles.xml b/Patchfield/res/values/styles.xml new file mode 100644 index 0000000..6ce89c7 --- /dev/null +++ b/Patchfield/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/AudioModule.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/AudioModule.java new file mode 100644 index 0000000..5d16dd7 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/AudioModule.java @@ -0,0 +1,232 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield; + +import android.app.Notification; +import android.os.RemoteException; +import android.util.Log; + +import com.noisepages.nettoyeur.patchfield.internal.SharedMemoryUtils; + +/** + * Abstract base class for Patchfield audio modules. Subclasses must implement + * methods for creating and releasing audio modules; these implementations will + * involve native code using the native audio_module library in Patchfield/jni. + * See the LowpassSample project for a representative audio module + * implementation. + * + * The Patchfield service operates at the native sample rate and buffer size of + * the device. This means that audio modules must operate at the native sample + * rate and buffer size as well. Native sample rates of 44100Hz and 48000Hz are + * common, and so audio modules must support both. Moreover, audio modules must + * be prepared to work with arbitrary buffer sizes. In particular, they cannot + * assume that the buffer size is a power of two. Multiples of three, such as + * 144, 192, and 384, have been seen in the wild. + * + * If an app is unable to run at the native buffer size, the buffer size adapter + * utility in Patchfield/jni/utils/buffer_size_adapter.{h,c} can be used. For an + * example of the buffer size adapter in action, see the PatchfieldPd library + * project. + */ +public abstract class AudioModule { + + static { + System.loadLibrary("audiomodulejava"); + } + + private static final String TAG = "AudioModule"; + + private String name = null; + private int token = -1; + private long handle = 0; + + private final Notification notification; + + private class FdReceiverThread extends Thread { + @Override + public void run() { + token = SharedMemoryUtils.receiveSharedMemoryFileDescriptor(); + Log.i(TAG, "fd: " + token); + } + } + + /** + * Constructor. + * + * @param notification + * Notification to be passed to the Patchfield service, so that the service + * can associate an audio module with an app. May be null. + */ + protected AudioModule(Notification notification) { + this.notification = notification; + } + + /** + * This method takes care of the elaborate choreography that it takes to set + * up an audio module and to connect it to its representation in the + * Patchfield service. + * + * Specifically, it sets up the shared memory between the local module and + * the Patchfield service, creates a new module in the service, and connects + * it to the local module. + * + * A module can only be configured once. If it times out, it cannot be + * reinstated and should be released. + * + * @param patchfield + * Stub for communicating with the Patchfield service. + * @param name + * Name of the new audio module in Patchfield. + * @return 0 on success, a negative error on failure; use + * {@link PatchfieldException} to interpret the return value. + * @throws RemoteException + */ + public int configure(IPatchfieldService patchfield, String name) + throws RemoteException { + int version = patchfield.getProtocolVersion(); + if (version != getProtocolVersion()) { + return PatchfieldException.PROTOCOL_VERSION_MISMATCH; + } + if (this.handle != 0) { + throw new IllegalStateException("Module is already configured."); + } + FdReceiverThread t = new FdReceiverThread(); + t.start(); + while (patchfield.sendSharedMemoryFileDescriptor() != 0 && t.isAlive()) { + try { + Thread.sleep(10); // Wait for receiver thread to spin up. + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + try { + t.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (token < 0) { + return token; + } + int index = patchfield.createModule(name, getInputChannels(), + getOutputChannels(), notification); + if (index < 0) { + SharedMemoryUtils.closeSharedMemoryFileDescriptor(token); + return index; + } + handle = createRunner(version, token, index); + if (handle == 0) { + patchfield.deleteModule(name); + SharedMemoryUtils.closeSharedMemoryFileDescriptor(token); + return PatchfieldException.FAILURE; + } + if (!configure(name, handle, patchfield.getSampleRate(), + patchfield.getBufferSize())) { + release(handle); + patchfield.deleteModule(name); + SharedMemoryUtils.closeSharedMemoryFileDescriptor(token); + return PatchfieldException.FAILURE; + } + this.name = name; + return PatchfieldException.SUCCESS; + } + + /** + * Releases all resources associated with this module and deletes its + * representation in the Patchfield service. + * + * @param patchfield + * Stub for communicating with the Patchfield service. + * @throws RemoteException + */ + public void release(IPatchfieldService patchfield) throws RemoteException { + if (handle != 0) { + patchfield.deleteModule(name); + release(handle); + release(); + SharedMemoryUtils.closeSharedMemoryFileDescriptor(token); + name = null; + handle = 0; + token = -1; + } else { + Log.w(TAG, "Not configured; nothing to release."); + } + } + + /** + * @return The name of the module if configured, null if it isn't. + */ + public String getName() { + return name; + } + + /** + * @return The notification associated with this module. + */ + protected Notification getNotification() { + return notification; + } + + /** + * @return True if the module has timed out. A module that has timed out + * cannot be reinstated and must be released. + */ + public final boolean hasTimedOut() { + return handle != 0 && hasTimedOut(handle); + } + + /** + * @return The number of input channels of this module. + */ + public abstract int getInputChannels(); + + /** + * @return The number of output channels of this module. + */ + public abstract int getOutputChannels(); + + /** + * This method is called by the public configure method. It is responsible + * to setting up the native components of an audio module implementation, + * such as the audio processing function and any data structures that make + * up the processing context. + * + * @param name + * @param handle + * Opaque handle to the internal data structure representing an + * audio module. + * @param sampleRate + * @param bufferSize + * @return True on success + */ + protected abstract boolean configure(String name, long handle, + int sampleRate, int bufferSize); + + /** + * Releases any resources held by the audio module, such as memory allocated + * for the processing context. + */ + protected abstract void release(); + + /** + * @return The Patchfield protocol version; mostly for internal use. + */ + public static native int getProtocolVersion(); + + private native long createRunner(int version, int token, int index); + + private native void release(long handle); + + private native boolean hasTimedOut(long handle); +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldClient.aidl b/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldClient.aidl new file mode 100644 index 0000000..454df7b --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldClient.aidl @@ -0,0 +1,36 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield; + +import android.app.Notification; + +/** + * Patchfield client interface for handling notifications when the state of the + * patchfield changes. + */ +oneway interface IPatchfieldClient { + + void onModuleCreated(String name, int inputChannels, int outputChannels, in Notification notification); + void onModuleDeleted(String name); + + void onModuleActivated(String name); + void onModuleDeactivated(String name); + + void onPortsConnected(String source, int sourcePort, String sink, int sinkPort); + void onPortsDisconnected(String source, int sourcePort, String sink, int sinkPort); + + void onStart(); + void onStop(); +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldService.aidl b/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldService.aidl new file mode 100644 index 0000000..9696526 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/IPatchfieldService.aidl @@ -0,0 +1,176 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield; + +import com.noisepages.nettoyeur.patchfield.IPatchfieldClient; + +import android.app.Notification; + +import java.util.List; + +/** + * Patchfield service interface, implemented by {@link Patchfield}. + */ +interface IPatchfieldService { + + /** + * Registers a Patchfield client. Note that a client is not an audio module. + * Rather, clients are Java objects that will be notified when the state + * of the patchfield changes. + */ + void registerClient(IPatchfieldClient client); + + /** + * Unregisters a Patchfield client. + */ + void unregisterClient(IPatchfieldClient client); + + /** + * Starts audio rendering. + * + * @return 0 on success, or a negative error code on failure. + */ + int start(); + + /** + * Stops audio rendering. + */ + void stop(); + + /** + * Activates the given audio module. + * + * @return 0 on success, or a negative error code on failure. + */ + int activateModule(String module); + + /** + * Deactivates the given audio module. + * + * @return 0 on success, or a negative error code on failure. + */ + int deactivateModule(String module); + + /** + * Connects the given source port to the given sink port. + * + * @return 0 on success, or a negative error code on failure. + */ + int connectPorts(String source, int sourcePort, String sink, int sinkPort); + + /** + * Disconnects the given source port from the given sink port. + * + * @return 0 on success, or a negative error code on failure. + */ + int disconnectPorts(String source, int sourcePort, String sink, int sinkPort); + + /** + * @return True if the Patchfield is currently rendering audio. + */ + boolean isRunning(); + + /** + * @return The list of currently registered audio modules. + */ + List getModules(); + + /** + * @return The number of input channels of the given module, or a negative error + * code if the module doesn't exist. + */ + int getInputChannels(String module); + + /** + * @return The number of output channels of the given module, or a negative error + * code if the module doesn't exist. + */ + int getOutputChannels(String module); + + /** + * @return The notification associated with this module, or null if the module + * doesn't exist. + */ + Notification getNotification(String module); + + /** + * @return True if the given module is currently active. + */ + boolean isActive(String module); + + /** + * @return True if the source port is directly connected to the sink port. + */ + boolean isConnected(String source, int sourcePort, String sink, int sinkPort); + + /** + * @return True if the input of of the given sink depends on the output of the given source, + * directly or indirectly. In other words, this method returns true if the source must have + * produced its output before the sink can run. + */ + boolean isDependent(String sink, String source); + + /** + * @return The sample rate in Hz at which the Patchfield operates. This value is determined + * by the hardware and cannot be changed. Audio modules should be able to operate at 44.1kHz + * as well as 48kHz. + */ + int getSampleRate(); + + /** + * @return The buffer size in frames at which the Patchfield operates. This value is determined + * by the hardware and cannot be changed. Audio modules should make no assumptions about the + * buffer size. In particular, it will not be a power of two on many devices. + */ + int getBufferSize(); + + /** + * Creates a new audio module in the Patchfield service; for internal use mostly, to be called by + * the configure method of {@link AudioModule}. + * + * @return The index of the new module on success, or a negative error code on failure. + */ + int createModule(String module, int inputChannels, int outputChannels, in Notification notification); + + /** + * Deletes an audio module from the Patchfield service; for internal use mostly, to be called by + * the release method of {@link AudioModule}. + * + * @return 0 on success, or a negative error code on failure. + */ + int deleteModule(String module); + + /** + * @return The native protocol version; for internal use only. + */ + int getProtocolVersion(); + + /** + * Passes the ashmem file descriptor through a Unix domain socket; for internal use only. + * + * @return 0 on success, or a negative error code on failure. + */ + int sendSharedMemoryFileDescriptor(); + + /** + * Delegates to the corresponding Service method. + */ + void startForeground(int id, in Notification notification); + + /** + * Delegates to the corresponding Service method. + */ + void stopForeground(boolean removeNotification); +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/Patchfield.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/Patchfield.java new file mode 100644 index 0000000..7fe1259 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/Patchfield.java @@ -0,0 +1,487 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import android.app.Notification; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; + +import com.noisepages.nettoyeur.patchfield.internal.OpenSlParams; + +/** + * The Java part of the Patchfield service implementation. This is mostly boilerplate; the action is + * in the native code, Patchfield/jni/patchfield.c. + */ +public class Patchfield implements IPatchfieldService { + + private static final String TAG = "Patchfield"; + + static { + System.loadLibrary("patchfield"); + } + + private final OpenSlParams params; + private long streamPtr; + private final Map modules = new LinkedHashMap(); + private final Map notifications = new LinkedHashMap(); + private final RemoteCallbackList clients = + new RemoteCallbackList(); + + public Patchfield(Context context, int inputChannels, int outputChannels) throws IOException { + params = OpenSlParams.createInstance(context); + streamPtr = + createInstance(params.getSampleRate(), params.getBufferSize(), inputChannels, + outputChannels); + if (streamPtr == 0) { + throw new IOException("Unable to open opensl_stream."); + } + Log.i(TAG, "Created stream with ptr " + streamPtr); + modules.put("system_in", 0); + modules.put("system_out", 1); + Notification micNotification = new Notification.Builder(context) + .setSmallIcon(R.drawable.perm_group_microphone) + .setContentTitle("Microphones") + .build(); + notifications.put("system_in", micNotification); + Notification speakerNotification = new Notification.Builder(context) + .setSmallIcon(R.drawable.perm_group_audio_settings) + .setContentTitle("Speakers") + .build(); + notifications.put("system_out", speakerNotification); + } + + public synchronized void release() { + if (streamPtr != 0) { + releaseInstance(streamPtr); + streamPtr = 0; + clients.kill(); + } + } + + @Override + public synchronized void registerClient(IPatchfieldClient client) throws RemoteException { + clients.register(client); + } + + @Override + public synchronized void unregisterClient(IPatchfieldClient client) throws RemoteException { + clients.unregister(client); + } + + @Override + public synchronized int getSampleRate() { + return params.getSampleRate(); + } + + @Override + public synchronized int getBufferSize() { + return params.getBufferSize(); + } + + @Override + public synchronized int sendSharedMemoryFileDescriptor() { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + return PatchfieldException.successOrFailure(sendSharedMemoryFileDescriptor(streamPtr)); + } + + @Override + public synchronized int start() { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + int result = start(streamPtr); + if (result == 0) { + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onStart(); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return PatchfieldException.successOrFailure(result); + } + + @Override + public synchronized void stop() { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + stop(streamPtr); + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onStop(); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + + @Override + public synchronized boolean isRunning() { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + return isRunning(streamPtr); + } + + @Override + public synchronized int createModule(String module, int inputChannels, int outputChannels, + Notification notification) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (inputChannels < 0 || outputChannels < 0 || (inputChannels == 0 && outputChannels == 0)) { + return PatchfieldException.INVALID_PARAMETERS; + } + if (modules.containsKey(module)) { + return PatchfieldException.MODULE_NAME_TAKEN; + } + int index = createModule(streamPtr, inputChannels, outputChannels); + if (index >= 0) { + modules.put(module, index); + notifications.put(module, notification); + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i) + .onModuleCreated(module, inputChannels, outputChannels, notification); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return index; + } + + @Override + public synchronized int deleteModule(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(module)) { + return PatchfieldException.NO_SUCH_MODULE; + } + int result = deleteModule(streamPtr, modules.get(module)); + if (result == 0) { + modules.remove(module); + Notification notification = notifications.remove(module); + if (notification != null && notification.deleteIntent != null) { + try { + notification.deleteIntent.send(); + } catch (CanceledException e) { + // Do nothing. + } + } + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onModuleDeleted(module); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return result; + } + + @Override + public synchronized int connectPorts(String source, int sourcePort, String sink, int sinkPort) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(source)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (!modules.containsKey(sink)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (sourcePort < 0 || sourcePort >= getOutputChannels(source)) { + return PatchfieldException.PORT_OUT_OF_RANGE; + } + if (sinkPort < 0 || sinkPort >= getInputChannels(sink)) { + return PatchfieldException.PORT_OUT_OF_RANGE; + } + if (isConnected(source, sourcePort, sink, sinkPort)) { + return PatchfieldException.SUCCESS; + } + if (isDependent(source, sink)) { + return PatchfieldException.CYCLIC_DEPENDENCY; + } + int result = + connectPorts(streamPtr, modules.get(source), sourcePort, modules.get(sink), sinkPort); + if (result == 0) { + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onPortsConnected(source, sourcePort, sink, sinkPort); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return result; + } + + @Override + public synchronized int disconnectPorts(String source, int sourcePort, String sink, int sinkPort) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(source)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (!modules.containsKey(sink)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (sourcePort < 0 || sourcePort >= getOutputChannels(source)) { + return PatchfieldException.PORT_OUT_OF_RANGE; + } + if (sinkPort < 0 || sinkPort >= getInputChannels(sink)) { + return PatchfieldException.PORT_OUT_OF_RANGE; + } + if (!isConnected(source, sourcePort, sink, sinkPort)) { + return 0; + } + int result = + disconnectPorts(streamPtr, modules.get(source), sourcePort, modules.get(sink), sinkPort); + if (result == 0) { + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onPortsDisconnected(source, sourcePort, sink, sinkPort); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return result; + } + + @Override + public synchronized boolean isConnected(String source, int sourcePort, String sink, int sinkPort) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + return modules.containsKey(source) && modules.containsKey(sink) + && isConnected(streamPtr, modules.get(source), sourcePort, modules.get(sink), sinkPort); + } + + @Override + public synchronized boolean isDependent(String sink, String source) { + List dependents = getDependents(source); + return dependents.contains(sink); + } + + @Override + public synchronized List getModules() { + return Collections.unmodifiableList(new ArrayList(modules.keySet())); + } + + @Override + public synchronized int getInputChannels(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(module)) { + return PatchfieldException.NO_SUCH_MODULE; + } + return getInputChannels(streamPtr, modules.get(module)); + } + + @Override + public synchronized int getOutputChannels(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(module)) { + return PatchfieldException.NO_SUCH_MODULE; + } + return getOutputChannels(streamPtr, modules.get(module)); + } + + @Override + public synchronized Notification getNotification(String module) { + return notifications.get(module); + } + + @Override + public synchronized boolean isActive(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + return modules.containsKey(module) && isActive(streamPtr, modules.get(module)); + } + + @Override + public synchronized int activateModule(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(module)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (isActive(module)) { + return 0; + } + int result = activateModule(streamPtr, modules.get(module)); + if (result == 0) { + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onModuleActivated(module); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return PatchfieldException.successOrFailure(result); + } + + @Override + public synchronized int deactivateModule(String module) { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + if (!modules.containsKey(module)) { + return PatchfieldException.NO_SUCH_MODULE; + } + if (!isActive(module)) { + return 0; + } + int result = deactivateModule(streamPtr, modules.get(module)); + if (result == 0) { + int i = clients.beginBroadcast(); + while (--i >= 0) { + try { + clients.getBroadcastItem(i).onModuleDeactivated(module); + } catch (RemoteException e) { + // Do nothing; RemoteCallbackList will take care of the cleanup. + } + } + clients.finishBroadcast(); + } + return PatchfieldException.successOrFailure(result); + } + + @Override + public int getProtocolVersion() throws RemoteException { + if (streamPtr == 0) { + throw new IllegalStateException("Stream closed."); + } + return getProtocolVersion(streamPtr); + } + + private native long createInstance(int sampleRate, int bufferSize, int inputChannels, + int outputChannels); + + private native int sendSharedMemoryFileDescriptor(long streamPtr); + + private native void releaseInstance(long streamPtr); + + private native int createModule(long streamPtr, int inputChannels, int outputChannels); + + private native int deleteModule(long streamPtr, int index); + + private native int connectPorts(long streamPtr, int sourceIndex, int sourcePort, int sinkIndex, + int sinkPort); + + private native int disconnectPorts(long streamPtr, int sourceIndex, int sourcePort, + int sinkIndex, int sinkPort); + + private native int activateModule(long streamPtr, int index); + + private native int deactivateModule(long streamPtr, int index); + + private native int start(long streamPtr); + + private native void stop(long streamPtr); + + private native boolean isActive(long streamPtr, int index); + + private native boolean isRunning(long streamPtr); + + private native boolean isConnected(long streamPtr, int sourceIndex, int sourcePort, + int sinkIndex, int sinkPort); + + private native int getInputChannels(long streamPtr, int index); + + private native int getOutputChannels(long streamPtr, int index); + + private native int getProtocolVersion(long streamPtr); + + @Override + public IBinder asBinder() { + throw new UnsupportedOperationException("Not implemented for local patchfield."); + } + + @Override + public void startForeground(int id, Notification notification) { + throw new UnsupportedOperationException("Not implemented for local patchfield."); + } + + @Override + public void stopForeground(boolean removeNotification) { + throw new UnsupportedOperationException("Not implemented for local patchfield."); + } + + private List getDependents(String source) { + List dependents = new ArrayList(); + collectDependents(source, dependents); + return dependents; + } + + private void collectDependents(String source, List dependents) { + dependents.add(source); + for (String sink : modules.keySet()) { + if (!dependents.contains(sink) && isDirectDependent(sink, source)) { + collectDependents(sink, dependents); + } + } + } + + private boolean isDirectDependent(String sink, String source) { + for (int i = 0; i < getOutputChannels(source); ++i) { + for (int j = 0; j < getInputChannels(sink); ++j) { + if (isConnected(source, i, sink, j)) { + return true; + } + } + } + return false; + } +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/PatchfieldException.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/PatchfieldException.java new file mode 100644 index 0000000..adc63e5 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/PatchfieldException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield; + +/** + * Checked exception for Java-style handling of error codes from Patchfield. The {@link Patchfield} + * class itself doesn't throw exceptions but returns C-style error codes because exceptions can't be + * thrown via AIDL. If, however, you prefer to use exceptions in Java, you can convert error codes + * by wrapping calls to Patchfield in the throwOnError method below. + */ +public class PatchfieldException extends Exception { + + // WARNING: Do not change these constants without updating references in patchfield.c. + public static final int SUCCESS = 0; + public static final int FAILURE = -1; + public static final int INVALID_PARAMETERS = -2; + public static final int NO_SUCH_MODULE = -3; + public static final int MODULE_NAME_TAKEN = -4; + public static final int TOO_MANY_MODULES = -5; + public static final int PORT_OUT_OF_RANGE = -6; + public static final int TOO_MANY_CONNECTIONS = -7; + public static final int CYCLIC_DEPENDENCY = -8; + public static final int OUT_OF_BUFFER_SPACE = -9; + public static final int PROTOCOL_VERSION_MISMATCH = -10; + + private static final long serialVersionUID = 1L; + private final int code; + + public PatchfieldException(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static int successOrFailure(int n) { + return (n >= 0) ? SUCCESS : FAILURE; + } + + public static int throwOnError(int code) throws PatchfieldException { + if (code < 0) { + throw new PatchfieldException(code); + } + return code; + } +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/OpenSlParams.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/OpenSlParams.java new file mode 100644 index 0000000..616e1ac --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/OpenSlParams.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.internal; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; +import android.util.Log; + +/** + * This class illustrates how to query OpenSL config parameters on Jelly Bean MR1 while maintaining + * backward compatibility with older versions of Android. The trick is to place the new API calls in + * an inner class that will only be loaded if we're running on JB MR1 or later. + */ +public abstract class OpenSlParams { + + /** + * @return The recommended sample rate in Hz. + */ + public abstract int getSampleRate(); + + /** + * @return The recommended buffer size in frames. + */ + public abstract int getBufferSize(); + + /** + * @param context, e.g., the current activity. + * @return OpenSlParams instance for the given context. + */ + public static OpenSlParams createInstance(Context context) { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + ? new JellyBeanMr1OpenSlParams(context) + : new DefaultOpenSlParams(); + } + + private OpenSlParams() { + // Not meant to be instantiated except here. + } + + // Implementation for Jelly Bean MR1 or later. + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private static class JellyBeanMr1OpenSlParams extends OpenSlParams { + + private final int sampleRate; + private final int bufferSize; + + private JellyBeanMr1OpenSlParams(Context context) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + // Provide default values in case config lookup fails. + int sr = 44100; + int bs = 64; + try { + // If possible, query the native sample rate and buffer size. + sr = Integer.parseInt(am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)); + bs = Integer.parseInt(am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)); + } catch (NumberFormatException e) { + Log.w(getClass().getName(), "Failed to read native OpenSL config: " + e); + } + sampleRate = sr; + bufferSize = bs; + } + + @Override + public int getSampleRate() { + return sampleRate; + } + + @Override + public int getBufferSize() { + return bufferSize; + } + }; + + // Default factory for Jelly Bean or older. + private static class DefaultOpenSlParams extends OpenSlParams { + @Override + public int getSampleRate() { + return 44100; + } + + @Override + public int getBufferSize() { + return 64; + } + }; +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/SharedMemoryUtils.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/SharedMemoryUtils.java new file mode 100644 index 0000000..9d7ec58 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/internal/SharedMemoryUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.internal; + +/** + * Utilities for receiving file descriptors from the Patchfield service. + */ +public class SharedMemoryUtils { + + static { + System.loadLibrary("shared_memory_utils"); + } + + public static native int receiveSharedMemoryFileDescriptor(); + + public static native int closeSharedMemoryFileDescriptor(int fd); +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/modules/JavaModule.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/modules/JavaModule.java new file mode 100644 index 0000000..4468e76 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/modules/JavaModule.java @@ -0,0 +1,136 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.modules; + +import android.app.Notification; + +import com.noisepages.nettoyeur.patchfield.AudioModule; + +/** + * An audio module subclass whose audio processing callback is to be implemented + * in Java (as opposed to native code). + * + * THIS IS NOT THE RECOMMENDED WAY TO USE PATCHFIELD. The Java processing callback + * cannot be invoked on a real-time thread and Java code is prone to garbage + * collection breaks. This means that instances of this class run a higher risk + * of missing their deadlines and causing dropouts than audio modules that do + * their processing natively. Use this class for quick-and-dirty prototypes, + * but not for any serious applications that require glitch-free performance. + */ +public abstract class JavaModule extends AudioModule { + + static { + System.loadLibrary("javamodule"); + } + + private final int inputChannels; + private final int outputChannels; + private final int bufferSize; + private float[] inputBuffer = null; + private float[] outputBuffer = null; + + private long ptr = 0; + private int sampleRate; + private Thread renderThread = null; + + private final Runnable processor = new Runnable() { + @Override + public void run() { + while (!hasTimedOut()) { + fillInputBuffer(ptr, inputBuffer); + if (Thread.interrupted()) { + break; + } + process(sampleRate, bufferSize, inputChannels, inputBuffer, outputChannels, outputBuffer); + sendOutputBuffer(ptr, outputBuffer); + } + } + }; + + /** + * Constructor. For best performance, make the buffer size equal to Patchfield.getBufferSize(). When + * this is not an option, choose a smallish buffer size if possible (64 is a good value). Large + * buffers will not improve stability. In fact, large buffers may increase the risk of dropouts + * because the patchfield runs a fixed buffer size internally; mismatched buffer sizes place an + * uneven load on the internal processing callback. + */ + public JavaModule(int bufferSize, int inputChannels, int outputChannels, Notification notification) { + super(notification); + this.bufferSize = bufferSize; + this.inputChannels = inputChannels; + this.outputChannels = outputChannels; + inputBuffer = new float[bufferSize * inputChannels]; + outputBuffer = new float[bufferSize * outputChannels]; + } + + /** + * Audio processing callback. The size of each buffer is the buffer size (in frames) times the + * number of channels. Buffers are non-interleaved. + */ + protected abstract void process(int sampleRate, int bufferSize, int inputChannels, + float[] inputBuffer, int outputChannels, float[] outputBuffer); + + @Override + public int getInputChannels() { + return inputChannels; + } + + @Override + public int getOutputChannels() { + return outputChannels; + } + + @Override + protected boolean configure(String name, long handle, int sampleRate, int hostBufferSize) { + this.sampleRate = sampleRate; + ptr = configure(handle, hostBufferSize, bufferSize, inputChannels, outputChannels); + if (ptr != 0) { + renderThread = new Thread(processor); + renderThread.start(); + return true; + } else { + return false; + } + } + + @Override + protected void release() { + if (renderThread != null) { + renderThread.interrupt(); + signalThread(ptr); + try { + renderThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + renderThread = null; + } + if (ptr != 0) { + release(ptr); + ptr = 0; + } + } + + private native long configure(long handle, int hostBufferSize, int bufferSize, int inputChannels, + int outputChannels); + + private native void release(long ptr); + + private native void fillInputBuffer(long ptr, float[] inputBuffer); + + private native void sendOutputBuffer(long ptr, float[] outputBuffer); + + private native void signalThread(long ptr); +} diff --git a/Patchfield/src/com/noisepages/nettoyeur/patchfield/service/PatchfieldService.java b/Patchfield/src/com/noisepages/nettoyeur/patchfield/service/PatchfieldService.java new file mode 100644 index 0000000..2af4554 --- /dev/null +++ b/Patchfield/src/com/noisepages/nettoyeur/patchfield/service/PatchfieldService.java @@ -0,0 +1,191 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.service; + +import java.io.IOException; +import java.util.List; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; + +import com.noisepages.nettoyeur.patchfield.IPatchfieldClient; +import com.noisepages.nettoyeur.patchfield.IPatchfieldService; +import com.noisepages.nettoyeur.patchfield.Patchfield; + +/** + * Boilerplate for turning {@link Patchfield} into an Android service. + */ +public class PatchfieldService extends Service { + + private Patchfield patchfield = null; + + private final IPatchfieldService.Stub binder = new IPatchfieldService.Stub() { + + @Override + public int sendSharedMemoryFileDescriptor() { + return patchfield.sendSharedMemoryFileDescriptor(); + } + + @Override + public void unregisterClient(IPatchfieldClient client) throws RemoteException { + patchfield.unregisterClient(client); + } + + @Override + public void registerClient(IPatchfieldClient client) throws RemoteException { + patchfield.registerClient(client); + } + + @Override + public int getSampleRate() { + return patchfield.getSampleRate(); + } + + @Override + public int getBufferSize() { + return patchfield.getBufferSize(); + } + + @Override + public int deleteModule(String module) throws RemoteException { + return patchfield.deleteModule(module); + } + + @Override + public int createModule(String module, int inputChannels, int outputChannels, + Notification notification) throws RemoteException { + return patchfield.createModule(module, inputChannels, outputChannels, notification); + } + + @Override + public int connectPorts(String source, int sourcePort, String sink, int sinkPort) + throws RemoteException { + return patchfield.connectPorts(source, sourcePort, sink, sinkPort); + } + + @Override + public int disconnectPorts(String source, int sourcePort, String sink, int sinkPort) + throws RemoteException { + return patchfield.disconnectPorts(source, sourcePort, sink, sinkPort); + } + + @Override + public List getModules() throws RemoteException { + return patchfield.getModules(); + } + + @Override + public int getInputChannels(String module) throws RemoteException { + return patchfield.getInputChannels(module); + } + + @Override + public int getOutputChannels(String module) throws RemoteException { + return patchfield.getOutputChannels(module); + } + + @Override + public Notification getNotification(String module) throws RemoteException { + return patchfield.getNotification(module); + } + + @Override + public boolean isConnected(String source, int sourcePort, String sink, int sinkPort) + throws RemoteException { + return patchfield.isConnected(source, sourcePort, sink, sinkPort); + } + + @Override + public boolean isDependent(String source, String sink) throws RemoteException { + return patchfield.isDependent(source, sink); + } + + @Override + public int start() { + return patchfield.start(); + } + + @Override + public void stop() { + patchfield.stop(); + } + + @Override + public boolean isRunning() { + return patchfield.isRunning(); + } + + @Override + public int activateModule(String module) throws RemoteException { + return patchfield.activateModule(module); + } + + @Override + public int deactivateModule(String module) throws RemoteException { + return patchfield.deactivateModule(module); + } + + @Override + public boolean isActive(String module) throws RemoteException { + return patchfield.isActive(module); + } + + @Override + public int getProtocolVersion() throws RemoteException { + return patchfield.getProtocolVersion(); + } + + @Override + public void startForeground(int id, Notification notification) throws RemoteException { + PatchfieldService.this.startForeground(id, notification); + } + + @Override + public void stopForeground(boolean removeNotification) throws RemoteException { + PatchfieldService.this.stopForeground(removeNotification); + } + }; + + @Override + public void onCreate() { + super.onCreate(); + try { + patchfield = new Patchfield(this, 2, 2); + } catch (IOException e) { + patchfield = null; + } + }; + + @Override + public IBinder onBind(Intent intent) { + return (patchfield != null) ? binder : null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + release(); + } + + private void release() { + if (patchfield != null) { + patchfield.release(); + patchfield = null; + } + } +} diff --git a/PatchfieldControl/.classpath b/PatchfieldControl/.classpath new file mode 100644 index 0000000..5cc5eb9 --- /dev/null +++ b/PatchfieldControl/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/PatchfieldControl/.project b/PatchfieldControl/.project new file mode 100644 index 0000000..1b5f660 --- /dev/null +++ b/PatchfieldControl/.project @@ -0,0 +1,33 @@ + + + PatchfieldControl + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/PatchfieldControl/.settings/org.eclipse.jdt.core.prefs b/PatchfieldControl/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/PatchfieldControl/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/PatchfieldControl/AndroidManifest.xml b/PatchfieldControl/AndroidManifest.xml new file mode 100644 index 0000000..2eaa205 --- /dev/null +++ b/PatchfieldControl/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PatchfieldControl/ic_launcher-web.png b/PatchfieldControl/ic_launcher-web.png new file mode 100644 index 0000000..39c760d Binary files /dev/null and b/PatchfieldControl/ic_launcher-web.png differ diff --git a/PatchfieldControl/libs/android-support-v4.jar b/PatchfieldControl/libs/android-support-v4.jar new file mode 100644 index 0000000..428bdbc Binary files /dev/null and b/PatchfieldControl/libs/android-support-v4.jar differ diff --git a/PatchfieldControl/proguard-project.txt b/PatchfieldControl/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/PatchfieldControl/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/PatchfieldControl/project.properties b/PatchfieldControl/project.properties new file mode 100644 index 0000000..32a175c --- /dev/null +++ b/PatchfieldControl/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library.reference.1=../Patchfield diff --git a/PatchfieldControl/res/drawable-hdpi/ic_launcher.png b/PatchfieldControl/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..c86e7e1 Binary files /dev/null and b/PatchfieldControl/res/drawable-hdpi/ic_launcher.png differ diff --git a/PatchfieldControl/res/drawable-mdpi/ic_launcher.png b/PatchfieldControl/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..52e9e33 Binary files /dev/null and b/PatchfieldControl/res/drawable-mdpi/ic_launcher.png differ diff --git a/PatchfieldControl/res/drawable-xhdpi/ic_launcher.png b/PatchfieldControl/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..efb51f4 Binary files /dev/null and b/PatchfieldControl/res/drawable-xhdpi/ic_launcher.png differ diff --git a/PatchfieldControl/res/drawable-xxhdpi/ic_launcher.png b/PatchfieldControl/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d2395fb Binary files /dev/null and b/PatchfieldControl/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/PatchfieldControl/res/layout/activity_main.xml b/PatchfieldControl/res/layout/activity_main.xml new file mode 100644 index 0000000..c8194ad --- /dev/null +++ b/PatchfieldControl/res/layout/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PatchfieldControl/res/layout/module.xml b/PatchfieldControl/res/layout/module.xml new file mode 100644 index 0000000..d4cb30a --- /dev/null +++ b/PatchfieldControl/res/layout/module.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PatchfieldControl/res/menu/main.xml b/PatchfieldControl/res/menu/main.xml new file mode 100644 index 0000000..c002028 --- /dev/null +++ b/PatchfieldControl/res/menu/main.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/PatchfieldControl/res/values-sw600dp/dimens.xml b/PatchfieldControl/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000..44f01db --- /dev/null +++ b/PatchfieldControl/res/values-sw600dp/dimens.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/PatchfieldControl/res/values-sw720dp-land/dimens.xml b/PatchfieldControl/res/values-sw720dp-land/dimens.xml new file mode 100644 index 0000000..61e3fa8 --- /dev/null +++ b/PatchfieldControl/res/values-sw720dp-land/dimens.xml @@ -0,0 +1,9 @@ + + + + 128dp + + diff --git a/PatchfieldControl/res/values-v11/styles.xml b/PatchfieldControl/res/values-v11/styles.xml new file mode 100644 index 0000000..3c02242 --- /dev/null +++ b/PatchfieldControl/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/PatchfieldControl/res/values-v14/styles.xml b/PatchfieldControl/res/values-v14/styles.xml new file mode 100644 index 0000000..3e61a52 --- /dev/null +++ b/PatchfieldControl/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/PatchfieldControl/res/values/dimens.xml b/PatchfieldControl/res/values/dimens.xml new file mode 100644 index 0000000..55c1e59 --- /dev/null +++ b/PatchfieldControl/res/values/dimens.xml @@ -0,0 +1,7 @@ + + + + 16dp + 16dp + + diff --git a/PatchfieldControl/res/values/strings.xml b/PatchfieldControl/res/values/strings.xml new file mode 100644 index 0000000..b59c12a --- /dev/null +++ b/PatchfieldControl/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + PatchfieldControl + Settings + Hello world! + + diff --git a/PatchfieldControl/res/values/styles.xml b/PatchfieldControl/res/values/styles.xml new file mode 100644 index 0000000..6ce89c7 --- /dev/null +++ b/PatchfieldControl/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/MainActivity.java b/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/MainActivity.java new file mode 100644 index 0000000..9fad7aa --- /dev/null +++ b/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/MainActivity.java @@ -0,0 +1,220 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.control; + +import java.util.List; + +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.view.Menu; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.FrameLayout; +import android.widget.Switch; +import android.widget.TextView; + +import com.noisepages.nettoyeur.patchfield.IPatchfieldClient; +import com.noisepages.nettoyeur.patchfield.IPatchfieldService; + +public class MainActivity extends Activity implements OnCheckedChangeListener { + + private static final String TAG = "PatchControl"; + + private IPatchfieldService patchfield = null; + + private TextView displayLine; + private Switch playButton; + private PatchView patchView; + + private IPatchfieldClient.Stub receiver = new IPatchfieldClient.Stub() { + + @Override + public void onStart() throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + playButton.setChecked(true); + } + }); + } + + @Override + public void onStop() throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + playButton.setChecked(false); + } + }); + } + + @Override + public void onModuleCreated(final String module, final int inputs, final int outputs, + final Notification notification) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.addModule(module, inputs, outputs, notification); + } + }); + } + + @Override + public void onModuleActivated(final String module) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.activateModule(module); + } + }); + } + + @Override + public void onModuleDeactivated(final String module) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.deactivateModule(module); + } + }); + } + + @Override + public void onModuleDeleted(final String module) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.deleteModule(module); + } + }); + } + + @Override + public void onPortsConnected(final String source, final int sourcePort, final String sink, + final int sinkPort) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.addConnection(source, sourcePort, sink, sinkPort); + } + }); + } + + @Override + public void onPortsDisconnected(final String source, final int sourcePort, final String sink, + final int sinkPort) throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + patchView.removeConnection(source, sourcePort, sink, sinkPort); + } + }); + } + }; + + private ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + patchfield = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Service connected."); + patchfield = IPatchfieldService.Stub.asInterface(service); + patchView.setPatchfield(patchfield); + PendingIntent pi = + PendingIntent.getActivity(MainActivity.this, 0, new Intent(MainActivity.this, + MainActivity.class), 0); + Notification notification = + new Notification.Builder(MainActivity.this) + .setSmallIcon(android.R.drawable.ic_media_play).setContentTitle("PatchfieldControl") + .setContentIntent(pi).build(); + try { + patchfield.startForeground(1, notification); + patchfield.registerClient(receiver); + playButton.setChecked(patchfield.isRunning()); + displayLine.setText("Sample rate: " + patchfield.getSampleRate() + ", buffer size: " + + patchfield.getBufferSize() + ", protocol version: " + patchfield.getProtocolVersion()); + List modules = patchfield.getModules(); + for (String module : modules) { + patchView.addModule(module, patchfield.getInputChannels(module), + patchfield.getOutputChannels(module), patchfield.getNotification(module)); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + displayLine = (TextView) findViewById(R.id.displayLine); + playButton = (Switch) findViewById(R.id.playButton); + playButton.setOnCheckedChangeListener(this); + FrameLayout frame = (FrameLayout) findViewById(R.id.moduleFrame); + patchView = new PatchView(this); + patchView.init(this, frame); + bindService(new Intent("IPatchfieldService"), connection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (patchfield != null) { + try { + patchfield.stopForeground(false); + patchfield.unregisterClient(receiver); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + unbindService(connection); + patchfield = null; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (patchfield != null) { + try { + if (isChecked) { + patchfield.start(); + } else { + patchfield.stop(); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } +} diff --git a/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/PatchView.java b/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/PatchView.java new file mode 100644 index 0000000..71f47c6 --- /dev/null +++ b/PatchfieldControl/src/com/noisepages/nettoyeur/patchfield/control/PatchView.java @@ -0,0 +1,413 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.control; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import android.app.Notification; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Cap; +import android.graphics.Path; +import android.os.RemoteException; +import android.util.Pair; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.FrameLayout; +import android.widget.GridLayout; +import android.widget.LinearLayout; +import android.widget.Space; +import android.widget.Toast; +import android.widget.ToggleButton; + +import com.noisepages.nettoyeur.patchfield.IPatchfieldService; + +public final class PatchView extends GridLayout { + + private IPatchfieldService patchfield; + private final List modules = new ArrayList(); + private final Map, List>> connections = + new HashMap, List>>(); + + public PatchView(Context context) { + super(context); + } + + public void setPatchfield(IPatchfieldService patchfield) { + this.patchfield = patchfield; + } + + public void addModule(String module, int inputChannels, int outputChannels, + Notification notification) { + for (String u : modules) { + int sinks = 0; + try { + sinks = patchfield.getInputChannels(u); + } catch (RemoteException e) { + e.printStackTrace(); + continue; + } + for (int i = 0; i < outputChannels; ++i) { + for (int j = 0; j < sinks; ++j) { + try { + if (patchfield.isConnected(module, i, u, j)) { + addConnection(module, i, u, j); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } + int sources = 0; + try { + sources = patchfield.getOutputChannels(u); + } catch (RemoteException e) { + e.printStackTrace(); + continue; + } + for (int i = 0; i < inputChannels; ++i) { + for (int j = 0; j < sources; ++j) { + try { + if (patchfield.isConnected(u, j, module, i)) { + addConnection(u, j, module, i); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } + } + + modules.add(module); + + if (notification == null) { + notification = + new Notification.Builder(getContext()).setContentTitle(module) + .setSmallIcon(R.drawable.emo_im_happy).build(); + } + addModuleView(module, inputChannels, outputChannels, notification); + } + + public void deleteModule(String module) { + for (Iterator, List>>> iter = + connections.entrySet().iterator(); iter.hasNext();) { + Entry, List>> entry = iter.next(); + if (entry.getKey().first.equals(module)) { + iter.remove(); + } else { + for (Iterator> iter2 = entry.getValue().iterator(); iter2.hasNext();) { + Pair q = iter2.next(); + if (q.first.equals(module)) { + iter2.remove(); + } + } + } + } + modules.remove(module); + deleteModuleView(module); + } + + public void activateModule(String module) { + updateModuleView(module, true); + } + + public void deactivateModule(String module) { + updateModuleView(module, false); + } + + public void addConnection(String source, int sourcePort, String sink, int sinkPort) { + Pair a = new Pair(source, sourcePort); + List> c = connections.get(a); + if (c == null) { + c = new ArrayList>(); + connections.put(a, c); + } + Pair b = new Pair(sink, sinkPort); + if (!c.contains(b)) { + c.add(b); + } + invalidateAll(); + } + + public void removeConnection(String source, int sourcePort, String sink, int sinkPort) { + Pair a = new Pair(source, sourcePort); + List> c = connections.get(a); + if (c == null) { + return; + } + Pair b = new Pair(sink, sinkPort); + c.remove(b); + invalidateAll(); + } + + + /* + * Crude GUI code below. + * + * Ideas for improvements: + * + * - Improve support for screen formats: The current layout works best with landscape orientation + * on large tablets. + * + * - Improve the visual appearance of play button and status line. + * + * - Improve the placement of modules. Instead of misusing GridLayout, consider + * letting users move modules around, or use a graph layout algorithm to place modules. + * + * - Improve drawing of patch cords so that they won't obscure modules views. + * + * - Add support for deleting modules. (what gesture would be appropriate?) + * + * - Implement support for saving and restoring patches. + */ + + private Toast toast = null; + + private void toast(String msg) { + if (toast == null) { + toast = Toast.makeText(getContext(), "", Toast.LENGTH_SHORT); + } + toast.setText(msg); + toast.show(); + } + + private class Overlay extends View { + + private final Paint paint = new Paint(); + private final Path path = new Path(); + private final int a[] = new int[4]; + private final int b[] = new int[4]; + + public Overlay(Context context) { + super(context); + paint.setAntiAlias(true); + paint.setColor(Color.RED); + paint.setStrokeWidth(12); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Cap.ROUND); + } + + @Override + protected void onDraw(Canvas canvas) { + path.reset(); + for (Pair p : connections.keySet()) { + getCoordinates(outputPorts.get(p.first).get(p.second), a); + int x0 = (a[0] + a[2]) / 2; + int y0 = (a[1] + a[3]) / 2; + for (Pair q : connections.get(p)) { + getCoordinates(inputPorts.get(q.first).get(q.second), b); + int x1 = (b[0] + b[2]) / 2; + int y1 = (b[1] + b[3]) / 2; + path.moveTo(x0, y0); + path.cubicTo(x0, (y0 + y1) / 2, x1, (y0 + y1) / 2, x1, y1); + } + } + canvas.drawPath(path, paint); + } + } + + private Overlay overlay = null; + private final Map moduleViews = new HashMap(); + private final Map> inputPorts = new HashMap>(); + private final Map> outputPorts = new HashMap>(); + + private CompoundButton inputButton = null; + private CompoundButton outputButton = null; + private String inputModule = null; + private String outputModule = null; + private int inputPort = -1; + private int outputPort = -1; + + private void handlePortEvent(CompoundButton button, String module, int port, boolean isOutput, + boolean isChecked) { + if (!isChecked) { + clearInputPortState(); + clearOutputPortState(); + return; + } + if (isOutput) { + clearOutputPortState(); + outputButton = button; + outputModule = module; + outputPort = port; + } else { + clearInputPortState(); + inputButton = button; + inputModule = module; + inputPort = port; + } + if (inputButton != null && outputButton != null) { + try { + if (patchfield.isConnected(outputModule, outputPort, inputModule, inputPort)) { + patchfield.disconnectPorts(outputModule, outputPort, inputModule, inputPort); + } else { + patchfield.connectPorts(outputModule, outputPort, inputModule, inputPort); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + clearInputPortState(); + clearOutputPortState(); + } + } + + private void clearInputPortState() { + if (inputButton != null) { + inputButton.setChecked(false); + inputButton = null; + } + } + + private void clearOutputPortState() { + if (outputButton != null) { + outputButton.setChecked(false); + outputButton = null; + } + } + + public void init(Context context, FrameLayout frame) { + setColumnCount(2); + setRowCount(32); + frame.addView(this); + overlay = new Overlay(context); + frame.addView(overlay); + } + + private void addModuleView(final String module, int inputChannels, int outputChannels, + final Notification notification) { + View moduleView = inflate(getContext(), R.layout.module, null); + // Warning: Atrocious hack to place views in the desired place, Part I. + addView(new Space(getContext())); + addView(new Space(getContext())); + addView(moduleView); + moduleViews.put(module, moduleView); + + LinearLayout buttonLayout = (LinearLayout) moduleView.findViewById(R.id.inputPorts); + inputPorts.put(module, createPorts(module, inputChannels, buttonLayout, false)); + + FrameLayout frame = (FrameLayout) moduleView.findViewById(R.id.moduleFrame); + View view = notification.contentView.apply(getContext(), frame); + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (notification.contentIntent != null) { + try { + notification.contentIntent.send(); + } catch (CanceledException e) { + e.printStackTrace(); + toast("Unable to launch app: " + e); + } + } else { + toast("Unable to launch app: App did not provide launch info."); + } + } + }); + view.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + try { + if (patchfield.isActive(module)) { + patchfield.deactivateModule(module); + } else { + patchfield.activateModule(module); + } + } catch (RemoteException e) { + e.printStackTrace(); + } + return true; + } + }); + frame.addView(view); + + buttonLayout = (LinearLayout) moduleView.findViewById(R.id.outputPorts); + outputPorts.put(module, createPorts(module, outputChannels, buttonLayout, true)); + + boolean isActive = false; + try { + isActive = patchfield.isActive(module); + } catch (RemoteException e) { + e.printStackTrace(); + } + updateModuleView(module, isActive); + } + + private ArrayList createPorts(final String module, int channels, LinearLayout buttonLayout, + final boolean isOutput) { + ArrayList buttons = new ArrayList(); + for (int i = 0; i < channels; ++i) { + final ToggleButton button = new ToggleButton(getContext()); + buttons.add(button); + String text = (isOutput ? "Out" : "In") + i; + button.setTextOn(text); + button.setTextOff(text); + button.setChecked(false); + buttonLayout.addView(button); + final int j = i; + button.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + handlePortEvent(buttonView, module, j, isOutput, isChecked); + } + }); + } + return buttons; + } + + private void deleteModuleView(String module) { + View moduleView = moduleViews.remove(module); + if (moduleView != null) { + int index; + for (index = 0; !moduleView.equals(getChildAt(index)); ++index); + removeView(moduleView); + // Warning: Atrocious hack to place views in the desired place, Part II. + while (index > 0 && getChildAt(--index) instanceof Space) { + removeViewAt(index); + } + } + invalidateAll(); + } + + private void getCoordinates(View v, int coords[]) { + int p[] = new int[2]; + v.getLocationInWindow(p); + coords[0] = p[0]; + coords[1] = p[1]; + getLocationInWindow(p); + coords[0] -= p[0]; + coords[1] -= p[1]; + coords[2] = coords[0] + v.getWidth(); + coords[3] = coords[1] + v.getHeight(); + } + + private void updateModuleView(String module, boolean isActive) { + View moduleView = moduleViews.get(module); + FrameLayout frame = (FrameLayout) moduleView.findViewById(R.id.moduleFrame); + frame.setAlpha(isActive ? 1.0f : 0.3f); + invalidateAll(); + } + + private void invalidateAll() { + invalidate(); + overlay.invalidate(); + } +} diff --git a/PatchfieldPd/.classpath b/PatchfieldPd/.classpath new file mode 100644 index 0000000..0e854b2 --- /dev/null +++ b/PatchfieldPd/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/PatchfieldPd/.project b/PatchfieldPd/.project new file mode 100644 index 0000000..b6d6070 --- /dev/null +++ b/PatchfieldPd/.project @@ -0,0 +1,33 @@ + + + PatchfieldPd + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/PatchfieldPd/.settings/org.eclipse.jdt.core.prefs b/PatchfieldPd/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/PatchfieldPd/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/PatchfieldPd/AndroidManifest.xml b/PatchfieldPd/AndroidManifest.xml new file mode 100644 index 0000000..af00d8d --- /dev/null +++ b/PatchfieldPd/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/PatchfieldPd/Makefile b/PatchfieldPd/Makefile new file mode 100644 index 0000000..a0f1380 --- /dev/null +++ b/PatchfieldPd/Makefile @@ -0,0 +1,3 @@ +all: + javah -classpath bin/classes -o jni/module/pdmodule.h com.noisepages.nettoyeur.patchfield.pd.PdModule + env NDK_MODULE_PATH=$(CURDIR)/.. ndk-build diff --git a/PatchfieldPd/ic_launcher-web.png b/PatchfieldPd/ic_launcher-web.png new file mode 100644 index 0000000..4131fb0 Binary files /dev/null and b/PatchfieldPd/ic_launcher-web.png differ diff --git a/PatchfieldPd/jni/Android.mk b/PatchfieldPd/jni/Android.mk new file mode 100644 index 0000000..5053e7d --- /dev/null +++ b/PatchfieldPd/jni/Android.mk @@ -0,0 +1 @@ +include $(call all-subdir-makefiles) diff --git a/PatchfieldPd/jni/Application.mk b/PatchfieldPd/jni/Application.mk new file mode 100644 index 0000000..84a096d --- /dev/null +++ b/PatchfieldPd/jni/Application.mk @@ -0,0 +1,2 @@ +APP_OPTIM := release +APP_ABI := armeabi armeabi-v7a x86 diff --git a/PatchfieldPd/jni/module/Android.mk b/PatchfieldPd/jni/module/Android.mk new file mode 100644 index 0000000..ef30668 --- /dev/null +++ b/PatchfieldPd/jni/module/Android.mk @@ -0,0 +1,14 @@ +# Note that NDK_MODULE_PATH must contain the patchfield parent directory. The +# makefile in PatchfieldPd implicitly takes care of this. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := pdmodule +LOCAL_LDLIBS := -llog +LOCAL_SRC_FILES := pdmodule.c +LOCAL_STATIC_LIBRARIES := audiomodule buffersizeadapter +LOCAL_SHARED_LIBRARIES := pdnativeopensl +include $(BUILD_SHARED_LIBRARY) +$(call import-module,Patchfield/jni) diff --git a/PatchfieldPd/jni/module/pdmodule.c b/PatchfieldPd/jni/module/pdmodule.c new file mode 100644 index 0000000..6966d0d --- /dev/null +++ b/PatchfieldPd/jni/module/pdmodule.c @@ -0,0 +1,53 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "pdmodule.h" + +#include "../libpd/jni/z_jni_native_hooks.h" +#include "audio_module.h" +#include "utils/buffer_size_adapter.h" + +#include + +static void process_pd(void *context, int sample_rate, int buffer_frames, + int input_channels, const float *input_buffer, + int output_channels, float *output_buffer) { + libpd_sync_process_raw(input_buffer, output_buffer); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_pdInitAudio +(JNIEnv *env, jobject obj, jint input_channels, jint output_channels, + jint sample_rate) { + libpd_sync_init_audio(input_channels, output_channels, sample_rate); +} + +JNIEXPORT jlong JNICALL +Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_configureModule +(JNIEnv *env, jobject obj, jlong handle, + jint host_buffer_size, jint user_buffer_size, + jint input_channels, jint output_channels) { + return (jlong) bsa_create((void *) handle, + host_buffer_size, user_buffer_size, + input_channels, output_channels, + process_pd, NULL); +} + +JNIEXPORT void JNICALL +Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_release +(JNIEnv *env, jobject obj, jlong p) { + bsa_release((buffer_size_adapter *) p); +} diff --git a/PatchfieldPd/jni/module/pdmodule.h b/PatchfieldPd/jni/module/pdmodule.h new file mode 100644 index 0000000..cc81030 --- /dev/null +++ b/PatchfieldPd/jni/module/pdmodule.h @@ -0,0 +1,37 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_noisepages_nettoyeur_patchfield_pd_PdModule */ + +#ifndef _Included_com_noisepages_nettoyeur_patchfield_pd_PdModule +#define _Included_com_noisepages_nettoyeur_patchfield_pd_PdModule +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_noisepages_nettoyeur_patchfield_pd_PdModule + * Method: pdInitAudio + * Signature: (III)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_pdInitAudio + (JNIEnv *, jobject, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_pd_PdModule + * Method: configureModule + * Signature: (JIIII)J + */ +JNIEXPORT jlong JNICALL Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_configureModule + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: com_noisepages_nettoyeur_patchfield_pd_PdModule + * Method: release + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_noisepages_nettoyeur_patchfield_pd_PdModule_release + (JNIEnv *, jobject, jlong); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/PatchfieldPd/libs/android-support-v4.jar b/PatchfieldPd/libs/android-support-v4.jar new file mode 100644 index 0000000..428bdbc Binary files /dev/null and b/PatchfieldPd/libs/android-support-v4.jar differ diff --git a/PatchfieldPd/proguard-project.txt b/PatchfieldPd/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/PatchfieldPd/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/PatchfieldPd/project.properties b/PatchfieldPd/project.properties new file mode 100644 index 0000000..032c08a --- /dev/null +++ b/PatchfieldPd/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true +android.library.reference.1=../Patchfield diff --git a/PatchfieldPd/res/drawable-hdpi/ic_launcher.png b/PatchfieldPd/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..bff79fd Binary files /dev/null and b/PatchfieldPd/res/drawable-hdpi/ic_launcher.png differ diff --git a/PatchfieldPd/res/drawable-mdpi/ic_launcher.png b/PatchfieldPd/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..6c47374 Binary files /dev/null and b/PatchfieldPd/res/drawable-mdpi/ic_launcher.png differ diff --git a/PatchfieldPd/res/drawable-xhdpi/ic_launcher.png b/PatchfieldPd/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d6e3e95 Binary files /dev/null and b/PatchfieldPd/res/drawable-xhdpi/ic_launcher.png differ diff --git a/PatchfieldPd/res/drawable-xxhdpi/ic_launcher.png b/PatchfieldPd/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..dec0b85 Binary files /dev/null and b/PatchfieldPd/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/PatchfieldPd/res/values-v11/styles.xml b/PatchfieldPd/res/values-v11/styles.xml new file mode 100644 index 0000000..3c02242 --- /dev/null +++ b/PatchfieldPd/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/PatchfieldPd/res/values-v14/styles.xml b/PatchfieldPd/res/values-v14/styles.xml new file mode 100644 index 0000000..a91fd03 --- /dev/null +++ b/PatchfieldPd/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/PatchfieldPd/res/values/strings.xml b/PatchfieldPd/res/values/strings.xml new file mode 100644 index 0000000..2ad500c --- /dev/null +++ b/PatchfieldPd/res/values/strings.xml @@ -0,0 +1,5 @@ + + + PatchfieldPd + + diff --git a/PatchfieldPd/res/values/styles.xml b/PatchfieldPd/res/values/styles.xml new file mode 100644 index 0000000..6ce89c7 --- /dev/null +++ b/PatchfieldPd/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/PatchfieldPd/src/com/noisepages/nettoyeur/patchfield/pd/PdModule.java b/PatchfieldPd/src/com/noisepages/nettoyeur/patchfield/pd/PdModule.java new file mode 100644 index 0000000..b87e2b9 --- /dev/null +++ b/PatchfieldPd/src/com/noisepages/nettoyeur/patchfield/pd/PdModule.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.noisepages.nettoyeur.patchfield.pd; + +import org.puredata.core.PdBase; + +import android.app.Notification; + +import com.noisepages.nettoyeur.patchfield.AudioModule; + +/** + * An audio module implementation that uses Pure Data (via libpd) internally. + * + * Since Pd is currently limited to one instance per process, PdModule is a singleton of sorts. When + * it is first created, the channel counts are configurable; once it has been created, the + * configuration is fixed for the lifetime of the process. + * + * PdModule takes care of the initialization of libpd. In particular, make sure to create your + * PdModule instance before calling any methods on PdBase. Do _not_ call PdBase.openAudio(...) or + * PdBase.computeAudio(...). After creating your PdModule instance, you can use PdBase as usual. + */ +public class PdModule extends AudioModule { + + static { + PdBase.blockSize(); // Make sure to load PdBase first. + System.loadLibrary("pdmodule"); + } + + private static PdModule instance = null; + + private long ptr = 0; + + private final int sampleRate; + private final int inputChannels; + private final int outputChannels; + + private PdModule(int sampleRate, int inputChannels, int outputChannels, Notification notification) { + super(notification); + this.sampleRate = sampleRate; + this.inputChannels = inputChannels; + this.outputChannels = outputChannels; + pdInitAudio(inputChannels, outputChannels, sampleRate); + PdBase.computeAudio(true); + } + + /** + * Static factory method for creating PdModule instances. Note that the sample rate must be the + * sample rate reported by the Patchfield service. + */ + public static PdModule getInstance(int sampleRate, int inputChannels, int outputChannels, + Notification notification) { + if (instance == null) { + return new PdModule(sampleRate, inputChannels, outputChannels, notification); + } else if (instance.getInputChannels() >= inputChannels + && instance.getOutputChannels() >= outputChannels + && (notification == null || notification.equals(instance.getNotification()))) { + return instance; + } else { + throw new IllegalStateException("PdModule instance can't be reconfigured once instantiated."); + } + } + + @Override + public int getInputChannels() { + return inputChannels; + } + + @Override + public int getOutputChannels() { + return outputChannels; + } + + @Override + protected boolean configure(String name, long handle, int sampleRate, int bufferSize) { + if (ptr != 0) { + throw new IllegalStateException("Module has already been configured."); + } + ptr = configureModule(handle, bufferSize, PdBase.blockSize(), inputChannels, outputChannels); + return ptr != 0; + } + + @Override + protected void release() { + if (ptr != 0) { + release(ptr); + } + } + + private native void pdInitAudio(int inputChannels, int outputChannels, int sampleRate); + + private native long configureModule(long handle, int bufferSize, int blockSize, + int inputChannels, int outputChannels); + + private native void release(long ptr); +}