diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index c352db493..0bd2d06c4 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -4,6 +4,8 @@ #include "maple_devs.h" #include "maple_cfg.h" #include +#include +#include #include "deps/zlib/zlib.h" @@ -225,7 +227,7 @@ struct maple_sega_controller: maple_base return MDRS_DataTransfer; default: - printf("UNKOWN MAPLE COMMAND %d\n",cmd); + //printf("UNKOWN MAPLE COMMAND %d\n",cmd); return MDRE_UnknownFunction; } } @@ -644,13 +646,191 @@ struct maple_sega_vmu: maple_base default: - printf("Unknown MAPLE COMMAND %d\n",cmd); + //printf("Unknown MAPLE COMMAND %d\n",cmd); return MDRE_UnknownCmd; } } }; #endif +struct maple_microphone: maple_base +{ + u8 micdata[SIZE_OF_MIC_DATA]; + + virtual void OnSetup() + { + memset(micdata,0,sizeof(micdata)); + } + + virtual u32 dma(u32 cmd) + { + //printf("maple_microphone::dma Called 0x%X;Command %d\n",this->maple_port,cmd); + //LOGD("maple_microphone::dma Called 0x%X;Command %d\n",this->maple_port,cmd); + + switch (cmd) + { + case MDC_DeviceRequest: + LOGI("maple_microphone::dma MDC_DeviceRequest"); + //this was copied from the controller case with just the id and name replaced! + + //caps + //4 + w32(MFID_4_Mic); + + //struct data + //3*4 + w32( 0xfe060f00); + w32( 0); + w32( 0); + + //1 area code + w8(0xFF); + + //1 direction + w8(0); + + //30 + wstr(maple_sega_mic_name,30); + + //60 + wstr(maple_sega_brand,60); + + //2 + w16(0x01AE); + + //2 + w16(0x01F4); + + return MDRS_DeviceStatus; + + case MDCF_GetCondition: + { + LOGI("maple_microphone::dma MDCF_GetCondition"); + //this was copied from the controller case with just the id replaced! + + //PlainJoystickState pjs; + //config->GetInput(&pjs); + //caps + //4 + w32(MFID_4_Mic); + + //state data + //2 key code + //w16(pjs.kcode); + + //triggers + //1 R + //w8(pjs.trigger[PJTI_R]); + //1 L + //w8(pjs.trigger[PJTI_L]); + + //joyx + //1 + //w8(pjs.joy[PJAI_X1]); + //joyy + //1 + //w8(pjs.joy[PJAI_Y1]); + + //not used + //1 + w8(0x80); + //1 + w8(0x80); + } + + return MDRS_DataTransfer; + + case MDC_DeviceReset: + //uhhh do nothing? + LOGI("maple_microphone::dma MDC_DeviceReset"); + return MDRS_DeviceReply; + + case MDCF_MICControl: + { + //LOGD("maple_microphone::dma handling MDCF_MICControl %d\n",cmd); + //MONEY + u32 function=r32(); + //LOGD("maple_microphone::dma MDCF_MICControl function (1st word) %#010x\n", function); + //LOGD("maple_microphone::dma MDCF_MICControl words: %d\n", dma_count_in); + + switch(function) + { + case MFID_4_Mic: + { + //MAGIC HERE + //http://dcemulation.org/phpBB/viewtopic.php?f=34&t=69600 + // <3 <3 BlueCrab <3 <3 + /* + 2nd word What it does: + 0x0000??03 Sets the amplifier gain, ?? can be from 00 to 1F + 0x0f = default + 0x00008002 Enables recording + 0x00000001 Returns sampled data while recording is enabled + While not enabled, returns status of the mic. + 0x00000002 Disables recording + * + */ + u32 secondword=r32(); + //LOGD("maple_microphone::dma MDCF_MICControl subcommand (2nd word) %#010x\n", subcommand); + + u32 subcommand = secondword & 0xFF; //just get last byte for now, deal with params later + + //LOGD("maple_microphone::dma MDCF_MICControl (3rd word) %#010x\n", r32()); + //LOGD("maple_microphone::dma MDCF_MICControl (4th word) %#010x\n", r32()); + switch(subcommand) + { + case 0x01: + { + //LOGI("maple_microphone::dma MDCF_MICControl someone wants some data! (2nd word) %#010x\n", secondword); + + w32(MFID_4_Mic); + + //from what i can tell this is up to spec but results in transmit again + //w32(secondword); + + //32 bit header + w8(0x04);//status (just the bit for recording) + w8(0x0f);//gain (default) + w8(0);//exp ? + + if(get_mic_data(micdata)){ + w8(240);//ct (240 samples) + wptr(micdata, SIZE_OF_MIC_DATA); + }else{ + w8(0); + } + + return MDRS_DataTransfer; + } + case 0x02: + LOGI("maple_microphone::dma MDCF_MICControl toggle recording %#010x\n",secondword); + return MDRS_DeviceReply; + case 0x03: + LOGI("maple_microphone::dma MDCF_MICControl set gain %#010x\n",secondword); + return MDRS_DeviceReply; + case MDRE_TransminAgain: + LOGW("maple_microphone::dma MDCF_MICControl MDRE_TransminAgain"); + //apparently this doesnt matter + //wptr(micdata, SIZE_OF_MIC_DATA); + return MDRS_DeviceReply;//MDRS_DataTransfer; + default: + LOGW("maple_microphone::dma UNHANDLED secondword %#010x\n",secondword); + break; + } + } + default: + LOGW("maple_microphone::dma UNHANDLED function %#010x\n",function); + break; + } + } + + default: + LOGW("maple_microphone::dma UNHANDLED MAPLE COMMAND %d\n",cmd); + return MDRE_UnknownFunction; + } + } +}; + maple_device* maple_Create(MapleDeviceType type) { maple_device* rv=0; @@ -659,6 +839,9 @@ maple_device* maple_Create(MapleDeviceType type) case MDT_SegaController: rv=new maple_sega_controller(); break; + case MDT_Microphone: + rv=new maple_microphone(); + break; #ifdef HAS_VMU case MDT_SegaVMU: rv = new maple_sega_vmu(); @@ -670,4 +853,4 @@ maple_device* maple_Create(MapleDeviceType type) } return rv; -} \ No newline at end of file +} diff --git a/core/hw/maple/maple_devs.h b/core/hw/maple/maple_devs.h index 0c9980dc0..936fc4fd6 100644 --- a/core/hw/maple/maple_devs.h +++ b/core/hw/maple/maple_devs.h @@ -5,6 +5,7 @@ enum MapleDeviceType { MDT_SegaController, MDT_SegaVMU, + MDT_Microphone, MDT_Count }; @@ -27,4 +28,6 @@ struct maple_device virtual u32 Dma(u32 Command,u32* buffer_in,u32 buffer_in_len,u32* buffer_out,u32& buffer_out_len)=0; }; -maple_device* maple_Create(MapleDeviceType type); \ No newline at end of file +maple_device* maple_Create(MapleDeviceType type); +#define SIZE_OF_MIC_DATA 480 //ALSO DEFINED IN SipEmulator.java +int get_mic_data(u8* buffer); //implemented in Android.cpp diff --git a/docs/microphone support notes.txt b/docs/microphone support notes.txt new file mode 100644 index 000000000..cc22ba69c --- /dev/null +++ b/docs/microphone support notes.txt @@ -0,0 +1,37 @@ +1/13/2014 hooby3dfx initial plan +1/16/2014 hooby3dfx updated with progress, more details on how it works + + +general info links from bluecrab: http://dreamcast-talk.com/forum/viewtopic.php?t=2921&f=5 +kallistios driver (could use for test app): http://cadcdev.sourceforge.net/docs/kos-2.0.0/sip_8h.html +maplebus: https://web.archive.org/web/20101117090620/http://mc.pp.se/dc/maplebus.html + +plan: +-figure out the mic commands that seaman is looking for and implement them [DONE (a couple commands not supported)] +-create mic test app (consult bluecrab) [Otoire tunes should be great and the other real games that support mic - should compile list] +-figure out how the game/dc requests the audio (or how the mic just streams)... [Done - at least in seaman DC tells mic to start recording then polls the mic pretty frequently for data] +-for poc just create some kind of global way to pass the audio data from android mic interface directly to the maple_base instance [Done] +-test [Created recording in Otoire, imported VMS into Demul and got the sound! Seaman needs more testing] +-cleanup [Partially done, lots of logging left] +-test more +-integrate into settings ui + + +16-bit PCM @ 11025 hz +== 176.4 kbit/s +== 22050 bytes/s + +maximum size of a Maple Bus packet is 256 words (1024 bytes) + + +================ +Games that can use mic: +-Seaman +-Alien Front Online +-Planet Ring +-Mr Driller +-Otoire +-Propeller Arena +-Visual Park +-Kiteretsu Boys Gangagan + diff --git a/shell/android/AndroidManifest.xml b/shell/android/AndroidManifest.xml index 7d138bd71..404e6237e 100644 --- a/shell/android/AndroidManifest.xml +++ b/shell/android/AndroidManifest.xml @@ -11,11 +11,13 @@ - + + - + + NewGlobalRef(sip); + getmicdata = env->GetMethodID(env->GetObjectClass(sipemu),"getData","()[B"); + delete MapleDevices[0][1]; + mcfg_Create(MDT_Microphone,0,1); +} + JNIEXPORT void JNICALL Java_com_reicast_emulator_JNIdc_stop(JNIEnv *env,jobject obj) { dc_term(); @@ -330,3 +345,15 @@ bool os_IsAudioBuffered() { return jenv->CallIntMethod(track,writemid,jsamples,-1)==0; } + +int get_mic_data(u8* buffer) +{ + jbyteArray jdata = (jbyteArray)jenv->CallObjectMethod(sipemu,getmicdata); + if(jdata==NULL){ + //LOGW("get_mic_data NULL"); + return 0; + } + jenv->GetByteArrayRegion(jdata, 0, SIZE_OF_MIC_DATA, (jbyte*)buffer); + jenv->DeleteLocalRef(jdata); + return 1; +} diff --git a/shell/android/res/layout/input_fragment.xml b/shell/android/res/layout/input_fragment.xml index 8419268c2..1de0b7213 100644 --- a/shell/android/res/layout/input_fragment.xml +++ b/shell/android/res/layout/input_fragment.xml @@ -92,6 +92,32 @@ android:ems="8" android:text="@string/launch_editor" /> + + + + + + + + + + diff --git a/shell/android/res/values/strings.xml b/shell/android/res/values/strings.xml index f5beb0e5f..e433ae06a 100644 --- a/shell/android/res/values/strings.xml +++ b/shell/android/res/values/strings.xml @@ -46,6 +46,7 @@ Enable Custom Key Layout Enable Compatibility Mode Joystick Uses DPAD Layout + Microphone plugged into port 2 Customize Physical Controls Modify Controller diff --git a/shell/android/src/com/reicast/emulator/GL2JNIActivity.java b/shell/android/src/com/reicast/emulator/GL2JNIActivity.java index f2f3f600d..b2a625b71 100644 --- a/shell/android/src/com/reicast/emulator/GL2JNIActivity.java +++ b/shell/android/src/com/reicast/emulator/GL2JNIActivity.java @@ -325,6 +325,14 @@ public class GL2JNIActivity extends Activity { Toast.makeText(getApplicationContext(), "Press the back button for a menu", Toast.LENGTH_SHORT).show(); + + //setup mic + boolean micPluggedIn = prefs.getBoolean("mic_plugged_in", false); + if(micPluggedIn){ + SipEmulator sip = new SipEmulator(); + sip.startRecording(); + JNIdc.setupMic(sip); + } } private void runCompatibilityMode() { diff --git a/shell/android/src/com/reicast/emulator/InputFragment.java b/shell/android/src/com/reicast/emulator/InputFragment.java index 77c567bc2..59bf7e2a4 100644 --- a/shell/android/src/com/reicast/emulator/InputFragment.java +++ b/shell/android/src/com/reicast/emulator/InputFragment.java @@ -39,6 +39,7 @@ public class InputFragment extends Fragment { private AlertDialog alertDialogSelectController; private SharedPreferences sharedPreferences; private Switch switchTouchVibrationEnabled; + private Switch micPluggedIntoFirstController; public MOGAInput moga = new MOGAInput(); @@ -106,6 +107,23 @@ public class InputFragment extends Fragment { } switchTouchVibrationEnabled.setOnCheckedChangeListener(touch_vibration); + micPluggedIntoFirstController = (Switch) getView().findViewById( + R.id.micInPort2); + boolean micPluggedIn = sharedPreferences.getBoolean("mic_plugged_in", false); + micPluggedIntoFirstController.setChecked(micPluggedIn); + if (getActivity().getPackageManager().hasSystemFeature( + "android.hardware.microphone")) { + //Microphone is present on the device + micPluggedIntoFirstController.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + sharedPreferences.edit().putBoolean("mic_plugged_in", isChecked).commit(); + } + }); + }else{ + micPluggedIntoFirstController.setEnabled(false); + } + + Button buttonKeycodeEditor = (Button) getView().findViewById( R.id.buttonKeycodeEditor); buttonKeycodeEditor.setOnClickListener(new View.OnClickListener() { diff --git a/shell/android/src/com/reicast/emulator/JNIdc.java b/shell/android/src/com/reicast/emulator/JNIdc.java index c29c67117..b1dc12fbb 100644 --- a/shell/android/src/com/reicast/emulator/JNIdc.java +++ b/shell/android/src/com/reicast/emulator/JNIdc.java @@ -22,6 +22,8 @@ public class JNIdc //public static native int play(short result[],int size); public static native void initControllers(boolean[] controllers); + + public static native void setupMic(Object sip); public static void show_osd() { JNIdc.vjoy(13, 1,0,0,0); diff --git a/shell/android/src/com/reicast/emulator/SipEmulator.java b/shell/android/src/com/reicast/emulator/SipEmulator.java new file mode 100644 index 000000000..bb5afd88d --- /dev/null +++ b/shell/android/src/com/reicast/emulator/SipEmulator.java @@ -0,0 +1,109 @@ +package com.reicast.emulator; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.util.Log; + +public class SipEmulator extends Thread { + + static final String TAG = "SipEmulator"; + + //one second of audio data in bytes + static final int BUFFER_SIZE = 22050; + //this needs to get set to the amount the mic normally sends per data request + //...cant be bigger than a maple packet + // 240 16 (or 14) bit samples + static final int ONE_BLIP_SIZE = 480; //ALSO DEFINED IN maple_devs.h + + private AudioRecord record; + private ConcurrentLinkedQueue bytesReadBuffer; + + private boolean continueRecording; + private boolean firstGet; + + /* + 16-bit PCM @ 11025 hz + == 176.4 kbit/s + == 22050 bytes/s + */ + + public SipEmulator(){ + + Log.d(TAG, "SipEmulator constructor called"); + + init(); + + } + + private void init(){ + Log.d(TAG, "SipEmulator init called"); + + record = new AudioRecord( + MediaRecorder.AudioSource.MIC, + 11025, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + BUFFER_SIZE); + + bytesReadBuffer = new ConcurrentLinkedQueue(); + + continueRecording = false; + firstGet = true; + } + + public void startRecording(){ + Log.d(TAG, "SipEmulator startRecording called"); + if(continueRecording){ + return; + } + record.startRecording(); + continueRecording = true; + this.start(); + } + + public void stopRecording(){ + Log.d(TAG, "SipEmulator stopRecording called"); + continueRecording = false; + record.stop(); + } + + public byte[] getData(){ + //Log.d(TAG, "SipEmulator getData called"); + //Log.d(TAG, "SipEmulator getData bytesReadBuffer size: "+bytesReadBuffer.size()); + if(firstGet || bytesReadBuffer.size()>50){//50 blips is about 2 seconds! + firstGet = false; + return catchUp(); + } + return bytesReadBuffer.poll(); + } + + private byte[] catchUp(){ + Log.d(TAG, "SipEmulator catchUp"); + byte[] last = bytesReadBuffer.poll(); + bytesReadBuffer.clear(); + return last; + } + + public void configSomething(int what, int setting){ + Log.d(TAG, "SipEmulator configSomething called"); + + } + + public void run() { + Log.d(TAG, "recordThread starting"); + + while(continueRecording){ + byte[] freshData = new byte[ONE_BLIP_SIZE]; + // read blocks + int bytesRead = record.read(freshData, 0, ONE_BLIP_SIZE); + //Log.d(TAG, "recordThread recorded: "+bytesRead); + if(!firstGet){ + bytesReadBuffer.add(freshData); + } + } + } + +}