├── .gitignore ├── .idea └── gradle.xml ├── androidfskmodem ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── bg │ └── cytec │ └── android │ └── fskmodem │ ├── FSKConfig.java │ ├── FSKDecoder.java │ └── FSKEncoder.java ├── arduino └── SoftTermExample │ └── SoftTermExample.ino ├── build.gradle ├── fSKModemTerminal ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── bg │ │ └── cytec │ │ └── android │ │ └── fskmodem │ │ └── examples │ │ └── terminal │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ └── values │ └── strings.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── import-summary.txt ├── settings.gradle └── waterlevel ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── akg │ └── sensprout │ └── waterlevel │ └── ApplicationTest.java └── main ├── AndroidManifest.xml ├── java └── com │ └── akg │ └── sensprout │ └── waterlevel │ └── MainActivity.java └── res ├── layout └── activity_main.xml ├── menu └── menu_main.xml ├── mipmap-hdpi └── ic_launcher.png ├── mipmap-mdpi └── ic_launcher.png ├── mipmap-xhdpi └── ic_launcher.png ├── mipmap-xxhdpi └── ic_launcher.png ├── values-w820dp └── dimens.xml └── values ├── dimens.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | /*/build/ 3 | build 4 | 5 | # Crashlytics configuations 6 | com_crashlytics_export_strings.xml 7 | 8 | # Local configuration file (sdk path, etc) 9 | local.properties 10 | 11 | # Gradle generated files 12 | .gradle/ 13 | 14 | # Signing files 15 | .signing/ 16 | 17 | # User-specific configurations 18 | .idea/libraries/ 19 | .idea/workspace.xml 20 | .idea/tasks.xml 21 | .idea/.name 22 | .idea/compiler.xml 23 | .idea/copyright/profiles_settings.xml 24 | .idea/encodings.xml 25 | .idea/misc.xml 26 | .idea/modules.xml 27 | .idea/scopes/scope_settings.xml 28 | .idea/vcs.xml 29 | *.iml 30 | 31 | # OS-specific files 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | ehthumbs.db 38 | Thumbs.db -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /androidfskmodem/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "23.0.0 rc2" 6 | 7 | defaultConfig { 8 | minSdkVersion 8 9 | targetSdkVersion 21 10 | } 11 | 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /androidfskmodem/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /androidfskmodem/src/main/java/bg/cytec/android/fskmodem/FSKConfig.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the FSKModem java/android library for 3 | * processing FSK audio signals. 4 | * 5 | * The FSKModem library is developed by Ivan Ganev, CEO at 6 | * Cytec BG Ltd. 7 | * 8 | * Copyright (C) 2014 Cytec BG Ltd. office@cytec.bg 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with this program. If not, see . 22 | */ 23 | 24 | package bg.cytec.android.fskmodem; 25 | 26 | import java.io.IOException; 27 | 28 | import android.media.AudioFormat; 29 | 30 | public class FSKConfig { 31 | 32 | public static final int SAMPLE_RATE_44100 = 44100; // LCMx2 33 | public static final int SAMPLE_RATE_22050 = 22050; // LEAST COMMON MULTIPLE 34 | // OF 2450, 1225, 630, 35 | // 315 and 126 36 | public static final int SAMPLE_RATE_29400 = 29400; // DEFAULT for 1225; 24 37 | // samples per bit 38 | 39 | public static final int PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; 40 | public static final int PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; 41 | 42 | public static final int CHANNELS_MONO = 1; 43 | public static final int CHANNELS_STEREO = 2; 44 | 45 | public static final int SOFT_MODEM_MODE_1 = 1; 46 | public static final int SOFT_MODEM_MODE_1_BAUD_RATE = 126; 47 | public static final int SOFT_MODEM_MODE_1_LOW_FREQ = 882; 48 | public static final int SOFT_MODEM_MODE_1_HIGH_FREQ = 1764; 49 | 50 | public static final int SOFT_MODEM_MODE_2 = 2; 51 | public static final int SOFT_MODEM_MODE_2_BAUD_RATE = 315; 52 | public static final int SOFT_MODEM_MODE_2_LOW_FREQ = 1575; 53 | public static final int SOFT_MODEM_MODE_2_HIGH_FREQ = 3150; 54 | 55 | public static final int SOFT_MODEM_MODE_3 = 3; 56 | public static final int SOFT_MODEM_MODE_3_BAUD_RATE = 630; 57 | public static final int SOFT_MODEM_MODE_3_LOW_FREQ = 3150; 58 | public static final int SOFT_MODEM_MODE_3_HIGH_FREQ = 6300; 59 | 60 | public static final int SOFT_MODEM_MODE_4 = 4; 61 | public static final int SOFT_MODEM_MODE_4_BAUD_RATE = 1225; 62 | public static final int SOFT_MODEM_MODE_4_LOW_FREQ = 4900; 63 | public static final int SOFT_MODEM_MODE_4_HIGH_FREQ = 7350; 64 | 65 | public static final int SOFT_MODEM_MODE_5 = 5; 66 | public static final int SOFT_MODEM_MODE_5_BAUD_RATE = 2450; 67 | public static final int SOFT_MODEM_MODE_5_LOW_FREQ = 4900; 68 | public static final int SOFT_MODEM_MODE_5_HIGH_FREQ = 7350; 69 | 70 | public static final int THRESHOLD_1P = 1; // above and under; sums 2% 71 | public static final int THRESHOLD_5P = 5; // above and under; sums 10% 72 | public static final int THRESHOLD_10P = 10; // above and under; sums 20% 73 | public static final int THRESHOLD_20P = 20; // above and under; sums 40% 74 | 75 | public static final int RMS_SILENCE_THRESHOLD_8BIT = 7; 76 | public static final int RMS_SILENCE_THRESHOLD_16BIT = 2000; 77 | 78 | public static final int DECODER_DATA_BUFFER_SIZE = 8; 79 | public static final int ENCODER_DATA_BUFFER_SIZE = 128; 80 | 81 | public static final int ENCODER_PRE_CARRIER_BITS = 3; 82 | public static final int ENCODER_POST_CARRIER_BITS = 1; 83 | public static final int ENCODER_SILENCE_BITS = 3; 84 | 85 | // / 86 | 87 | public int sampleRate; 88 | public int pcmFormat; 89 | public int channels; 90 | public int modemMode; 91 | 92 | // / 93 | 94 | public int samplesPerBit; 95 | 96 | public int modemBaudRate; 97 | public int modemFreqLow; 98 | public int modemFreqHigh; 99 | 100 | public int modemFreqLowThresholdHigh; 101 | public int modemFreqHighThresholdHigh; 102 | 103 | public int rmsSilenceThreshold; 104 | 105 | public FSKConfig(int sampleRate, int pcmFormat, int channels, 106 | int modemMode, int threshold) throws IOException { 107 | this.sampleRate = sampleRate; 108 | this.pcmFormat = pcmFormat; 109 | this.channels = channels; 110 | this.modemMode = modemMode; 111 | 112 | switch (modemMode) { 113 | case SOFT_MODEM_MODE_1: 114 | 115 | this.modemBaudRate = SOFT_MODEM_MODE_1_BAUD_RATE; 116 | this.modemFreqLow = SOFT_MODEM_MODE_1_LOW_FREQ; 117 | this.modemFreqHigh = SOFT_MODEM_MODE_1_HIGH_FREQ; 118 | 119 | break; 120 | 121 | case SOFT_MODEM_MODE_2: 122 | 123 | this.modemBaudRate = SOFT_MODEM_MODE_2_BAUD_RATE; 124 | this.modemFreqLow = SOFT_MODEM_MODE_2_LOW_FREQ; 125 | this.modemFreqHigh = SOFT_MODEM_MODE_2_HIGH_FREQ; 126 | 127 | break; 128 | 129 | case SOFT_MODEM_MODE_3: 130 | 131 | this.modemBaudRate = SOFT_MODEM_MODE_3_BAUD_RATE; 132 | this.modemFreqLow = SOFT_MODEM_MODE_3_LOW_FREQ; 133 | this.modemFreqHigh = SOFT_MODEM_MODE_3_HIGH_FREQ; 134 | 135 | break; 136 | 137 | case SOFT_MODEM_MODE_4: 138 | 139 | this.modemBaudRate = SOFT_MODEM_MODE_4_BAUD_RATE; 140 | this.modemFreqLow = SOFT_MODEM_MODE_4_LOW_FREQ; 141 | this.modemFreqHigh = SOFT_MODEM_MODE_4_HIGH_FREQ; 142 | 143 | break; 144 | 145 | case SOFT_MODEM_MODE_5: 146 | 147 | this.modemBaudRate = SOFT_MODEM_MODE_5_BAUD_RATE; 148 | this.modemFreqLow = SOFT_MODEM_MODE_5_LOW_FREQ; 149 | this.modemFreqHigh = SOFT_MODEM_MODE_5_HIGH_FREQ; 150 | 151 | break; 152 | } 153 | 154 | if (this.sampleRate % this.modemBaudRate > 0) { 155 | // wrong config 156 | 157 | throw new IOException("Invalid sample rate or baudrate"); 158 | } 159 | 160 | this.samplesPerBit = this.sampleRate / this.modemBaudRate; 161 | 162 | this.modemFreqLowThresholdHigh = this.modemFreqLow 163 | + Math.round((this.modemFreqLow * threshold) / 100.0f); 164 | this.modemFreqHighThresholdHigh = this.modemFreqHigh 165 | + Math.round((this.modemFreqHigh * threshold) / 100.0f); 166 | 167 | if (this.pcmFormat == FSKConfig.PCM_8BIT) { 168 | this.rmsSilenceThreshold = FSKConfig.RMS_SILENCE_THRESHOLD_8BIT; 169 | } else if (this.pcmFormat == FSKConfig.PCM_16BIT) { 170 | this.rmsSilenceThreshold = FSKConfig.RMS_SILENCE_THRESHOLD_16BIT; 171 | } 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /androidfskmodem/src/main/java/bg/cytec/android/fskmodem/FSKDecoder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the FSKModem java/android library for 3 | * processing FSK audio signals. 4 | * 5 | * The FSKModem library is developed by Ivan Ganev, CEO at 6 | * Cytec BG Ltd. 7 | * 8 | * Copyright (C) 2014 Cytec BG Ltd. office@cytec.bg 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with this program. If not, see . 22 | */ 23 | 24 | package bg.cytec.android.fskmodem; 25 | 26 | import java.nio.ByteBuffer; 27 | import java.nio.ShortBuffer; 28 | 29 | public class FSKDecoder { 30 | 31 | public interface FSKDecoderCallback { 32 | /** 33 | * This method will be called every time there is new data received 34 | * @param newData 35 | */ 36 | public void decoded(byte[] newData); 37 | } 38 | 39 | /** 40 | * Decoding state-driven thread 41 | */ 42 | protected Runnable mProcessor = new Runnable() { 43 | 44 | @Override 45 | public void run() { 46 | 47 | while (mRunning) { 48 | 49 | synchronized (mSignal) { 50 | 51 | switch (mDecoderStatus) { 52 | case IDLE: 53 | stop(); 54 | 55 | break; 56 | case SEARCHING_SIGNAL:case SEARCHING_START_BIT: 57 | processIterationSearch(); 58 | 59 | break; 60 | 61 | case DECODING: 62 | processIterationDecode(); 63 | 64 | break; 65 | 66 | } 67 | } 68 | } 69 | } 70 | }; 71 | 72 | /// 73 | 74 | protected enum STATE { 75 | HIGH, LOW, SILENCE, UNKNOWN 76 | } 77 | 78 | protected enum DecoderStatus { 79 | IDLE, SEARCHING_SIGNAL, SEARCHING_START_BIT, DECODING 80 | } 81 | 82 | // / 83 | 84 | protected FSKConfig mConfig; 85 | 86 | protected FSKDecoderCallback mCallback; 87 | 88 | protected Thread mThread; 89 | 90 | protected boolean mRunning = false; 91 | 92 | protected DecoderStatus mDecoderStatus = DecoderStatus.IDLE; 93 | 94 | protected DecoderStatus mDecoderStatusPaused = DecoderStatus.IDLE; 95 | 96 | // / 97 | 98 | protected ShortBuffer mSignal; 99 | 100 | protected ShortBuffer mFrame; 101 | 102 | protected StringBuffer mBitBuffer; // concat 0 and 1 to build a binary representation of a byte 103 | 104 | protected int mCurrentBit = 0; 105 | 106 | protected int mSignalEnd = 0; // where is the end of the signal in mSignal 107 | 108 | protected int mSignalPointer = 0; //where is the current processing poing in mSignal 109 | 110 | protected int mSignalBufferSize = 0; 111 | 112 | // / 113 | 114 | protected ByteBuffer mData; 115 | 116 | protected int mDataLength = 0; 117 | 118 | // / 119 | 120 | /** 121 | * Create FSK Decoder instance to feed with audio data 122 | * @param config 123 | * @param callback 124 | */ 125 | public FSKDecoder(FSKConfig config, FSKDecoderCallback callback) { 126 | mConfig = config; 127 | 128 | mCallback = callback; 129 | 130 | mSignalBufferSize = mConfig.sampleRate; // 1 second buffer 131 | 132 | allocateBufferSignal(); 133 | 134 | allocateBufferFrame(); 135 | 136 | allocateBufferData(); 137 | } 138 | 139 | @Override 140 | protected void finalize() throws Throwable { 141 | 142 | stop(); 143 | 144 | super.finalize(); 145 | } 146 | 147 | // / 148 | 149 | protected void notifyCallback(byte[] data) { 150 | if (mCallback != null) { 151 | mCallback.decoded(data); 152 | } 153 | } 154 | 155 | protected void start() { 156 | if (!mRunning) { 157 | if (!mDecoderStatusPaused.equals(DecoderStatus.IDLE)) { 158 | setStatus(mDecoderStatusPaused); //resume task 159 | } 160 | else { 161 | setStatus(DecoderStatus.SEARCHING_SIGNAL); //start new process 162 | } 163 | 164 | mRunning = true; 165 | 166 | mThread = new Thread(mProcessor); 167 | mThread.setPriority(Thread.MIN_PRIORITY); 168 | mThread.start(); 169 | } 170 | } 171 | 172 | /** 173 | * Stop decoding process 174 | */ 175 | public void stop() { 176 | if (mRunning) { 177 | if (mThread != null && mThread.isAlive()) { 178 | mRunning = false; 179 | 180 | mThread.interrupt(); 181 | } 182 | } 183 | } 184 | 185 | protected void allocateBufferSignal() { 186 | mSignal = ShortBuffer.allocate(mSignalBufferSize); 187 | } 188 | 189 | protected void allocateBufferFrame() { 190 | mFrame = ShortBuffer.allocate(mConfig.samplesPerBit); // one frame contains one bit 191 | } 192 | 193 | protected void allocateBufferData() { 194 | mData = ByteBuffer.allocate(FSKConfig.DECODER_DATA_BUFFER_SIZE); // maximum bytes 195 | } 196 | 197 | protected void nextStatus() { 198 | switch (mDecoderStatus) { 199 | case IDLE: 200 | setStatus(DecoderStatus.SEARCHING_SIGNAL); 201 | break; 202 | case SEARCHING_SIGNAL: 203 | setStatus(DecoderStatus.SEARCHING_START_BIT); 204 | break; 205 | case SEARCHING_START_BIT: 206 | setStatus(DecoderStatus.DECODING); 207 | break; 208 | case DECODING: 209 | setStatus(DecoderStatus.IDLE); 210 | break; 211 | } 212 | } 213 | 214 | protected void setStatus(DecoderStatus status) { 215 | setStatus(status, DecoderStatus.IDLE); 216 | } 217 | 218 | protected void setStatus(DecoderStatus status, DecoderStatus paused) { 219 | mDecoderStatus = status; 220 | mDecoderStatusPaused = paused; 221 | } 222 | 223 | protected short[] byteArrayToShortArray(byte[] data) { 224 | int size = data.length; 225 | short[] result = new short[size]; 226 | 227 | for (int i = 0; i < size; i++) { 228 | result[i] = (short) data[i]; 229 | } 230 | 231 | return result; 232 | } 233 | 234 | protected short[] convertToMono(short[] data) { 235 | 236 | short[] monoData = new short[data.length/2]; 237 | 238 | for (int i = 0; i < data.length-1; i+=2) { 239 | int mixed = ((int)(data[i] + data[i+1]))/2; 240 | 241 | monoData[i/2] = (short) mixed; 242 | } 243 | 244 | return monoData; 245 | } 246 | 247 | // / 248 | 249 | protected void trimSignal() { 250 | 251 | if (mSignalPointer <= mSignalEnd) { 252 | 253 | short[] currentData = mSignal.array(); 254 | short[] remainingData = new short[mSignalEnd - mSignalPointer]; 255 | 256 | for (int i = 0; i < remainingData.length; i++) { 257 | remainingData[i] = currentData[mSignalPointer+i]; 258 | } 259 | 260 | allocateBufferSignal(); 261 | mSignal.put(remainingData); 262 | mSignal.rewind(); 263 | 264 | mSignalPointer = 0; 265 | mSignalEnd = remainingData.length; 266 | } 267 | else { 268 | clearSignal(); 269 | } 270 | } 271 | 272 | /** 273 | * Use this method to feed 8bit PCM data to the decoder 274 | * @param data 275 | * @return samples space left in the buffer 276 | */ 277 | public int appendSignal(byte[] data) { 278 | return appendSignal(byteArrayToShortArray(data)); 279 | } 280 | 281 | /** 282 | * Use this method to feed 16bit PCM data to the decoder 283 | * @param data 284 | * @return samples space left in the buffer 285 | */ 286 | public int appendSignal(short[] data) { 287 | 288 | synchronized (mSignal) { 289 | short[] monoData; 290 | 291 | if (mConfig.channels == FSKConfig.CHANNELS_STEREO) { 292 | monoData = convertToMono(data); 293 | } 294 | else { 295 | monoData = data; 296 | } 297 | 298 | if (mSignalEnd + monoData.length > mSignal.capacity()) { 299 | //the buffer will overflow... attempt to trim data 300 | 301 | if ((mSignalEnd + monoData.length)-mSignalPointer <= mSignal.capacity()) { 302 | //we can cut off part of the data to fit the rest 303 | 304 | trimSignal(); 305 | } 306 | else { 307 | //the decoder is gagging 308 | 309 | return (mSignal.capacity() - (mSignalEnd + monoData.length)); // refuse data and tell the amount of overflow 310 | } 311 | } 312 | 313 | mSignal.position(mSignalEnd); 314 | mSignal.put(monoData); 315 | 316 | mSignalEnd += monoData.length; 317 | 318 | start(); //if idle 319 | 320 | return (mSignal.capacity() - mSignalEnd); //return the remaining amount of space in the buffer 321 | } 322 | } 323 | 324 | /** 325 | * Use this method to set 8bit PCM data to the decoder 326 | * @param data 327 | * @return samples space left in the buffer 328 | */ 329 | public int setSignal(byte[] data) { 330 | allocateBufferData(); //reset data buffer 331 | 332 | clearSignal(); 333 | 334 | return appendSignal(byteArrayToShortArray(data)); 335 | } 336 | 337 | /** 338 | * Use this method to set 16bit PCM data to the decoder 339 | * @param data 340 | * @return samples space left in the buffer 341 | */ 342 | public int setSignal(short[] data) { 343 | allocateBufferData(); //reset data buffer 344 | 345 | clearSignal(); 346 | 347 | return appendSignal(data); 348 | } 349 | 350 | /** 351 | * Use this method to destroy all data currently queued for decoding 352 | * @return samples space left in the buffer 353 | */ 354 | public int clearSignal() { 355 | synchronized (mSignal) { 356 | allocateBufferSignal(); 357 | 358 | mSignalEnd = 0; 359 | mSignalPointer = 0; 360 | 361 | return mSignal.capacity(); 362 | } 363 | } 364 | 365 | /// 366 | 367 | protected int calcFrequencyZerocrossing(short[] data) { 368 | int numSamples = data.length; 369 | int numCrossing = 0; 370 | 371 | for (int i = 0; i < numSamples-1; i++) { 372 | if ((data[i] > 0 && data[i+1] <= 0) 373 | || (data[i] < 0 && data[i+1] >= 0)) { 374 | 375 | numCrossing++; 376 | } 377 | } 378 | 379 | double numSecondsRecorded = (double) numSamples 380 | / (double) mConfig.sampleRate; 381 | double numCycles = numCrossing / 2; 382 | double frequency = numCycles / numSecondsRecorded; 383 | 384 | return (int) Math.round(frequency); 385 | } 386 | 387 | protected double rootMeanSquared(short[] data) { 388 | double ms = 0; 389 | 390 | for (int i = 0; i < data.length; i++) { 391 | ms += data[i] * data[i]; 392 | } 393 | 394 | ms /= data.length; 395 | 396 | return Math.sqrt(ms); 397 | } 398 | 399 | protected STATE determineState(int frequency, double rms) { 400 | 401 | STATE state = STATE.UNKNOWN; 402 | 403 | if (rms <= mConfig.rmsSilenceThreshold) { 404 | state = STATE.SILENCE; 405 | } 406 | else if (frequency <= mConfig.modemFreqLowThresholdHigh) { 407 | state = STATE.LOW; 408 | } 409 | else if (frequency <= mConfig.modemFreqHighThresholdHigh) { 410 | state = STATE.HIGH; 411 | } 412 | 413 | return state; 414 | } 415 | 416 | protected short[] getFrameData(int position) { 417 | mSignal.position(position); 418 | 419 | allocateBufferFrame(); 420 | 421 | for (int j = 0; j < mConfig.samplesPerBit; j++) { 422 | mFrame.put(j, mSignal.get()); 423 | } 424 | 425 | return mFrame.array(); 426 | } 427 | 428 | protected void flushData() { 429 | if (mDataLength > 0) { 430 | byte[] data = new byte[mDataLength]; 431 | 432 | for (int i = 0; i < mDataLength; i++) { 433 | data[i] = mData.get(i); 434 | } 435 | 436 | allocateBufferData(); 437 | 438 | mDataLength = 0; 439 | 440 | notifyCallback(data); 441 | } 442 | } 443 | 444 | /// 445 | 446 | protected void processIterationSearch() { 447 | if (mSignalPointer <= mSignalEnd-mConfig.samplesPerBit) { 448 | 449 | short[] frameData = getFrameData(mSignalPointer); 450 | 451 | int freq = calcFrequencyZerocrossing(frameData); 452 | 453 | STATE state = determineState(freq, rootMeanSquared(frameData)); 454 | 455 | if (state.equals(STATE.HIGH) && mDecoderStatus.equals(DecoderStatus.SEARCHING_SIGNAL)) { 456 | //found pre-carrier bit 457 | nextStatus(); //start searching for start bit 458 | } 459 | 460 | if (mDecoderStatus.equals(DecoderStatus.SEARCHING_START_BIT) && freq == mConfig.modemFreqLow && state.equals(STATE.LOW)) { 461 | //found start bit 462 | 463 | mSignalPointer += (mConfig.samplesPerBit/2); //shift 0.5 period forward 464 | 465 | nextStatus(); //begin decoding 466 | 467 | return; 468 | } 469 | 470 | mSignalPointer++; 471 | } 472 | else { 473 | trimSignal(); //get rid of data that is already processed 474 | 475 | flushData(); 476 | 477 | setStatus(DecoderStatus.IDLE); 478 | } 479 | } 480 | 481 | protected void processIterationDecode() { 482 | 483 | if (mSignalPointer <= mSignalEnd-mConfig.samplesPerBit) { 484 | 485 | short[] frameData = getFrameData(mSignalPointer); 486 | 487 | double rms = rootMeanSquared(frameData); 488 | 489 | int freq = calcFrequencyZerocrossing(frameData); 490 | 491 | STATE state = determineState(freq, rms); 492 | 493 | if (mCurrentBit == 0 && state.equals(STATE.LOW)) { 494 | //start bit 495 | 496 | //prepare buffers 497 | mBitBuffer = new StringBuffer(); 498 | mCurrentBit++; 499 | } 500 | else if (mCurrentBit == 0 && state.equals(STATE.HIGH)) { 501 | //post-carrier bit(s) 502 | 503 | //go searching for a new transmission 504 | setStatus(DecoderStatus.SEARCHING_START_BIT); 505 | } 506 | else if (mCurrentBit == 9 && state.equals(STATE.HIGH)) { 507 | //end bit 508 | 509 | try { 510 | mData.put((byte) Integer.parseInt(mBitBuffer.toString(), 2)); 511 | 512 | mDataLength++; 513 | 514 | if (mDataLength == mData.capacity()) { 515 | //the data buffer is full, puke back to application and cleanup 516 | 517 | flushData(); 518 | } 519 | } 520 | catch (Exception e) { 521 | e.printStackTrace(); 522 | } 523 | 524 | mCurrentBit = 0; 525 | } 526 | else if (mCurrentBit > 0 && mCurrentBit < 9 && (state.equals(STATE.HIGH) || state.equals(STATE.LOW))) { 527 | 528 | mBitBuffer.insert(0, (state.equals(STATE.HIGH) ? 1 : 0)); 529 | 530 | mCurrentBit++; 531 | } 532 | else { 533 | //corrupted data, clear bit buffer 534 | 535 | mBitBuffer = new StringBuffer(); 536 | mCurrentBit = 0; 537 | 538 | setStatus(DecoderStatus.SEARCHING_START_BIT); 539 | } 540 | 541 | mSignalPointer += mConfig.samplesPerBit; 542 | } 543 | else { 544 | 545 | trimSignal(); //get rid of data that is already processed 546 | 547 | flushData(); 548 | 549 | setStatus(DecoderStatus.IDLE, DecoderStatus.DECODING); //we need to wait for more data to continue decoding 550 | } 551 | } 552 | 553 | } 554 | -------------------------------------------------------------------------------- /androidfskmodem/src/main/java/bg/cytec/android/fskmodem/FSKEncoder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the FSKModem java/android library for 3 | * processing FSK audio signals. 4 | * 5 | * The FSKModem library is developed by Ivan Ganev, CEO at 6 | * Cytec BG Ltd. 7 | * 8 | * Copyright (C) 2014 Cytec BG Ltd. office@cytec.bg 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with this program. If not, see . 22 | */ 23 | 24 | package bg.cytec.android.fskmodem; 25 | 26 | import java.nio.ByteBuffer; 27 | import java.nio.ShortBuffer; 28 | 29 | public class FSKEncoder { 30 | 31 | public interface FSKEncoderCallback { 32 | /** 33 | * This method will be called every time there is new data encoded 34 | * @param newData 35 | */ 36 | public void encoded(byte[] pcm8, short[] pcm16); 37 | } 38 | 39 | /** 40 | * Encoding thread 41 | */ 42 | protected Runnable mProcessor = new Runnable() { 43 | 44 | @Override 45 | public void run() { 46 | 47 | while (mRunning) { 48 | synchronized (mData) { 49 | 50 | switch (mEncoderStatus) { 51 | case IDLE: 52 | stop(); 53 | 54 | break; 55 | case PRE_CARRIER:case POST_CARRIER: 56 | processIterationCarrier(); 57 | 58 | break; 59 | 60 | case ENCODING: 61 | processIterationEncoding(); 62 | 63 | break; 64 | 65 | case SILENCE: 66 | processIterationSilence(); 67 | 68 | break; 69 | 70 | } 71 | } 72 | } 73 | } 74 | }; 75 | 76 | /// 77 | 78 | protected enum STATE { 79 | HIGH, LOW, SILENCE 80 | } 81 | 82 | protected enum EncoderStatus { 83 | IDLE, PRE_CARRIER, ENCODING, POST_CARRIER, SILENCE 84 | } 85 | 86 | /// 87 | 88 | protected FSKConfig mConfig; 89 | 90 | protected FSKEncoderCallback mCallback; 91 | 92 | protected Thread mThread; 93 | 94 | protected boolean mRunning = false; 95 | 96 | protected EncoderStatus mEncoderStatus = EncoderStatus.IDLE; 97 | 98 | /// 99 | 100 | protected ShortBuffer mSignalPCM16; 101 | 102 | protected ByteBuffer mSignalPCM8; 103 | 104 | protected int mSignalLength = 0; 105 | 106 | /// 107 | 108 | protected ByteBuffer mData; 109 | 110 | protected int mDataLength = 0; 111 | 112 | protected int mDataPointer = 0; 113 | 114 | /// 115 | 116 | protected int mPreCarrierBits = 0; 117 | 118 | protected int mPostCarrierBits = 0; 119 | 120 | protected int mSilenceBits = 0; 121 | 122 | /// 123 | 124 | /** 125 | * Create FSK Encoder instance to feed with byte array data 126 | * @param config 127 | * @param callback 128 | */ 129 | public FSKEncoder(FSKConfig config, FSKEncoderCallback callback) { 130 | mConfig = config; 131 | 132 | mCallback = callback; 133 | 134 | /// 135 | 136 | mPreCarrierBits = FSKConfig.ENCODER_PRE_CARRIER_BITS; //(int) Math.ceil(mConfig.sampleRate * 40.0f / 1000.0f / mConfig.samplesPerBit); 137 | mPostCarrierBits = FSKConfig.ENCODER_POST_CARRIER_BITS; //(int) Math.ceil(mConfig.sampleRate * 5.0f / 1000.0f / mConfig.samplesPerBit); 138 | mSilenceBits = FSKConfig.ENCODER_SILENCE_BITS;//(int) Math.ceil(mConfig.sampleRate * 5.0f / 1000.0f / mConfig.samplesPerBit); 139 | 140 | /// 141 | 142 | allocateBufferSignal(); 143 | 144 | allocateBufferData(); 145 | } 146 | 147 | @Override 148 | protected void finalize() throws Throwable { 149 | 150 | stop(); 151 | 152 | super.finalize(); 153 | } 154 | 155 | // / 156 | 157 | protected void notifyCallback(byte[] pcm8, short[] pcm16) { 158 | if (mCallback != null) { 159 | mCallback.encoded(pcm8, pcm16); 160 | } 161 | } 162 | 163 | protected void start() { 164 | if (!mRunning) { 165 | 166 | setStatus(EncoderStatus.PRE_CARRIER); 167 | 168 | mRunning = true; 169 | 170 | mThread = new Thread(mProcessor); 171 | mThread.setPriority(Thread.MIN_PRIORITY); 172 | mThread.start(); 173 | } 174 | } 175 | 176 | /** 177 | * Stop decoding process 178 | */ 179 | public void stop() { 180 | if (mRunning) { 181 | if (mThread != null && mThread.isAlive()) { 182 | mRunning = false; 183 | 184 | mThread.interrupt(); 185 | } 186 | } 187 | } 188 | 189 | protected void nextStatus() { 190 | switch (mEncoderStatus) { 191 | case IDLE: 192 | setStatus(EncoderStatus.PRE_CARRIER); 193 | break; 194 | case PRE_CARRIER: 195 | setStatus(EncoderStatus.ENCODING); 196 | break; 197 | case ENCODING: 198 | setStatus(EncoderStatus.POST_CARRIER); 199 | break; 200 | case POST_CARRIER: 201 | setStatus(EncoderStatus.SILENCE); 202 | break; 203 | case SILENCE: 204 | setStatus(EncoderStatus.IDLE); 205 | break; 206 | } 207 | } 208 | 209 | protected void setStatus(EncoderStatus status) { 210 | mEncoderStatus = status; 211 | } 212 | 213 | protected void allocateBufferSignal() { 214 | if (mConfig.pcmFormat == FSKConfig.PCM_8BIT) { 215 | mSignalPCM8 = ByteBuffer.allocate(mConfig.sampleRate); //1 second buffer 216 | } 217 | else if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 218 | mSignalPCM16 = ShortBuffer.allocate(mConfig.sampleRate); //1 second buffer 219 | } 220 | } 221 | 222 | protected void allocateBufferData() { 223 | mData = ByteBuffer.allocate(FSKConfig.ENCODER_DATA_BUFFER_SIZE); 224 | } 225 | 226 | protected void trimData() { 227 | 228 | if (mDataPointer <= mDataLength) { 229 | 230 | byte[] currentData = mData.array(); 231 | byte[] remainingData = new byte[mDataLength - mDataPointer]; 232 | 233 | for (int i = 0; i < remainingData.length; i++) { 234 | remainingData[i] = currentData[mDataPointer+i]; 235 | } 236 | 237 | mData = ByteBuffer.allocate(FSKConfig.ENCODER_DATA_BUFFER_SIZE); 238 | mData.put(remainingData); 239 | mData.rewind(); 240 | 241 | mDataPointer = 0; 242 | mDataLength = remainingData.length; 243 | } 244 | else { 245 | clearData(); 246 | } 247 | } 248 | 249 | /** 250 | * Use this method to feed byte array data to the encoder 251 | * @param data 252 | * @return bytes left in the buffer 253 | */ 254 | public int appendData(byte[] data) { 255 | 256 | synchronized (mData) { 257 | 258 | if (mDataLength + data.length > mData.capacity()) { 259 | //the buffer will overflow... attempt to trim data 260 | 261 | if ((mDataLength + data.length)-mDataPointer <= mData.capacity()) { 262 | //we can cut off part of the data to fit the rest 263 | 264 | trimData(); 265 | } 266 | else { 267 | //the encoder is gagging 268 | 269 | return (mData.capacity() - (mDataLength + data.length)); // refuse data and tell the amount of overflow 270 | } 271 | } 272 | 273 | mData.position(mDataLength); 274 | mData.put(data); 275 | 276 | mDataLength += data.length; 277 | 278 | start(); //if idle 279 | 280 | return (mData.capacity() - mDataLength); //return the remaining amount of space in the buffer 281 | } 282 | } 283 | 284 | /** 285 | * Use this method to set byte array data to the encoder 286 | * @param data 287 | * @return bytes left in the buffer 288 | */ 289 | public int setData(byte[] data) { 290 | allocateBufferSignal(); //reset signal output buffer 291 | 292 | clearData(); 293 | 294 | return appendData(data); 295 | } 296 | 297 | /** 298 | * Use this method to destroy all data currently queued for encoding 299 | * @return bytes left in the buffer 300 | */ 301 | public int clearData() { 302 | synchronized (mData) { 303 | allocateBufferSignal(); 304 | 305 | mDataLength = 0; 306 | mDataPointer = 0; 307 | 308 | return mData.capacity(); 309 | } 310 | } 311 | 312 | protected void flushSignal() { 313 | if (mSignalLength > 0) { 314 | 315 | if (mConfig.pcmFormat == FSKConfig.PCM_8BIT) { 316 | byte[] dataPCM8 = new byte[mSignalLength]; 317 | 318 | for (int i = 0; i < mSignalLength; i++) { 319 | dataPCM8[i] = mSignalPCM8.get(i); 320 | } 321 | 322 | notifyCallback(dataPCM8, null); 323 | } 324 | else if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 325 | short[] dataPCM16 = new short[mSignalLength]; 326 | 327 | for (int i = 0; i < mSignalLength; i++) { 328 | dataPCM16[i] = mSignalPCM16.get(i); 329 | } 330 | 331 | notifyCallback(null, dataPCM16); 332 | } 333 | 334 | mSignalLength = 0; 335 | 336 | allocateBufferSignal(); 337 | } 338 | } 339 | 340 | protected void checkSignalBuffer() { 341 | if (mConfig.pcmFormat == FSKConfig.PCM_8BIT) { 342 | if (mSignalLength >= mSignalPCM8.capacity() - 2) { 343 | //time to flush 344 | 345 | flushSignal(); 346 | } 347 | } 348 | else { 349 | if (mSignalLength >= mSignalPCM16.capacity() - 2) { 350 | //time to flush 351 | 352 | flushSignal(); 353 | } 354 | } 355 | } 356 | 357 | protected void modulate(STATE state) { 358 | if (!state.equals(STATE.SILENCE)) { 359 | //high or low bit 360 | 361 | if (mConfig.pcmFormat == FSKConfig.PCM_8BIT) { 362 | byte[] newData = modulate8(state); 363 | 364 | mSignalPCM8.position(mSignalLength); 365 | 366 | for (int i = 0; i < mConfig.samplesPerBit; i++) { 367 | 368 | mSignalPCM8.put(newData[i]); 369 | 370 | mSignalLength++; 371 | 372 | if (mConfig.channels == FSKConfig.CHANNELS_STEREO) { 373 | mSignalPCM8.put(newData[i]); 374 | 375 | mSignalLength++; 376 | } 377 | 378 | checkSignalBuffer(); 379 | } 380 | } 381 | else if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 382 | short[] newData = modulate16(state); 383 | 384 | for (int i = 0; i < mConfig.samplesPerBit; i++) { 385 | mSignalPCM16.put(newData[i]); 386 | 387 | mSignalLength++; 388 | 389 | if (mConfig.channels == FSKConfig.CHANNELS_STEREO) { 390 | mSignalPCM16.put(newData[i]); 391 | 392 | mSignalLength++; 393 | } 394 | 395 | checkSignalBuffer(); 396 | } 397 | } 398 | } 399 | else { 400 | if (mConfig.pcmFormat == FSKConfig.PCM_8BIT) { 401 | mSignalPCM8.position(mSignalLength); 402 | 403 | for (int i = 0; i < mConfig.samplesPerBit; i++) { 404 | mSignalPCM8.put((byte) 0); 405 | 406 | mSignalLength++; 407 | 408 | if (mConfig.channels == FSKConfig.CHANNELS_STEREO) { 409 | mSignalPCM8.put((byte) 0); 410 | 411 | mSignalLength++; 412 | } 413 | 414 | checkSignalBuffer(); 415 | } 416 | } 417 | else if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 418 | mSignalPCM16.position(mSignalLength); 419 | 420 | for (int i = 0; i < mConfig.samplesPerBit; i++) { 421 | mSignalPCM16.put((short) 0); 422 | 423 | mSignalLength++; 424 | 425 | if (mConfig.channels == FSKConfig.CHANNELS_STEREO) { 426 | mSignalPCM16.put((short) 0); 427 | 428 | mSignalLength++; 429 | } 430 | 431 | checkSignalBuffer(); 432 | } 433 | } 434 | } 435 | } 436 | 437 | protected byte[] modulate8(STATE state) { 438 | int freq = 0; 439 | byte[] buffer = new byte[mConfig.samplesPerBit]; 440 | 441 | if (state.equals(STATE.HIGH)) { 442 | freq = mConfig.modemFreqHigh; 443 | } 444 | else { 445 | freq = mConfig.modemFreqLow; 446 | } 447 | 448 | for (int i = 0; i < buffer.length; i++) { 449 | buffer[i] = (byte) (128 + 127 * Math.sin((2 * Math.PI) * (i*1.0f / mConfig.sampleRate) * freq)); 450 | } 451 | 452 | return buffer; 453 | } 454 | 455 | protected short[] modulate16(STATE state) { 456 | int freq = 0; 457 | short[] buffer = new short[mConfig.samplesPerBit]; 458 | 459 | if (state.equals(STATE.HIGH)) { 460 | freq = mConfig.modemFreqHigh; 461 | } 462 | else { 463 | freq = mConfig.modemFreqLow; 464 | } 465 | 466 | for (int i = 0; i < buffer.length; i++) { 467 | buffer[i] = (short) (32767 * Math.sin((2 * Math.PI) * (i*1.0f / mConfig.sampleRate) * freq)); 468 | } 469 | 470 | return buffer; 471 | } 472 | 473 | protected void processIterationCarrier() { 474 | if (mEncoderStatus.equals(EncoderStatus.PRE_CARRIER)) { 475 | //40ms HIGH signal 476 | 477 | for (int i = 0; i < mPreCarrierBits; i++) { 478 | modulate(STATE.HIGH); 479 | } 480 | 481 | } 482 | else if (mEncoderStatus.equals(EncoderStatus.POST_CARRIER)) { 483 | //5ms HIGH signal as a push bit to end transmission 484 | 485 | for (int i = 0; i < mPostCarrierBits; i++) { 486 | modulate(STATE.HIGH); 487 | } 488 | } 489 | 490 | nextStatus(); 491 | } 492 | 493 | protected void processIterationEncoding() { 494 | if (mDataPointer < mDataLength) { 495 | mData.position(mDataPointer); 496 | 497 | byte data = mData.get(); 498 | 499 | modulate(STATE.LOW); //start bit 500 | 501 | //modulate data bits 502 | for(byte mask = 1; mask != 0; mask <<= 1) { 503 | if((data & mask) > 0){ 504 | modulate(STATE.HIGH); 505 | } 506 | else{ 507 | modulate(STATE.LOW); 508 | } 509 | } 510 | 511 | modulate(STATE.HIGH); //end bit 512 | 513 | mDataPointer++; 514 | } 515 | else { 516 | nextStatus(); 517 | } 518 | } 519 | 520 | protected void processIterationSilence() { 521 | for (int i = 0; i < mSilenceBits; i++) { 522 | modulate(STATE.SILENCE); 523 | } 524 | 525 | flushSignal(); 526 | 527 | nextStatus(); 528 | } 529 | 530 | } 531 | -------------------------------------------------------------------------------- /arduino/SoftTermExample/SoftTermExample.ino: -------------------------------------------------------------------------------- 1 | #include //ライブラリをインクルード 2 | #include 3 | 4 | SoftModem modem; //SoftModemのインスタンスを作る 5 | //int delayTime = 1000; 6 | int txLed = 13; 7 | int rxLed = 12; 8 | 9 | void setup() 10 | { 11 | Serial.begin(57600); 12 | modem.begin(); // setup()でbeginをコールする 13 | pinMode(txLed, OUTPUT); 14 | pinMode(rxLed, OUTPUT); 15 | } 16 | 17 | void loop() 18 | { 19 | while (modem.available()) { //iPhoneからデータ受信しているか確認 20 | blinkLed(rxLed); 21 | int c = modem.read(); //1byteリード 22 | if (isprint(c)) { 23 | Serial.println((char)c); //PCに送信 24 | } 25 | else { 26 | Serial.print("("); //表示できない文字はHexで表示 27 | Serial.print(c, HEX); 28 | Serial.print(")"); 29 | } 30 | } 31 | while (Serial.available()) { //PCからデータを受信しているか確認 32 | blinkLed(txLed); 33 | char c = Serial.read(); //1byteリード 34 | modem.write(c); //iPhoneに送信 35 | 36 | } 37 | 38 | // blinkLed(); 39 | // modem.write('a'); 40 | // delayMicroseconds(delayTime); 41 | // modem.write('b'); 42 | // delayMicroseconds(delayTime); 43 | // modem.write('c'); 44 | // delayMicroseconds(delayTime); 45 | // modem.write('d'); 46 | // delayMicroseconds(delayTime); 47 | // modem.write('e'); 48 | // delayMicroseconds(delayTime); 49 | // modem.write(' '); 50 | // delay(100); 51 | } 52 | 53 | void blinkLed(int led){ 54 | digitalWrite(led, HIGH); 55 | delay(100); 56 | digitalWrite(led, LOW); 57 | delay(100); 58 | } 59 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:1.2.3' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fSKModemTerminal/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "23.0.0 rc2" 6 | 7 | defaultConfig { 8 | applicationId "bg.cytec.android.fskmodem.examples.terminal" 9 | minSdkVersion 8 10 | targetSdkVersion 19 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile project(':androidfskmodem') 23 | compile 'com.android.support:appcompat-v7:19.1.0' 24 | compile 'com.android.support:support-v4:19.1.0' 25 | } 26 | -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/java/bg/cytec/android/fskmodem/examples/terminal/MainActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the FSKModem java/android library for 3 | * processing FSK audio signals. 4 | * 5 | * The FSKModem library is developed by Ivan Ganev, CEO at 6 | * Cytec BG Ltd. 7 | * 8 | * Copyright (C) 2014 Cytec BG Ltd. office@cytec.bg 9 | * 10 | * This program is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU General Public License as published by 12 | * the Free Software Foundation, either version 3 of the License, or 13 | * (at your option) any later version. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU General Public License 21 | * along with this program. If not, see . 22 | */ 23 | 24 | package bg.cytec.android.fskmodem.examples.terminal; 25 | 26 | import java.io.IOException; 27 | 28 | import bg.cytec.android.fskmodem.FSKConfig; 29 | import bg.cytec.android.fskmodem.FSKDecoder; 30 | import bg.cytec.android.fskmodem.FSKDecoder.FSKDecoderCallback; 31 | import bg.cytec.android.fskmodem.FSKEncoder.FSKEncoderCallback; 32 | import bg.cytec.android.fskmodem.FSKEncoder; 33 | import android.support.v7.app.ActionBarActivity; 34 | import android.util.Log; 35 | import android.view.KeyEvent; 36 | import android.view.MotionEvent; 37 | import android.view.View; 38 | import android.view.View.OnTouchListener; 39 | import android.view.inputmethod.EditorInfo; 40 | import android.widget.EditText; 41 | import android.widget.ScrollView; 42 | import android.widget.TextView; 43 | import android.widget.TextView.OnEditorActionListener; 44 | import android.annotation.SuppressLint; 45 | import android.media.AudioFormat; 46 | import android.media.AudioManager; 47 | import android.media.AudioRecord; 48 | import android.media.AudioTrack; 49 | import android.media.MediaRecorder.AudioSource; 50 | import android.os.Bundle; 51 | 52 | public class MainActivity extends ActionBarActivity { 53 | 54 | protected FSKConfig mConfig; 55 | protected FSKEncoder mEncoder; 56 | protected FSKDecoder mDecoder; 57 | 58 | protected AudioTrack mAudioTrack; 59 | 60 | protected AudioRecord mRecorder; 61 | 62 | protected int mBufferSize = 0; 63 | 64 | protected boolean mScrollLock = true; 65 | 66 | protected ScrollView mScroll; 67 | protected TextView mTerminal; 68 | protected EditText mInput; 69 | 70 | protected Runnable mRecordFeed = new Runnable() { 71 | 72 | @Override 73 | public void run() { 74 | 75 | while (mRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { 76 | 77 | short[] data = new short[mBufferSize/2]; //the buffer size is in bytes 78 | 79 | // gets the audio output from microphone to short array samples 80 | mRecorder.read(data, 0, mBufferSize/2); 81 | 82 | mDecoder.appendSignal(data); 83 | } 84 | } 85 | }; 86 | 87 | @Override 88 | protected void onCreate(Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | setContentView(R.layout.activity_main); 91 | 92 | // INIT VIEWS 93 | 94 | mTerminal = (TextView) findViewById(R.id.terminal); 95 | mInput = (EditText) findViewById(R.id.input); 96 | 97 | mInput.setOnEditorActionListener(new OnEditorActionListener() { 98 | 99 | @Override 100 | public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { 101 | 102 | if (actionId == EditorInfo.IME_ACTION_SEND) { 103 | 104 | String text = view.getText().toString()+"\n"; 105 | 106 | mEncoder.appendData(text.getBytes()); 107 | 108 | mTerminal.setText(mTerminal.getText()+ "\nSend: " + text); 109 | 110 | mInput.setText(""); 111 | 112 | if (mScrollLock) { 113 | mScroll.fullScroll(ScrollView.FOCUS_DOWN); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | }); 122 | 123 | mScroll = (ScrollView) findViewById(R.id.scrollview); 124 | 125 | mScroll.setOnTouchListener(new OnTouchListener() { 126 | 127 | @SuppressLint("ClickableViewAccessibility") 128 | @Override 129 | public boolean onTouch(View view, MotionEvent event) { 130 | 131 | if (event.getAction() == MotionEvent.ACTION_UP) { 132 | //finger released, detect if view is scrolled to bottom 133 | 134 | int diff = (mScroll.getScrollY()+mScroll.getHeight()) - mTerminal.getHeight(); 135 | 136 | if (diff == 0) { 137 | mScrollLock = true; 138 | } 139 | } 140 | else if (event.getAction() == MotionEvent.ACTION_DOWN) { 141 | //finger placed down, lock scroll 142 | 143 | mScrollLock = false; 144 | } 145 | 146 | return false; 147 | } 148 | }); 149 | 150 | /// INIT FSK CONFIG 151 | 152 | try { 153 | mConfig = new FSKConfig(FSKConfig.SAMPLE_RATE_44100, FSKConfig.PCM_16BIT, FSKConfig.CHANNELS_MONO, FSKConfig.SOFT_MODEM_MODE_4, FSKConfig.THRESHOLD_20P); 154 | } catch (IOException e1) { 155 | e1.printStackTrace(); 156 | } 157 | 158 | /// INIT FSK DECODER 159 | 160 | mDecoder = new FSKDecoder(mConfig, new FSKDecoderCallback() { 161 | 162 | @Override 163 | public void decoded(byte[] newData) { 164 | 165 | final String text = new String(newData); 166 | 167 | runOnUiThread(new Runnable() { 168 | public void run() { 169 | 170 | mTerminal.setText(mTerminal.getText()+text); 171 | 172 | if (mScrollLock) { 173 | mScroll.fullScroll(ScrollView.FOCUS_DOWN); 174 | } 175 | } 176 | }); 177 | } 178 | }); 179 | 180 | /// INIT FSK ENCODER 181 | 182 | mEncoder = new FSKEncoder(mConfig, new FSKEncoderCallback() { 183 | 184 | @Override 185 | public void encoded(byte[] pcm8, short[] pcm16) { 186 | if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 187 | //16bit buffer is populated, 8bit buffer is null 188 | 189 | mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 190 | mConfig.sampleRate, AudioFormat.CHANNEL_OUT_MONO, 191 | AudioFormat.ENCODING_PCM_16BIT, pcm16.length*2, 192 | AudioTrack.MODE_STATIC); 193 | 194 | mAudioTrack.write(pcm16, 0, pcm16.length); 195 | 196 | mAudioTrack.play(); 197 | } 198 | } 199 | }); 200 | 201 | /// 202 | 203 | //make sure that the settings of the recorder match the settings of the decoder 204 | //most devices cant record anything but 44100 samples in 16bit PCM format... 205 | mBufferSize = AudioRecord.getMinBufferSize(FSKConfig.SAMPLE_RATE_44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 206 | 207 | //scale up the buffer... reading larger amounts of data 208 | //minimizes the chance of missing data because of thread priority 209 | mBufferSize *= 10; 210 | 211 | //again, make sure the recorder settings match the decoder settings 212 | mRecorder = new AudioRecord(AudioSource.MIC, FSKConfig.SAMPLE_RATE_44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mBufferSize); 213 | 214 | if (mRecorder.getState() == AudioRecord.STATE_INITIALIZED) { 215 | mRecorder.startRecording(); 216 | 217 | //start a thread to read the audio data 218 | Thread thread = new Thread(mRecordFeed); 219 | thread.setPriority(Thread.MAX_PRIORITY); 220 | thread.start(); 221 | } 222 | else { 223 | Log.i("FSKDecoder", "Please check the recorder settings, something is wrong!"); 224 | } 225 | } 226 | 227 | @Override 228 | protected void onDestroy() { 229 | 230 | mDecoder.stop(); 231 | mEncoder.stop(); 232 | 233 | if (mRecorder != null && mRecorder.getState() == AudioRecord.STATE_INITIALIZED) 234 | { 235 | mRecorder.stop(); 236 | mRecorder.release(); 237 | } 238 | 239 | if (mAudioTrack != null && mAudioTrack.getPlayState() == AudioTrack.STATE_INITIALIZED) 240 | { 241 | mAudioTrack.stop(); 242 | mAudioTrack.release(); 243 | } 244 | 245 | super.onDestroy(); 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/fSKModemTerminal/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/fSKModemTerminal/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/fSKModemTerminal/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/fSKModemTerminal/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 24 | 25 | 26 | 27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /fSKModemTerminal/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSKModem Terminal 5 | 6 | Input 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /import-summary.txt: -------------------------------------------------------------------------------- 1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY 2 | ====================================== 3 | 4 | Manifest Merging: 5 | ----------------- 6 | Your project uses libraries that provide manifests, and your Eclipse 7 | project did not explicitly turn on manifest merging. In Android Gradle 8 | projects, manifests are always merged (meaning that contents from your 9 | libraries' manifests will be merged into the app manifest. If you had 10 | manually copied contents from library manifests into your app manifest 11 | you may need to remove these for the app to build correctly. 12 | 13 | Ignored Files: 14 | -------------- 15 | The following files were *not* copied into the new Gradle project; you 16 | should evaluate whether these are still needed in your project and if 17 | so manually move them: 18 | 19 | From FSKModemTerminal: 20 | * ic_launcher-web.png 21 | * proguard-project.txt 22 | From android.fskmodem: 23 | * .directory 24 | * proguard-project.txt 25 | 26 | Replaced Jars with Dependencies: 27 | -------------------------------- 28 | The importer recognized the following .jar files as third party 29 | libraries and replaced them with Gradle dependencies instead. This has 30 | the advantage that more explicit version information is known, and the 31 | libraries can be updated automatically. However, it is possible that 32 | the .jar file in your project was of an older version than the 33 | dependency we picked, which could render the project not compileable. 34 | You can disable the jar replacement in the import wizard and try again: 35 | 36 | android-support-v4.jar => com.android.support:support-v4:19.1.0 37 | 38 | Replaced Libraries with Dependencies: 39 | ------------------------------------- 40 | The importer recognized the following library projects as third party 41 | libraries and replaced them with Gradle dependencies instead. This has 42 | the advantage that more explicit version information is known, and the 43 | libraries can be updated automatically. However, it is possible that 44 | the source files in your project were of an older version than the 45 | dependency we picked, which could render the project not compileable. 46 | You can disable the library replacement in the import wizard and try 47 | again: 48 | 49 | appcompat-v7 => [com.android.support:appcompat-v7:19.1.0] 50 | 51 | Moved Files: 52 | ------------ 53 | Android Gradle projects use a different directory structure than ADT 54 | Eclipse projects. Here's how the projects were restructured: 55 | 56 | In android.fskmodem: 57 | * AndroidManifest.xml => androidfskmodem/src/main/AndroidManifest.xml 58 | * assets/ => androidfskmodem/src/main/assets 59 | * res/ => androidfskmodem/src/main/res 60 | * src/ => androidfskmodem/src/main/java/ 61 | In FSKModemTerminal: 62 | * AndroidManifest.xml => fSKModemTerminal/src/main/AndroidManifest.xml 63 | * res/ => fSKModemTerminal/src/main/res/ 64 | * src/ => fSKModemTerminal/src/main/java/ 65 | 66 | Next Steps: 67 | ----------- 68 | You can now build the project. The Gradle project needs network 69 | connectivity to download dependencies. 70 | 71 | Bugs: 72 | ----- 73 | If for some reason your project does not build, and you determine that 74 | it is due to a bug or limitation of the Eclipse to Gradle importer, 75 | please file a bug at http://b.android.com with category 76 | Component-Tools. 77 | 78 | (This import summary is for your information only, and can be deleted 79 | after import once you are satisfied with the results.) 80 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':androidfskmodem', ':waterlevel' 2 | include ':fSKModemTerminal' 3 | -------------------------------------------------------------------------------- /waterlevel/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /waterlevel/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "23.0.0 rc2" 6 | 7 | defaultConfig { 8 | applicationId "com.akg.sensprout.waterlevel" 9 | minSdkVersion 8 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:appcompat-v7:22.2.0' 25 | compile project(':androidfskmodem') 26 | } 27 | -------------------------------------------------------------------------------- /waterlevel/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Volumes/Data/bin/adt-bundle-mac-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /waterlevel/src/androidTest/java/com/akg/sensprout/waterlevel/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.akg.sensprout.waterlevel; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /waterlevel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /waterlevel/src/main/java/com/akg/sensprout/waterlevel/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.akg.sensprout.waterlevel; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.media.AudioFormat; 5 | import android.media.AudioManager; 6 | import android.media.AudioRecord; 7 | import android.media.AudioTrack; 8 | import android.media.MediaRecorder; 9 | import android.support.v7.app.ActionBarActivity; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.view.KeyEvent; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.view.MotionEvent; 16 | import android.view.View; 17 | import android.view.inputmethod.EditorInfo; 18 | import android.widget.EditText; 19 | import android.widget.ScrollView; 20 | import android.widget.TextView; 21 | 22 | import java.io.IOException; 23 | 24 | import bg.cytec.android.fskmodem.FSKConfig; 25 | import bg.cytec.android.fskmodem.FSKDecoder; 26 | import bg.cytec.android.fskmodem.FSKEncoder; 27 | 28 | 29 | public class MainActivity extends ActionBarActivity { 30 | 31 | protected FSKConfig mConfig; 32 | protected FSKEncoder mEncoder; 33 | protected FSKDecoder mDecoder; 34 | 35 | protected AudioTrack mAudioTrack; 36 | 37 | protected AudioRecord mRecorder; 38 | 39 | protected int mBufferSize = 0; 40 | 41 | protected boolean mScrollLock = true; 42 | 43 | protected ScrollView mScroll; 44 | protected TextView mTerminal; 45 | protected EditText mInput; 46 | 47 | protected Runnable mRecordFeed = new Runnable() { 48 | 49 | @Override 50 | public void run() { 51 | 52 | while (mRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { 53 | 54 | short[] data = new short[mBufferSize / 2]; //the buffer size is in bytes 55 | 56 | // gets the audio output from microphone to short array samples 57 | mRecorder.read(data, 0, mBufferSize / 2); 58 | 59 | mDecoder.appendSignal(data); 60 | } 61 | } 62 | }; 63 | 64 | @Override 65 | protected void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | setContentView(R.layout.activity_main); 68 | 69 | // INIT VIEWS 70 | 71 | mTerminal = (TextView) findViewById(R.id.lblSense); 72 | mInput = (EditText) findViewById(R.id.txtInput); 73 | 74 | mInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { 75 | 76 | @Override 77 | public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { 78 | 79 | if (actionId == EditorInfo.IME_ACTION_SEND) { 80 | 81 | String text = view.getText().toString() + "\n"; 82 | 83 | mEncoder.appendData(text.getBytes()); 84 | 85 | mTerminal.setText(mTerminal.getText() + "\nSend: " + text); 86 | 87 | mInput.setText(""); 88 | 89 | if (mScrollLock) { 90 | mScroll.fullScroll(ScrollView.FOCUS_DOWN); 91 | } 92 | 93 | return true; 94 | } 95 | 96 | return false; 97 | } 98 | }); 99 | 100 | mScroll = (ScrollView) findViewById(R.id.scrollView); 101 | 102 | mScroll.setOnTouchListener(new View.OnTouchListener() { 103 | 104 | @SuppressLint("ClickableViewAccessibility") 105 | @Override 106 | public boolean onTouch(View view, MotionEvent event) { 107 | 108 | if (event.getAction() == MotionEvent.ACTION_UP) { 109 | //finger released, detect if view is scrolled to bottom 110 | 111 | int diff = (mScroll.getScrollY() + mScroll.getHeight()) - mTerminal.getHeight(); 112 | 113 | if (diff == 0) { 114 | mScrollLock = true; 115 | } 116 | } else if (event.getAction() == MotionEvent.ACTION_DOWN) { 117 | //finger placed down, lock scroll 118 | 119 | mScrollLock = false; 120 | } 121 | 122 | return false; 123 | } 124 | }); 125 | 126 | /// INIT FSK CONFIG 127 | 128 | try { 129 | mConfig = new FSKConfig(FSKConfig.SAMPLE_RATE_44100, FSKConfig.PCM_16BIT, FSKConfig.CHANNELS_MONO, FSKConfig.SOFT_MODEM_MODE_4, FSKConfig.THRESHOLD_20P); 130 | } catch (IOException e1) { 131 | e1.printStackTrace(); 132 | } 133 | 134 | /// INIT FSK DECODER 135 | 136 | mDecoder = new FSKDecoder(mConfig, new FSKDecoder.FSKDecoderCallback() { 137 | 138 | @Override 139 | public void decoded(byte[] newData) { 140 | 141 | final String text = new String(newData); 142 | 143 | runOnUiThread(new Runnable() { 144 | public void run() { 145 | 146 | mTerminal.setText(mTerminal.getText() + text); 147 | 148 | if (mScrollLock) { 149 | mScroll.fullScroll(ScrollView.FOCUS_DOWN); 150 | } 151 | } 152 | }); 153 | } 154 | }); 155 | 156 | /// INIT FSK ENCODER 157 | 158 | mEncoder = new FSKEncoder(mConfig, new FSKEncoder.FSKEncoderCallback() { 159 | 160 | @Override 161 | public void encoded(byte[] pcm8, short[] pcm16) { 162 | if (mConfig.pcmFormat == FSKConfig.PCM_16BIT) { 163 | //16bit buffer is populated, 8bit buffer is null 164 | 165 | mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 166 | mConfig.sampleRate, AudioFormat.CHANNEL_OUT_MONO, 167 | AudioFormat.ENCODING_PCM_16BIT, pcm16.length * 2, 168 | AudioTrack.MODE_STATIC); 169 | 170 | mAudioTrack.write(pcm16, 0, pcm16.length); 171 | 172 | mAudioTrack.play(); 173 | } 174 | } 175 | }); 176 | 177 | /// 178 | 179 | //make sure that the settings of the recorder match the settings of the decoder 180 | //most devices cant record anything but 44100 samples in 16bit PCM format... 181 | mBufferSize = AudioRecord.getMinBufferSize(FSKConfig.SAMPLE_RATE_44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 182 | 183 | //scale up the buffer... reading larger amounts of data 184 | //minimizes the chance of missing data because of thread priority 185 | mBufferSize *= 10; 186 | 187 | //again, make sure the recorder settings match the decoder settings 188 | mRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, FSKConfig.SAMPLE_RATE_44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mBufferSize); 189 | 190 | if (mRecorder.getState() == AudioRecord.STATE_INITIALIZED) { 191 | mRecorder.startRecording(); 192 | 193 | //start a thread to read the audio data 194 | Thread thread = new Thread(mRecordFeed); 195 | thread.setPriority(Thread.MAX_PRIORITY); 196 | thread.start(); 197 | } else { 198 | Log.i("FSKDecoder", "Please check the recorder settings, something is wrong!"); 199 | } 200 | } 201 | 202 | @Override 203 | public boolean onCreateOptionsMenu(Menu menu) { 204 | // Inflate the menu; this adds items to the action bar if it is present. 205 | getMenuInflater().inflate(R.menu.menu_main, menu); 206 | return true; 207 | } 208 | 209 | @Override 210 | public boolean onOptionsItemSelected(MenuItem item) { 211 | // Handle action bar item clicks here. The action bar will 212 | // automatically handle clicks on the Home/Up button, so long 213 | // as you specify a parent activity in AndroidManifest.xml. 214 | int id = item.getItemId(); 215 | 216 | //noinspection SimplifiableIfStatement 217 | if (id == R.id.action_settings) { 218 | return true; 219 | } 220 | 221 | return super.onOptionsItemSelected(item); 222 | } 223 | 224 | @Override 225 | protected void onDestroy() { 226 | 227 | mDecoder.stop(); 228 | mEncoder.stop(); 229 | 230 | if (mRecorder != null && mRecorder.getState() == AudioRecord.STATE_INITIALIZED) { 231 | mRecorder.stop(); 232 | mRecorder.release(); 233 | } 234 | 235 | if (mAudioTrack != null && mAudioTrack.getPlayState() == AudioTrack.STATE_INITIALIZED) { 236 | mAudioTrack.stop(); 237 | mAudioTrack.release(); 238 | } 239 | 240 | super.onDestroy(); 241 | } 242 | } -------------------------------------------------------------------------------- /waterlevel/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 18 | 19 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /waterlevel/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /waterlevel/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/waterlevel/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /waterlevel/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/waterlevel/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /waterlevel/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/waterlevel/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /waterlevel/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatung/FSKModemTerminal/ccabe56a4911e74210c44a646052a211c155d87b/waterlevel/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /waterlevel/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /waterlevel/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /waterlevel/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Water Level 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /waterlevel/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | --------------------------------------------------------------------------------