diff --git a/framework/CMakeLists.txt b/framework/CMakeLists.txt new file mode 100644 index 0000000..79159b0 --- /dev/null +++ b/framework/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 2.6) + +set(CMAKE_CXX_STANDARD 14) + +set(HAS_LIBPS3EYE 1 PARENT_SCOPE) +message("setting HAS_LIBPS3EYE") + +# --- dependencies --- + +if (NOT HAS_LIBGG) + message("including libgg") + add_subdirectory(../../../libgg libgg) +endif (NOT HAS_LIBGG) +if (NOT HAS_FRAMEWORK) + message("including framework") + add_subdirectory(../../../framework framework) +endif (NOT HAS_FRAMEWORK) + +# + +project(libps3eye) + +# --- libps3eye --- + +file(GLOB_RECURSE source "../src/*.*") + +add_library(libps3eye ${source}) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules) + +find_package(PkgConfig) +find_package(LibUSB REQUIRED) + +target_include_directories(libps3eye PUBLIC "${PROJECT_SOURCE_DIR}/../src") +target_include_directories(libps3eye PRIVATE "${PROJECT_SOURCE_DIR}" "${LibUSB_INCLUDE_DIRS}") + +target_link_libraries(libps3eye PRIVATE libgg ${LibUSB_LIBRARIES}) + +# + +add_executable(example main.cpp) + +target_link_libraries(example PRIVATE libps3eye framework ${FRAMEWORK_LIBRARIES}) diff --git a/framework/README.md b/framework/README.md new file mode 100644 index 0000000..bf650a1 --- /dev/null +++ b/framework/README.md @@ -0,0 +1,4 @@ +PS3EYE Driver +======== + +Library and example for [framework](https://github.com/marcel303/framework), the free and open source creative coding library. diff --git a/framework/cmake_modules/FindLibUSB.cmake b/framework/cmake_modules/FindLibUSB.cmake new file mode 100755 index 0000000..5a71fd7 --- /dev/null +++ b/framework/cmake_modules/FindLibUSB.cmake @@ -0,0 +1,103 @@ +# - Find libusb for portable USB support +# +# If the LibUSB_ROOT environment variable +# is defined, it will be used as base path. +# The following standard variables get defined: +# LibUSB_FOUND: true if LibUSB was found +# LibUSB_INCLUDE_DIR: the directory that contains the include file +# LibUSB_LIBRARIES: the libraries + +IF(PKG_CONFIG_FOUND) + IF(DEPENDS_DIR) #Otherwise use System pkg-config path + SET(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${DEPENDS_DIR}/libusb/lib/pkgconfig") + ENDIF() + SET(MODULE "libusb-1.0") + IF(CMAKE_SYSTEM_NAME MATCHES "Linux") + SET(MODULE "libusb-1.0>=1.0.20") + ENDIF() + IF(LibUSB_FIND_REQUIRED) + SET(LibUSB_REQUIRED "REQUIRED") + ENDIF() + PKG_CHECK_MODULES(LibUSB ${LibUSB_REQUIRED} ${MODULE}) + + FIND_LIBRARY(LibUSB_LIBRARY + NAMES ${LibUSB_LIBRARIES} + HINTS ${LibUSB_LIBRARY_DIRS} + ) + SET(LibUSB_LIBRARIES ${LibUSB_LIBRARY}) + + RETURN() +ENDIF() + +FIND_PATH(LibUSB_INCLUDE_DIRS + NAMES libusb.h + PATHS + "${DEPENDS_DIR}/libusb" + "${DEPENDS_DIR}/libusbx" + ENV LibUSB_ROOT + PATH_SUFFIXES + include + libusb + include/libusb-1.0 +) + +SET(LIBUSB_NAME libusb) +IF(WIN32) + INCLUDE(CheckCSourceRuns) + CHECK_C_SOURCE_RUNS("#include \nint main(){return !LoadLibraryA(\"libusbK\");}" LIBUSB_WITH_LIBUSBK) + CHECK_C_SOURCE_RUNS("#include \nint main(){return !LoadLibraryA(\"UsbDkHelper\");}" LIBUSB_WITH_USBDK) + + IF(LIBUSB_USE_USBDK) + SET(LIBUSB_NAME libusb-usbdk) + ENDIF() + + IF(LIBUSB_NAME MATCHES ^libusb-usbdk$ AND NOT LIBUSB_WITH_USBDK) + MESSAGE(WARNING "UsbDk device driver is not found. Fall back to libusbK.") + SET(LIBUSB_NAME libusb) + ENDIF() + + IF(LIBUSB_NAME MATCHES ^libusb$ AND NOT LIBUSB_WITH_LIBUSBK) + MESSAGE(FATAL_ERROR "No USB device driver is installed.") + ENDIF() +ENDIF() + +FIND_LIBRARY(LibUSB_LIBRARIES + NAMES ${LIBUSB_NAME}-1.0 + PATHS + "${DEPENDS_DIR}/libusb" + "${DEPENDS_DIR}/libusbx" + ENV LibUSB_ROOT + PATH_SUFFIXES + x64/Release/dll + x64/Debug/dll + Win32/Release/dll + Win32/Debug/dll + MS64 + MS64/dll +) + +IF(WIN32) +FIND_FILE(LibUSB_DLL + ${LIBUSB_NAME}-1.0.dll + PATHS + "${DEPENDS_DIR}/libusb" + "${DEPENDS_DIR}/libusbx" + ENV LibUSB_ROOT + PATH_SUFFIXES + x64/Release/dll + x64/Debug/dll + Win32/Release/dll + Win32/Debug/dll + MS64 + MS64/dll +) +IF(LibUSB_DLL AND LIBUSB_USE_USBDK) + FILE(COPY ${LibUSB_DLL} DESTINATION ${CMAKE_BINARY_DIR}) + SET(LibUSB_DLL ${CMAKE_BINARY_DIR}/libusb-1.0.dll) + FILE(RENAME ${CMAKE_BINARY_DIR}/${LIBUSB_NAME}-1.0.dll ${LibUSB_DLL}) +ENDIF() +ENDIF() + +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibUSB FOUND_VAR LibUSB_FOUND + REQUIRED_VARS LibUSB_LIBRARIES LibUSB_INCLUDE_DIRS) diff --git a/framework/main.cpp b/framework/main.cpp new file mode 100644 index 0000000..2bef047 --- /dev/null +++ b/framework/main.cpp @@ -0,0 +1,294 @@ +#include "framework.h" +#include "ps3eye.h" +#include "ps3mic.h" + +#define CAM_SX 640 +#define CAM_SY 480 +#define CAM_FPS 60 // Frames per second. At 320x240 this can go as high as 187 fps. + +#define CAM_GRAYSCALE false // If true the camera will output a grayscale image instead of rgb color. + +#define DO_SUM true // Calculates and draws the sum of all four microphones. The effect of this is that the summed channel amplifies sounds coming from the front, while reducing ambient noise and sounds coming from the sides. Try it out by making hissing sounds from different directions towards the camera! + +#define NUM_CHANNELS (DO_SUM ? 5 : 4) // The number of channels to record. Normally this would be four, but when DO_SUM is enabled, this becomes five. + +#define AUDIO_HISTORY_LENGTH .5f // Length of the audio history buffer in seconds. + +using namespace ps3eye; + +static std::string errorString; + +static uint8_t imageData[CAM_SX * CAM_SY * 3]; + +struct MyAudioCallback : AudioCallback +{ + std::vector histories[NUM_CHANNELS]; + int maxHistoryFrames = 0; + int numHistoryFrames = 0; + int firstHistoryFrame = 0; + + MyAudioCallback(const float seconds) + { + maxHistoryFrames = seconds * PS3EYEMic::kSampleRate; + + for (int i = 0; i < NUM_CHANNELS; ++i) + { + histories[i].resize(maxHistoryFrames, 0.f); + } + } + + virtual void handleAudioData(const AudioFrame * frames, const int numFrames) override + { + //printf("received %d frames!\n", numFrames); + + for (int i = 0; i < numFrames; ++i) + { + for (int c = 0; c < 4; ++c) + { + auto & history = histories[c]; + + const float value = frames[i].channel[c] / float(1 << 15); + + history[firstHistoryFrame] = value; + } + + #if DO_SUM + histories[4][firstHistoryFrame] = + ( + histories[0][firstHistoryFrame] + + histories[1][firstHistoryFrame] + + histories[2][firstHistoryFrame] + + histories[3][firstHistoryFrame] + ) / 4.f; + #endif + + if (numHistoryFrames < maxHistoryFrames) + numHistoryFrames++; + + firstHistoryFrame++; + + if (firstHistoryFrame == maxHistoryFrames) + firstHistoryFrame = 0; + } + } +}; + +static void drawAudioHistory(const MyAudioCallback & audioCallback) +{ + gxPushMatrix(); + { + gxScalef(CAM_SX / float(audioCallback.maxHistoryFrames - 1), CAM_SY, 1.f); + + for (int c = 0; c < NUM_CHANNELS; ++c) + { + auto & history = audioCallback.histories[c]; + + gxPushMatrix(); + { + gxTranslatef(0, (c + 1.f) / (NUM_CHANNELS + 1), 0); + gxScalef(1, 1.f / NUM_CHANNELS, 1); + + const Color colors[NUM_CHANNELS] = + { + colorRed, + colorGreen, + colorBlue, + colorYellow, + colorWhite + }; + + setColor(colors[c]); + hqBegin(HQ_LINES, true); + { + for (int i = audioCallback.firstHistoryFrame, n = 0; n + 1 < audioCallback.maxHistoryFrames; ++n) + { + const int index1 = i; + const int index2 = i + 1 == audioCallback.maxHistoryFrames ? 0 : i + 1; + + const float value1 = history[index1]; + const float value2 = history[index2]; + const float strokeSize = .4f; + + hqLine(n + 0, value1, strokeSize, n + 1, value2, strokeSize); + + // + + i = index2; + } + } + hqEnd(); + } + gxPopMatrix(); + } + } + gxPopMatrix(); +} + +#if 0 // todo : remove + +static void testAudioStreaming(PS3EYECam * eye) +{ + const int testDuration = 10; // seconds + const int historySize = 1; // seconds + + PS3EYEMic mic; + MyAudioCallback audioCallback(historySize); + + if (mic.init(eye->getDevice(), &audioCallback)) + { + const auto endTime = SDL_GetTicks() + testDuration * 1000; + + while (SDL_GetTicks() < endTime && !keyboard.wentDown(SDLK_SPACE)) + { + framework.process(); + + framework.beginDraw(0, 0, 0, 0); + { + drawAudioHistory(audioCallback); + } + framework.endDraw(); + } + + mic.shut(); + } +} + +#endif + +int main(int argc, char * argv[]) +{ + if (!framework.init(0, nullptr, 640, 480)) + return -1; + + auto devices = PS3EYECam::getDevices(); + + logInfo("found %d devices", devices.size()); + + PS3EYECam::PS3EYERef eye; + + if (devices.empty()) + errorString = "no PS3 eye camera found"; + else + eye = devices[0]; + + devices.clear(); + +#if 0 // todo : remove + if (eye != nullptr) + { + testAudioStreaming(eye.get()); + } +#endif + + if (eye != nullptr) + { + if (!eye->init(CAM_SX, CAM_SY, CAM_FPS, + CAM_GRAYSCALE + ? PS3EYECam::EOutputFormat::Gray + : PS3EYECam::EOutputFormat::RGB)) + { + errorString = "failed to initialize PS3 eye camera"; + } + else + { + eye->start(); + } + } + + PS3EYEMic mic; + MyAudioCallback audioCallback(AUDIO_HISTORY_LENGTH); + + if (eye != nullptr) + { + mic.init(eye->getDevice(), &audioCallback); + } + + if (!errorString.empty()) + { + logError("error: %s", errorString.c_str()); + } + + GLuint texture = 0; + + bool stressTest = false; + + while (!framework.quitRequested) + { + framework.process(); + + if (keyboard.wentDown(SDLK_ESCAPE)) + framework.quitRequested = true; + + if (keyboard.isDown(SDLK_LSHIFT) && keyboard.wentDown(SDLK_t)) + stressTest = !stressTest; + + if (stressTest && (rand() % 100) == 0) + { + if (mic.getIsInitialized()) + mic.shut(); + else + mic.init(eye->getDevice(), &audioCallback); + } + + if (eye != nullptr && eye->isStreaming()) + { + eye->getFrame(imageData); + + // + + if (texture != 0) + glDeleteTextures(1, &texture); + + if (CAM_GRAYSCALE) + { + texture = createTextureFromR8(imageData, CAM_SX, CAM_SY, false, true); + + glBindTexture(GL_TEXTURE_2D, texture); + GLint swizzleMask[4] = { GL_RED, GL_RED, GL_RED, GL_ONE }; + glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzleMask); + glBindTexture(GL_TEXTURE_2D, 0); + } + else + { + texture = createTextureFromRGB8(imageData, CAM_SX, CAM_SY, false, true); + } + } + + framework.beginDraw(0, 0, 0, 0); + { + if (texture != 0) + { + pushBlend(BLEND_OPAQUE); + { + setColor(colorWhite); + gxSetTexture(texture); + drawRect(0, 0, CAM_SX, CAM_SY); + gxSetTexture(0); + } + popBlend(); + } + + drawAudioHistory(audioCallback); + + hqBegin(HQ_FILLED_CIRCLES); + setColor((eye != nullptr && eye->isStreaming()) ? colorGreen : colorRed); + hqFillCircle(14, 14, 10); + setColor(mic.getIsInitialized() ? colorGreen : colorRed); + hqFillCircle(14, 40, 10); + hqEnd(); + } + framework.endDraw(); + } + + mic.shut(); + + if (eye != nullptr) + { + eye->stop(); + eye = nullptr; + } + + framework.shutdown(); + + return 0; +} diff --git a/src/ps3eye.cpp b/src/ps3eye.cpp index 98d0e01..48b1d2c 100644 --- a/src/ps3eye.cpp +++ b/src/ps3eye.cpp @@ -1,10 +1,24 @@ // source code from https://github.com/inspirit/PS3EYEDriver #include "ps3eye.h" +// Get rid of annoying zero length structure warnings from libusb.h in MSVC + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4200) +#endif + +#include "libusb.h" + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + #include #include #include #include +#include #if defined WIN32 || defined _WIN32 || defined WINCE #include @@ -70,6 +84,12 @@ #define snprintf _snprintf #endif +#if defined(DEBUG) && 0 +#define debug(...) fprintf(stdout, __VA_ARGS__) +#else +#define debug(...) +#endif + namespace ps3eye { #define TRANSFER_SIZE 65536 @@ -424,6 +444,16 @@ int USBMgr::listDevices( std::vector& list ) return cnt; } +void micStarted() +{ + USBMgr::instance()->cameraStarted(); +} + +void micStopped() +{ + USBMgr::instance()->cameraStopped(); +} + static void LIBUSB_CALL transfer_completed_callback(struct libusb_transfer *xfr); class FrameQueue @@ -525,8 +555,8 @@ class FrameQueue uint8_t* dest_row = outBuffer + dest_stride + 1; // We start outputting at the second pixel of the second row's G component uint32_t R,G,B; - // Fill rows 1 to height-1 of the destination buffer. First and last row are filled separately (they are copied from the second row and second-to-last rows respectively) - for (int y = 0; y < frame_height-1; source_row += source_stride, dest_row += dest_stride, ++y) + // Fill rows 1 to height-2 of the destination buffer. First and last row are filled separately (they are copied from the second row and second-to-last rows respectively) + for (int y = 0; y < frame_height-2; source_row += source_stride, dest_row += dest_stride, ++y) { const uint8_t* source = source_row; const uint8_t* source_end = source + (source_stride-2); // -2 to deal with the fact that we're starting at the second pixel of the row and should end at the second-to-last pixel of the row (first and last are filled separately) @@ -626,8 +656,8 @@ class FrameQueue uint8_t* dest_row = outBuffer + dest_stride + num_output_channels + 1; // We start outputting at the second pixel of the second row's G component int swap_br = inBGR ? 1 : -1; - // Fill rows 1 to height-1 of the destination buffer. First and last row are filled separately (they are copied from the second row and second-to-last rows respectively) - for (int y = 0; y < frame_height-1; source_row += source_stride, dest_row += dest_stride, ++y) + // Fill rows 1 to height-2 of the destination buffer. First and last row are filled separately (they are copied from the second row and second-to-last rows respectively) + for (int y = 0; y < frame_height-2; source_row += source_stride, dest_row += dest_stride, ++y) { const uint8_t* source = source_row; const uint8_t* source_end = source + (source_stride-2); // -2 to deal with the fact that we're starting at the second pixel of the row and should end at the second-to-last pixel of the row (first and last are filled separately) @@ -801,6 +831,13 @@ class URBDesc // Wait for cancelation to finish num_active_transfers_condition.wait(lock, [this]() { return num_active_transfers == 0; }); + // Free completed transfers + for (int index = 0; index < NUM_TRANSFERS; ++index) + { + libusb_free_transfer(xfr[index]); + xfr[index] = nullptr; + } + USBMgr::instance()->cameraStopped(); free(transfer_buffer); @@ -960,8 +997,7 @@ static void LIBUSB_CALL transfer_completed_callback(struct libusb_transfer *xfr) if (status != LIBUSB_TRANSFER_COMPLETED) { debug("transfer status %d\n", status); - - libusb_free_transfer(xfr); + urb->transfer_canceled(); if(status != LIBUSB_TRANSFER_CANCELLED) @@ -1015,7 +1051,7 @@ PS3EYECam::PS3EYECam(libusb_device *device) greenblc = 128; flip_h = false; flip_v = false; - + testPattern = false; usb_buf = NULL; handle_ = NULL; @@ -1228,6 +1264,10 @@ bool PS3EYECam::open_usb() return false; } + // Linux has a kernel module for the PS3 eye camera (that's where most of the code in here comes from..) + // so we must detach the driver before we can hook up with the eye ourselves + libusb_detach_kernel_driver(handle_, 0); + //libusb_set_configuration(handle_, 0); res = libusb_claim_interface(handle_, 0); @@ -1243,6 +1283,7 @@ void PS3EYECam::close_usb() { debug("closing device\n"); libusb_release_interface(handle_, 0); + libusb_attach_kernel_driver(handle_, 0); libusb_close(handle_); libusb_unref_device(device_); handle_ = NULL; diff --git a/src/ps3eye.h b/src/ps3eye.h index 3c801d8..4bbeb22 100644 --- a/src/ps3eye.h +++ b/src/ps3eye.h @@ -2,25 +2,11 @@ #ifndef PS3EYECAM_H #define PS3EYECAM_H -#include -#include -#include -#include - #include +#include -// Get rid of annoying zero length structure warnings from libusb.h in MSVC - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable : 4200) -#endif - -#include "libusb.h" - -#ifdef _MSC_VER -#pragma warning(pop) -#endif +struct libusb_device; +struct libusb_device_handle; #ifndef __STDC_CONSTANT_MACROS # define __STDC_CONSTANT_MACROS @@ -28,13 +14,6 @@ #include -#if defined(DEBUG) -#define debug(...) fprintf(stdout, __VA_ARGS__) -#else -#define debug(...) -#endif - - namespace ps3eye { class PS3EYECam @@ -161,10 +140,21 @@ class PS3EYECam sccb_reg_write(0x0c, val); } + bool getTestPattern() const { return testPattern; } + void setTestPattern(bool enable) + { + testPattern = enable; + uint8_t val = sccb_reg_read(0x0C); + val &= ~0b00000001; + if (testPattern) val |= 0b00000001; // 0x80; + sccb_reg_write(0x0C, val); + } + bool isStreaming() const { return is_streaming; } bool isInitialized() const { return device_ != NULL && handle_ != NULL && usb_buf != NULL; } + libusb_device *getDevice() const { return device_; } bool getUSBPortPath(char *out_identifier, size_t max_identifier_length) const; // Get a frame from the camera. Notes: @@ -217,6 +207,7 @@ class PS3EYECam uint8_t greenblc; // 0 <-> 255 bool flip_h; bool flip_v; + bool testPattern; // bool is_streaming; diff --git a/src/ps3eye_capi.cpp b/src/ps3eye_capi.cpp index 34be865..c504c57 100644 --- a/src/ps3eye_capi.cpp +++ b/src/ps3eye_capi.cpp @@ -31,6 +31,7 @@ #include "ps3eye.h" #include +#include struct ps3eye_context_t { ps3eye_context_t() @@ -250,4 +251,4 @@ ps3eye_set_parameter(ps3eye_t *eye, ps3eye_parameter param, int value) } return 0; -} \ No newline at end of file +} diff --git a/src/ps3mic.cpp b/src/ps3mic.cpp new file mode 100644 index 0000000..ea70586 --- /dev/null +++ b/src/ps3mic.cpp @@ -0,0 +1,366 @@ +#include "ps3eye.h" // for vendor and product IDs +#include "ps3mic.h" +#include "libusb.h" +#include + +/* + +The PS3 eye camera exposes its microphone array as a regular USB audio device with four channels + +Each USB device can expose one or more 'interface'. The PS3 eye camera exposes its video +capabilities on interface 0. Its USB audio interface is exposed on interface 2 (INTERFACE_NUMBER). + +We set the audio interface to alt setting 1, which means 'mono' output, which actually means to +output one or more mono streams. For reference: alt setting 0 is energy savings mode (disabled) and +alt setting 2 is stereo mode, which always produces a stream of stereo frames, even if the mic +only has one capsule. + +*/ + +#if defined(DEBUG) + #include + #define debugMessage(...) do { fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } while (false) +#else + #define debugMessage(...) +#endif + +namespace ps3eye { + +static const int INTERFACE_NUMBER = 2; + +static const int ISO_ENDPOINT_ADDRESS = 0x84; + +/* +For streaming we set up a bunch of transfers, which will each transfer a number of packets of a certain size. I have no idea what the ideal values are here. I guess there's a sweet spot between still having robust transfers while still maintaining an acceptable latency. + +todo : find good values for these numbers +*/ +static const int PACKET_SIZE = 1024; +static const int NUM_PACKETS = 1; +static const int NUM_TRANSFERS = 4; + +// + +extern void micStarted(); +extern void micStopped(); + +// + +const int PS3EYEMic::kSampleRate = 16000; + +void PS3EYEMic::handleTransfer(struct libusb_transfer * transfer) +{ + PS3EYEMic * mic = (PS3EYEMic*)transfer->user_data; + + assert(mic->numActiveTransfers > 0); + + bool endTransfer = false; + + if (mic->endTransfers) + { + endTransfer = true; + } + else if (transfer->status == LIBUSB_TRANSFER_COMPLETED) + { + for (int i = 0; i < transfer->num_iso_packets; ++i) + { + const libusb_iso_packet_descriptor & packet = transfer->iso_packet_desc[i]; + + if (packet.status != LIBUSB_TRANSFER_COMPLETED) + { + debugMessage("packet status != LIBUSB_TRANSFER_COMPLETED"); + continue; + } + + const int frameSize = 4 * sizeof(int16_t); + + const int numBytes = packet.actual_length; + assert((numBytes % frameSize) == 0); + const int numFrames = numBytes / frameSize; + + if (numFrames > 0) + { + const AudioFrame * frames = (AudioFrame*)libusb_get_iso_packet_buffer_simple(transfer, i); + + mic->audioCallback->handleAudioData(frames, numFrames); + } + } + + int res = LIBUSB_SUCCESS; + + for (int i = 0; i < NUM_TRANSFERS; ++i) + { + // on Macos (at least), libusb_submit_transfer may fail due to 'kIOReturnIsoTooOld'. the version of libusb + // we're using doesn't actually report this, instead we get LIBUSB_ERROR_OTHER. for now, handle errors + // by simply retrying, until we retried for every transfer + + res = libusb_submit_transfer(transfer); + + if (res == LIBUSB_SUCCESS) + break; + } + + if (res != LIBUSB_SUCCESS) + { + debugMessage("failed to submit transfer: %d: %s", res, libusb_error_name(res)); + endTransfer = true; + } + } + else + { + debugMessage("transfer ended but status is not COMPLETED: status=%d", transfer->status); + + const int res = libusb_submit_transfer(transfer); + + if (res < 0) + { + debugMessage("failed to submit transfer: %d: %s", res, libusb_error_name(res)); + endTransfer = true; + } + } + + if (endTransfer) + { + std::unique_lock lock(mic->numActiveTransfersMutex); + mic->numActiveTransfers--; + mic->numActiveTransfersCondition.notify_one(); + } +} + +PS3EYEMic::PS3EYEMic() + : endTransfers(false) +{ +} + +PS3EYEMic::~PS3EYEMic() +{ +} + +bool PS3EYEMic::init(libusb_device * device, AudioCallback * audioCallback) +{ + const bool result = initImpl(device, audioCallback); + + if (result == false) + shut(); + + return result; +} + +void PS3EYEMic::shut() +{ + shutImpl(); +} + +bool PS3EYEMic::getIsInitialized() const +{ + return started == true; +} + +bool PS3EYEMic::initImpl(libusb_device * _device, AudioCallback * _audioCallback) +{ + assert(device == nullptr); + assert(audioCallback == nullptr); + + device = _device; + audioCallback = _audioCallback; + + assert(device != nullptr); + assert(audioCallback != nullptr); + + // open the USB device + + int res; + + res = libusb_open(device, &deviceHandle); + if (res != LIBUSB_SUCCESS) + { + debugMessage("device open error: %d: %s", res, libusb_error_name(res)); + return false; + } + + res = libusb_set_configuration(deviceHandle, 1); + if (res != LIBUSB_SUCCESS) + { + debugMessage("failed to set device configuration: %d: %s", res, libusb_error_name(res)); + return false; + } + + // before we claim the interface, we must first detach any kernel drivers that may use it + + if (libusb_kernel_driver_active(deviceHandle, INTERFACE_NUMBER)) + { + res = libusb_detach_kernel_driver(deviceHandle, INTERFACE_NUMBER); + if (res != LIBUSB_SUCCESS) + { + debugMessage("failed to detach kernel driver: %d: %s", res, libusb_error_name(res)); + return false; + } + } + + // claim interface + + res = libusb_claim_interface(deviceHandle, INTERFACE_NUMBER); + if (res != LIBUSB_SUCCESS) + { + #ifdef __APPLE__ + printf( + "-- Failed to claim interface. On Macos this may be due to the OS already having loaded the Apple USB Audio kernel driver for your Eye camera. LibUSB cannot unload this driver for us, so you'll have to run the following command from the terminal to unload it manually. Note this will kill audio to/from ALL USB audio devices!!:\n" + "sudo kextunload -b com.apple.driver.AppleUSBAudio\n" + "--\n" + ); + #endif + // todo : add Windows help text here in case claiming the interface fails + + debugMessage("failed to claim interface: %d: %s", res, libusb_error_name(res)); + return false; + } + + // set the interface alt mode. 1 = (multi-channel) mono + + res = libusb_set_interface_alt_setting(deviceHandle, INTERFACE_NUMBER, 1); + if (res != LIBUSB_SUCCESS) + { + debugMessage("failed to set interface alt setting: %d: %sn", res, libusb_error_name(res)); + return false; + } + + // + + micStarted(); + started = true; + + // + + if (!beginTransfers(PACKET_SIZE, NUM_PACKETS, NUM_TRANSFERS)) + { + return false; + } + + return true; +} + +void PS3EYEMic::shutImpl() +{ + if (numActiveTransfers > 0) + { + endTransfersBegin(); + + endTransfersWait(); + } + + freeTransfers(); + + // + + if (started) + { + micStopped(); + started = false; + } + + // + + if (deviceHandle != nullptr) + { + int res = libusb_release_interface(deviceHandle, INTERFACE_NUMBER); + if (res != LIBUSB_SUCCESS) + debugMessage("failed to release interface. %d: %s", res, libusb_error_name(res)); + + res = libusb_attach_kernel_driver(deviceHandle, INTERFACE_NUMBER); + if (res != LIBUSB_SUCCESS) + debugMessage("failed to attach kernel driver for interface. %d: %s", res, libusb_error_name(res)); + + libusb_close(deviceHandle); + deviceHandle = nullptr; + } + + // + + device = nullptr; + audioCallback = nullptr; +} + +bool PS3EYEMic::beginTransfers(const int packetSize, const int numPackets, const int numTransfers) +{ + const int transferSize = packetSize * numPackets; + + assert(transferData == nullptr); + transferData = new uint8_t[transferSize]; + + // + + assert(transfers.empty()); + transfers.resize(numTransfers); + + for (int i = 0; i < numTransfers; ++i) + { + auto & transfer = transfers[i]; + + transfer = libusb_alloc_transfer(numPackets); + + if (transfer == nullptr) + { + debugMessage("failed to allocate transfer"); + return false; + } + + libusb_fill_iso_transfer( + transfer, + deviceHandle, + ISO_ENDPOINT_ADDRESS, + transferData, + transferSize, + numPackets, + handleTransfer, + this, + 1000); + + libusb_set_iso_packet_lengths(transfer, packetSize); + + const int res = libusb_submit_transfer(transfer); + + if (res != LIBUSB_SUCCESS) + { + debugMessage("failed to submit transfer. %d: %s", res, libusb_error_name(res)); + return false; + } + + std::unique_lock lock(numActiveTransfersMutex); + numActiveTransfers++; + } + + return true; +} + +void PS3EYEMic::endTransfersBegin() +{ + endTransfers = true; +} + +void PS3EYEMic::endTransfersWait() +{ + std::unique_lock lock(numActiveTransfersMutex); + numActiveTransfersCondition.wait(lock, [this]() { return numActiveTransfers == 0; }); + + endTransfers = false; +} + +void PS3EYEMic::freeTransfers() +{ + for (auto & transfer : transfers) + { + libusb_free_transfer(transfer); + transfer = nullptr; + } + + transfers.clear(); + + // + + delete [] transferData; + transferData = nullptr; +} + +#undef debugMessage + +} diff --git a/src/ps3mic.h b/src/ps3mic.h new file mode 100644 index 0000000..19813ad --- /dev/null +++ b/src/ps3mic.h @@ -0,0 +1,93 @@ +/* + +PS3 eye routines for capturing audio input from the four-channel microphone array. + +Support and donate at, +https://www.patreon.com/marcelsmit + +Code origionally written by Marcel Smit, +Author of the Framework creative coding library, +https://github.com/marcel303/framework + +Donated to the community to hack away with. + +If you want to support my efforts building a new creative coding library, +an audio-visual patching system, tutorials and examples and ofcourse +projects like these, please become a patron. :-) + +todo : + +- 48kHz sampling rate + Marketing materials, Wikipedia etc all consistently say the PS3 Eye camera supports a 48kHz sampling rate. However, the USB audio class descriptor only mentions support for up to 16kHz, and this is what I get. Perhaps it's possible to switch modes? There's a mysterious vendor specific interface in the USB interface descriptor list. Perhaps this is the key? + +- Combine PS3 eye camera feed with microphone array input + This should be trivial to accomplish once all of the building blocks are in place. + +*/ + +#pragma once + +#include +#include +#include +#include + +struct libusb_device; +struct libusb_device_handle; +struct libusb_transfer; + +namespace ps3eye { + +struct AudioFrame +{ + int16_t channel[4]; +}; + +struct AudioCallback +{ + // numSamples = numFrames * 4, as the PS3 eye contains a four-channel microphone array + + virtual void handleAudioData(const AudioFrame * frames, const int numFrames) = 0; +}; + +class PS3EYEMic +{ +private: + libusb_device * device = nullptr; + libusb_device_handle * deviceHandle = nullptr; + bool started = false; + + AudioCallback * audioCallback = nullptr; + + uint8_t * transferData = nullptr; + + std::vector transfers; + int numActiveTransfers = 0; + std::mutex numActiveTransfersMutex; + std::condition_variable numActiveTransfersCondition; + std::atomic endTransfers; + + static void handleTransfer(struct libusb_transfer * transfer); + +public: + static const int kSampleRate; + + PS3EYEMic(); + ~PS3EYEMic(); + + bool init(libusb_device * device, AudioCallback * audioCallback); // Initialize and begin capturing microphone data + void shut(); // End capturing microphone data and shutdown + + bool getIsInitialized() const; + +private: + bool initImpl(libusb_device * device, AudioCallback * audioCallback); + void shutImpl(); + + bool beginTransfers(const int packetSize, const int numPackets, const int numTransfers); + void endTransfersBegin(); + void endTransfersWait(); + void freeTransfers(); +}; + +}