diff --git a/app/build.gradle b/app/build.gradle index c15effd..ff742f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation 'com.android.support:appcompat-v7:26.0.1' implementation 'com.android.support.constraint:constraint-layout:1.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ceeba7..8fbc3f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,9 +12,9 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:largeHeap="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"> + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/chrhsmt/sisheng/AudioService.kt b/app/src/main/java/com/chrhsmt/sisheng/AudioService.kt index 34380aa..fc52c7f 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/AudioService.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/AudioService.kt @@ -52,7 +52,7 @@ class AudioService : AudioServiceInterface { private val TAG: String = "AudioService" private val activity: Activity - private val chart: Chart + private var chart: Chart? = null private var audioDispatcher: AudioDispatcher? = null private var analyzeThread: Thread? = null private var isRunning: Boolean = false @@ -60,7 +60,7 @@ class AudioService : AudioServiceInterface { private var frequencies: MutableList = ArrayList() private var testFrequencies: MutableList = ArrayList() - constructor(chart: Chart, activity: Activity) { + constructor(chart: Chart?, activity: Activity) { this.activity = activity this.chart = chart // Setting ffmpeg @@ -70,7 +70,8 @@ class AudioService : AudioServiceInterface { override fun startAudioRecord() { // 既存データをクリア this.frequencies.clear() - this.chart.clearDateSet(MICROPHONE_DATA_SET_LABEL_NAME) + this.chart?.clearDateSet(MICROPHONE_DATA_SET_LABEL_NAME) + // マイクロフォンバッファサイズの計算 val microphoneBufferSize = AudioRecord.getMinBufferSize( @@ -87,18 +88,22 @@ class AudioService : AudioServiceInterface { } @SuppressLint("WrongConstant") - override fun testPlay(fileName: String, playback: Boolean, callback: Runnable?) { + override fun testPlay(fileName: String, path: String?, playback: Boolean, callback: Runnable?, async: Boolean, labelName: String) { this.testFrequencies.clear() - this.chart.clear() + this.chart?.clear() - Thread(Runnable { + val run = Runnable { - val path = this.copyAudioFile(fileName) + val audioPath: String = if (path == null) { + this.copyAudioFile(fileName) + } else { + path + } this.startRecord( AudioDispatcherFactory.fromPipe( - path, + audioPath, AUDIO_FILE_SAMPLING_RATE, BUFFER_SIZE, 0 @@ -107,9 +112,38 @@ class AudioService : AudioServiceInterface { playback = playback, samplingRate = AUDIO_FILE_SAMPLING_RATE, targetList = this.testFrequencies, - labelName = "SampleAudio", + labelName = labelName, callback = callback ) + } + if (async) { + Thread(run).start() + } else { + run.run() + } + + } + + @SuppressLint("WrongConstant") + override fun debugTestPlay(fileName: String, path: String, playback: Boolean, callback: Runnable?) { + + Thread(Runnable { + + this.startRecord( + AudioDispatcherFactory.fromPipe( + path, + AUDIO_FILE_SAMPLING_RATE, + BUFFER_SIZE, + 0 + ), + false, + playback = playback, + samplingRate = AUDIO_FILE_SAMPLING_RATE, + targetList = this.frequencies, + labelName = "RecordedSampleAudio", + callback = callback, + color = Color.rgb(255, 10, 10) + ) }).start() } @@ -161,6 +195,24 @@ class AudioService : AudioServiceInterface { return point } + @Throws(AudioServiceException::class) + fun analyze(calculator: PointCalculator) : Point { + val point = calculator.calc(this.frequencies, this.testFrequencies) + if (point.base <= this.testFrequencies.size) { + // 録音が失敗している場合 + throw AudioServiceException("不好意思,我听不懂") + } + return point + } + + override fun clearTestFrequencies() { + this.testFrequencies.clear() + } + + override fun clearFrequencies() { + this.frequencies.clear() + } + override fun clear() { this.frequencies.clear() this.testFrequencies.clear() @@ -210,7 +262,7 @@ class AudioService : AudioServiceInterface { } } this@AudioService.activity.runOnUiThread { - chart.addEntry(pitch, name = labelName, color = color) + chart?.addEntry(pitch, name = labelName, color = color) } } } @@ -228,10 +280,15 @@ class AudioService : AudioServiceInterface { if (shouldRecord) { ExternalMedia.saveDir?.takeIf { it -> it.canWrite() }?.let { it -> - val dateString = SimpleDateFormat("yyyy-MM-dd_HH_mm_ss").format(Date()) - val id = Regex("^mfsz/(\\d)_[f|m].wav$").find(Settings.sampleAudioFileName!!)?.groups?.last()?.value + val dateString = SimpleDateFormat("yyyy-MM-dd").format(Date()) + val directory = File(it, dateString) + if (!directory.isDirectory) { + directory.mkdir() + } + val dateTimeString = SimpleDateFormat("yyyy-MM-dd_HH_mm_ss").format(Date()) + val id = Regex("^mfsz/(\\d+)_[f|m].wav$").find(Settings.sampleAudioFileName!!)?.groups?.last()?.value val sex = Settings.sex!!.first().toLowerCase() - val newFile = File(it, String.format("%s-%s-%s.wav", dateString, id, sex)) + val newFile = File(directory, String.format("%s-%s-%s.wav", dateTimeString, id, sex)) val format = TarsosDSPAudioFormat(AUDIO_FILE_SAMPLING_RATE.toFloat(), 16, 1, true, false) val writeProcessor: AudioProcessor = WriterProcessor(format, RandomAccessFile(newFile, "rw")) dispatcher.addAudioProcessor(writeProcessor) @@ -255,8 +312,8 @@ class AudioService : AudioServiceInterface { } private fun stopRecord() { - this.audioDispatcher!!.stop() - this.analyzeThread!!.interrupt() + this.audioDispatcher?.stop() + this.analyzeThread?.interrupt() this.isRunning = false this@AudioService.activity.runOnUiThread { this.activity.button?.text = "開始" @@ -291,4 +348,14 @@ class AudioService : AudioServiceInterface { return path } + + fun getTestFreq(): MutableList { + return this.testFrequencies + } + + fun addOtherChart(freqs: MutableList?, labelName: String, color: Int) { + freqs?.forEach { fl -> + chart?.addEntry(fl, name = labelName, color = color) + } + } } diff --git a/app/src/main/java/com/chrhsmt/sisheng/AudioServiceInterface.kt b/app/src/main/java/com/chrhsmt/sisheng/AudioServiceInterface.kt index eeb1a6b..3b0e3dc 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/AudioServiceInterface.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/AudioServiceInterface.kt @@ -7,11 +7,19 @@ import com.chrhsmt.sisheng.point.Point */ interface AudioServiceInterface { fun startAudioRecord() - fun testPlay(fileName: String, playback: Boolean = true, callback: Runnable? = null) + fun testPlay(fileName: String, + path: String? = null, + playback: Boolean = true, + callback: Runnable? = null, + async: Boolean = true, + labelName: String = "SampleAudio") + fun debugTestPlay(fileName: String, path: String, playback: Boolean = false, callback: Runnable?) fun attemptPlay(fileName: String) fun stop() fun analyze() : Point fun analyze(klassName: String) : Point fun clear() + fun clearTestFrequencies() + fun clearFrequencies() fun isRunning(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/chrhsmt/sisheng/AudioServiceMock.kt b/app/src/main/java/com/chrhsmt/sisheng/AudioServiceMock.kt index 7adf37a..ee672d9 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/AudioServiceMock.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/AudioServiceMock.kt @@ -11,6 +11,7 @@ import com.chrhsmt.sisheng.ui.Chart * Created by hkimura on 2017/10/30. */ class AudioServiceMock : AudioServiceInterface { + private val TAG: String = "AudioServiceMock" private val activity: Activity @@ -26,10 +27,13 @@ class AudioServiceMock : AudioServiceInterface { // NOP } - override fun testPlay(fileName: String, playback: Boolean, callback: Runnable?) { + override fun testPlay(fileName: String, path: String?, playback: Boolean, callback: Runnable?, async: Boolean, labelName: String) { this.isRunning = true } + override fun debugTestPlay(fileName: String, path: String, playback: Boolean, callback: Runnable?) { + } + override fun attemptPlay(fileName: String) { this.isRunning = true } @@ -53,13 +57,22 @@ class AudioServiceMock : AudioServiceInterface { 80, 5.0, 5.0, - 1) + 1, + null) } override fun clear() { // NOP } + override fun clearFrequencies() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun clearTestFrequencies() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + override fun isRunning(): Boolean { return this.isRunning } diff --git a/app/src/main/java/com/chrhsmt/sisheng/ReibunInfo.kt b/app/src/main/java/com/chrhsmt/sisheng/ReibunInfo.kt index e8ef2e4..7f2e97c 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/ReibunInfo.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/ReibunInfo.kt @@ -1,6 +1,7 @@ package com.chrhsmt.sisheng import android.content.Context +import android.util.Log import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserFactory import java.io.StringReader @@ -10,6 +11,7 @@ import java.io.StringReader */ class ReibunInfo { companion object { + private val TAG = "ReibunInfo" private var instance : ReibunInfo? = null private var instanceForTest : ReibunInfo? = null @@ -93,21 +95,21 @@ class ReibunInfo { if (eventType == XmlPullParser.START_DOCUMENT) { println("Start document") } else if (eventType == XmlPullParser.START_TAG) { - System.out.println("Start tag " + xpp.name) + Log.d(TAG,"Start tag " + xpp.name) // センテンス開始処理 if (xpp.name == "SENTENCE") { item = ReibunInfoItem() } xppName = xpp.name } else if (eventType == XmlPullParser.END_TAG) { - System.out.println("End tag " + xpp.name) + Log.d(TAG,"End tag " + xpp.name) // センテンス終了処理 if (xpp.name == "SENTENCE" && item != null) { itemList.add(item) } xppName = "" } else if (eventType == XmlPullParser.TEXT) { - System.out.println("Name" + xpp.name + " Text " + xpp.text) + Log.d(TAG,"Name" + xpp.name + " Text " + xpp.text) // 各要素処理 if (item != null) { when (xppName){ @@ -172,4 +174,8 @@ class ReibunInfo { fun setSelectedItem(position : Int) { selectedItem = itemList[position] } + + fun getItemList(): List { + return this.itemList + } } \ No newline at end of file diff --git a/app/src/main/java/com/chrhsmt/sisheng/Settings.kt b/app/src/main/java/com/chrhsmt/sisheng/Settings.kt index 62097e9..7d17f79 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/Settings.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/Settings.kt @@ -20,7 +20,7 @@ object Settings { // 対数得点計算の底 var baseLogarithmForPoint: Int = 5 // ポイント成功閾値 - var pointSuccessThreshold: Int = 80 + var pointSuccessThreshold: Int = 70 // ノイズのバッファカウントの閾値 var freqNoizeCountThreashold: Int = 2 diff --git a/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzeActivity.kt b/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzeActivity.kt new file mode 100644 index 0000000..6cd82dd --- /dev/null +++ b/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzeActivity.kt @@ -0,0 +1,186 @@ +package com.chrhsmt.sisheng.debug + +import android.os.Bundle +import android.app.Activity +import android.content.Intent +import android.util.Log +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ListAdapter +import android.widget.Toast +import com.chrhsmt.sisheng.* +import com.chrhsmt.sisheng.persistence.SDCardManager +import com.chrhsmt.sisheng.point.SimplePointCalculator +import com.chrhsmt.sisheng.ui.ScreenUtils +import dmax.dialog.SpotsDialog + +import kotlinx.android.synthetic.main.activity_analyze.* +import java.io.File + +class AnalyzeActivity : Activity() { + + companion object { + val TAG = "AnalyzeActivity" + } + + private var service: AudioService? = null + private var files: List? = null + private var datas: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_analyze) + + // フルスクリーンにする + ScreenUtils.setFullScreen(this.window) + ScreenUtils.setScreenBackground(this) + + Settings.setDefaultValue(this@AnalyzeActivity, true) + + this.service = AudioService(null, this) + + val cardManager: SDCardManager = SDCardManager() + cardManager.setUpReadWriteExternalStorage(this) + this.files = cardManager.getFileList() + val lines = cardManager.readCsv(this@AnalyzeActivity, "result.csv") + + try { + this.datas = this.files?.map { file -> + val line = lines.find { line -> line.startsWith(file.name) } + lines.minus(line) + AnalyzedRecordedData(file, line).init() + } + } catch (e: Exception) { + e.printStackTrace() + } + + // ファイルリスト + val fileNames = this.datas?.map { data -> data.toString() } + val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, fileNames) + listFiles.adapter = adapter as ListAdapter + + listFiles.setOnItemClickListener { parent, view, position, id -> + + val reibunInfo = ReibunInfo.getInstance(this) + val itemPosition = this.datas?.get(position)?.id?.minus(1) ?: run { + Log.e(TAG, "Data not found !!!") + return@setOnItemClickListener + } + reibunInfo.setSelectedItem(itemPosition) + val data = this.datas!!.get(position) + AnalyzedRecordedData.setSelected(data) + Settings.sex = data.sex!! + + val intent = Intent(this@AnalyzeActivity, CompareActivity::class.java) + startActivity(intent) + overridePendingTransition(0, 0); + } + + // タイトル長押下された場合は、デバッグ画面に遷移する。 + if (Settings.DEBUG_MODE) { + analyze_title.setOnLongClickListener(View.OnLongClickListener { + val intent = Intent(this@AnalyzeActivity, MainActivity::class.java) + startActivity(intent) + true + }) + } + + btnAnalyze.setOnClickListener({ v: View? -> + val dialog = SpotsDialog(this@AnalyzeActivity, getString(R.string.screen6_3), R.style.CustomSpotDialog) + analyze_title.setText("解析開始します") + // プログレスダイアログを表示する + dialog.show() + ScreenUtils.setFullScreen(dialog.window) + runOnUiThread { + } + + Toast.makeText(this@AnalyzeActivity, "解析開始します", Toast.LENGTH_SHORT).show() + + Settings.setDefaultValue(this@AnalyzeActivity, false) + + + Thread(Runnable { + var count = 0 + var map: MutableMap> = HashMap>() + val reibunInfo = ReibunInfo.getInstance(this@AnalyzeActivity) + reibunInfo.getItemList().forEach { + this@AnalyzeActivity.service?.testPlay(it.getMFSZExampleAudioFileName(), playback = false, async = false) + val sampleData = this@AnalyzeActivity.service?.getTestFreq() + map[it.id.toString()] = sampleData!!.toMutableList() + } + + var buffer = StringBuilder() + var nullMaleBuffer = StringBuilder() + var nullFemaleBuffer = StringBuilder() + + this@AnalyzeActivity.files?.forEach { file -> + val fileName = file.name + + // ファイル名から条件取得 + ".*-(.*)-(.*)\\.wav".toRegex().find(fileName)?.groups?.let { it -> + val id = it.get(1)?.value + val sex = it.get(2)?.value + if (id != null && !"null".equals(id)) { + val position = id.toInt().minus(1) + reibunInfo.setSelectedItem(position) + val reibunName = reibunInfo.selectedItem!!.getMFSZExampleAudioFileName() + Settings.sampleAudioFileName = reibunName + + if (sex == "null") { + // male + Settings.sex = "m" + this@AnalyzeActivity.extractLastYearData(file, nullMaleBuffer, map[id]!!, id, "m", fileName) + // female + Settings.sex = "f" + this@AnalyzeActivity.extractLastYearData(file, nullFemaleBuffer, map[id]!!, id, "f", fileName) + } else { + Settings.sex = sex + this@AnalyzeActivity.extractLastYearData(file, buffer, map[id]!!, id, sex!!, fileName) + } + count++ + } + } + + } + + // file書き出し? + SDCardManager().write(this@AnalyzeActivity, "result.csv", buffer.toString()) + if (nullMaleBuffer.isNotEmpty()) { + SDCardManager().write(this@AnalyzeActivity, "nullMaleResult.csv", nullMaleBuffer.toString()) + } + if (nullFemaleBuffer.isNotEmpty()) { + SDCardManager().write(this@AnalyzeActivity, "nullFemaleResult.csv", nullFemaleBuffer.toString()) + } + + Log.i(TAG, "finished") + Toast.makeText(this@AnalyzeActivity, String.format("解析完了: %d件", count), Toast.LENGTH_LONG).show() + runOnUiThread { + dialog.dismiss() + analyze_title.text = String.format("解析完了: %d件", count) + } + }).start() + }) + } + + private fun extractLastYearData(file: File, buffer: StringBuilder, sampleData: MutableList, id: String, sex: String, fileName: String) { + val cardManager: SDCardManager = SDCardManager() + // 去年のデータ抽出 + val path= cardManager.copyAudioFile(file, this@AnalyzeActivity) + this@AnalyzeActivity.service?.testPlay(file.name, path, playback = false, callback = Runnable { + + val data = this@AnalyzeActivity.service?.getTestFreq() + + val calculator: SimplePointCalculator = SimplePointCalculator() + calculator.setV1() + val point = calculator.calc(data!!, sampleData) + val sexBaseScore= point.score + val sexBaseSuccess= point.success() + + calculator.setV2() + val freqPoint = calculator.calc(data!!, sampleData) + buffer.append(fileName + ", " + id + ", " + sex + ", " + sexBaseScore + ", " + sexBaseSuccess + ", " + freqPoint.score + "\n") + + }, async = false) + } + +} diff --git a/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzedRecordedData.kt b/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzedRecordedData.kt new file mode 100644 index 0000000..da9173b --- /dev/null +++ b/app/src/main/java/com/chrhsmt/sisheng/debug/AnalyzedRecordedData.kt @@ -0,0 +1,38 @@ +package com.chrhsmt.sisheng.debug + +import android.content.Context +import com.chrhsmt.sisheng.ReibunInfo +import com.chrhsmt.sisheng.Settings +import com.chrhsmt.sisheng.point.Point +import java.io.File + +data class AnalyzedRecordedData(var file: File, var csvLine: String?) { + + companion object { + private val TAG = "AnalyzedRecordedData" + private var selected : AnalyzedRecordedData? = null + + fun getSelected(): AnalyzedRecordedData? { + return selected + } + fun setSelected(instance: AnalyzedRecordedData) { + selected = instance + } + } + + var id: Int? = null + var point: Point? = null + var sex: String? = null + + fun init(): AnalyzedRecordedData { + val line = this.csvLine?.split(",") + this.id = line?.get(1)?.trim()?.toInt() + this.sex = line?.get(2)?.trim() + this.point = Point(line?.get(3)?.trim()?.toInt()?: 0, 0.0, 0.0, 0, null) + return this + } + + override fun toString(): String { + return String.format("%s - %s - %s", this.file.name, this.point?.score, this.point?.success()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chrhsmt/sisheng/debug/CompareActivity.kt b/app/src/main/java/com/chrhsmt/sisheng/debug/CompareActivity.kt new file mode 100644 index 0000000..f978d81 --- /dev/null +++ b/app/src/main/java/com/chrhsmt/sisheng/debug/CompareActivity.kt @@ -0,0 +1,307 @@ +package com.chrhsmt.sisheng.debug + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.os.Bundle +import android.support.v4.app.ActivityCompat +import android.support.v7.app.AppCompatActivity +import android.util.Log +import android.view.View +import android.view.animation.AlphaAnimation +import android.widget.TextView +import android.widget.Toast +import com.chrhsmt.sisheng.* +import com.chrhsmt.sisheng.exception.AudioServiceException +import com.chrhsmt.sisheng.font.FontUtils +import com.chrhsmt.sisheng.network.RaspberryPi +import com.chrhsmt.sisheng.persistence.SDCardManager +import com.chrhsmt.sisheng.point.SimplePointCalculator +import com.chrhsmt.sisheng.ui.Chart +import com.chrhsmt.sisheng.ui.ScreenUtils +import com.github.mikephil.charting.charts.LineChart +import dmax.dialog.SpotsDialog +import kotlinx.android.synthetic.main.activity_analyze_compare.* +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException + +class CompareActivity : AppCompatActivity() { + + private val TAG: String = "CompareActivity" + private val PERMISSION_REQUEST_CODE = 1 + + private var service: AudioServiceInterface? = null + private var chart: Chart? = null + + private var isRecording = false + enum class REIBUN_STATUS(val rawValue: Int) { + PREPARE(1), + NORMAL(2), + PLAYING(3), + RECODING(4), + ANALYZING(5), + ANALYZE_FINISH(6), + ANALYZE_ERROR_OCCUR(7), + } + private var nowStatus: REIBUN_STATUS = REIBUN_STATUS.NORMAL + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_analyze_compare) + + // フルスクリーンにする + ScreenUtils.setFullScreen(this.window) + ScreenUtils.setScreenBackground(this) + + // ピンイン、中文、英文の配置 + val reibunInfo = ReibunInfo.getInstance(this) + txtDebugPinyin.text = ReibunInfo.replaceNewLine(reibunInfo.selectedItem!!.pinyin) + txtDebugChinese.text = ReibunInfo.replaceNewLine(reibunInfo.selectedItem!!.chinese) + + // 音声再生、録画の準備 + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), PERMISSION_REQUEST_CODE) + //return + } + + Settings.setDefaultValue(this, false) + + // グラフの準備 + this.chart = Chart() + this.chart!!.initChartView(this.findViewById(R.id.chart)) + this.service = AudioService(this.chart!!, this) + + // プログレスダイアログを表示する + val dialog = SpotsDialog(this@CompareActivity, getString(R.string.screen6_2), R.style.CustomSpotDialog) + dialog.show() + ScreenUtils.setFullScreen(dialog.window) + + // お手本事前再生 + nowStatus = REIBUN_STATUS.PREPARE + updateButtonStatus() + val fileName = reibunInfo.selectedItem!!.getMFSZExampleAudioFileName() + Settings.sampleAudioFileName = fileName + this.service!!.testPlay(fileName, playback = false, callback = object : Runnable { + override fun run() { + this@CompareActivity.runOnUiThread { + + // DEBUG解析 + val analyzed = AnalyzedRecordedData.getSelected() + if (analyzed != null) { + val file = analyzed.file + val path= SDCardManager().copyAudioFile(file, this@CompareActivity) + this@CompareActivity.service!!.debugTestPlay(file.name, path, callback = object : Runnable { + override fun run() { + val v2Point = this@CompareActivity.service?.analyze() + + val calcurator = SimplePointCalculator() + calcurator.setV1() + val v1Point = (this@CompareActivity.service as AudioService)?.analyze(calcurator) + + this@CompareActivity.runOnUiThread { + (this@CompareActivity.service as? AudioService)?.addOtherChart( + v1Point?.analyzedFreqList, + "男女設定キャリブレーション", + Color.rgb(10, 255, 10)) + (this@CompareActivity.service as? AudioService)?.addOtherChart( + v2Point?.analyzedFreqList, + "周波数キャリブレーション", + Color.rgb(255, 10, 255)) + txtScore.text = String.format("Point: %s, F-Point: %s", v1Point?.score, v2Point?.score) + } + } + }) + } + + nowStatus = REIBUN_STATUS.NORMAL + updateButtonStatus() + dialog.dismiss() + } + } + }) + + // お手本再生 + btnAnalyzeOtehon.setOnClickListener({ + nowStatus = CompareActivity.REIBUN_STATUS.PLAYING + updateButtonStatus() + + this@CompareActivity.service!!.clearTestFrequencies() + this@CompareActivity.chart!!.clear() + + val fileName = reibunInfo.selectedItem!!.getMFSZExampleAudioFileName() + Log.d(TAG, "Play " + fileName) + this.service!!.testPlay(fileName, callback = object : Runnable { + override fun run() { + Thread.sleep(300) + + this@CompareActivity.runOnUiThread { + + // DEBUG解析 + val analyzed = AnalyzedRecordedData.getSelected() + if (analyzed != null) { + val file = analyzed.file + val path= SDCardManager().copyAudioFile(file, this@CompareActivity) + this@CompareActivity.service!!.clearFrequencies() + this@CompareActivity.service!!.debugTestPlay(file.name, path, playback = true, callback = object : Runnable { + override fun run() { + val v2Point = this@CompareActivity.service?.analyze() + + val calcurator = SimplePointCalculator() + calcurator.setV1() + val v1Point = (this@CompareActivity.service as AudioService)?.analyze(calcurator) + + this@CompareActivity.runOnUiThread { + (this@CompareActivity.service as? AudioService)?.addOtherChart( + v1Point?.analyzedFreqList, + "男女設定キャリブレーション", + Color.rgb(10, 255, 10)) + (this@CompareActivity.service as? AudioService)?.addOtherChart( + v2Point?.analyzedFreqList, + "周波数キャリブレーション", + Color.rgb(255, 10, 255)) + txtScore.text = String.format("Point: %s, F-Point: %s", v1Point?.score, v2Point?.score) + + this@CompareActivity.nowStatus = REIBUN_STATUS.NORMAL + this@CompareActivity.updateButtonStatus() + dialog.dismiss() + } + } + }) + } + } + } + }) + + Thread(Runnable { + Thread.sleep(2000) + when (this@CompareActivity.service!!.isRunning()) { + true -> Thread.sleep(1000) + } + + this@CompareActivity.runOnUiThread { + nowStatus = CompareActivity.REIBUN_STATUS.NORMAL + updateButtonStatus() + } + }).start() + }) + + // タイトル長押下された場合は、デバッグ画面に遷移する。 + if (Settings.DEBUG_MODE) { + txtDebugReibun.setOnLongClickListener(View.OnLongClickListener { + val intent = Intent(this@CompareActivity, MainActivity::class.java) + startActivity(intent) + true + }) + } + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + ScreenUtils.setFullScreen(this@CompareActivity.window) + } + + fun analyze() { + // プログレスダイアログを表示する + val dialog = SpotsDialog(this@CompareActivity, getString(R.string.screen6_3), R.style.CustomSpotDialog) + dialog.show() + FontUtils.changeFont(this@CompareActivity, dialog.findViewById(dmax.dialog.R.id.dmax_spots_title), 1.1f) + ScreenUtils.setFullScreen(dialog.window) + + // スレッドを開始する + Thread(Runnable { + Thread.sleep(1000 * 3) + + try { + analyzeInner() + } catch (e: AudioServiceException) { + Log.e(TAG, e.message) + runOnUiThread { + dialog.dismiss() + txtDebugError.visibility = View.VISIBLE + + nowStatus = REIBUN_STATUS.ANALYZE_ERROR_OCCUR + updateButtonStatus() + } + } + }).start() + } + + @Throws(AudioServiceException::class) + private fun analyzeInner() { + val info = this@CompareActivity.service!!.analyze() +// val info2 = this@ReibunActivity.service!!.analyze(FreqTransitionPointCalculator::class.qualifiedName!!) +// val info = this@ReibunActivity.service!!.analyze(NMultiplyLogarithmPointCalculator::class.qualifiedName!!) + if (info.success()) { + RaspberryPi().send(object: Callback { + override fun onFailure(call: Call?, e: IOException?) { + runOnUiThread { + Toast.makeText(this@CompareActivity, e!!.message, Toast.LENGTH_LONG).show() + } + } + + override fun onResponse(call: Call?, response: Response?) { + runOnUiThread { + Toast.makeText(this@CompareActivity, response!!.body()!!.string(), Toast.LENGTH_LONG).show() + } + } + }) + } + + runOnUiThread { + nowStatus = REIBUN_STATUS.ANALYZE_FINISH + updateButtonStatus() + + val intent = Intent(this@CompareActivity, ResultActivity::class.java) + intent.putExtra("result", info.success()) + intent.putExtra("score", info.score.toString()) + startActivity(intent) + overridePendingTransition(0, 0); + } + } + + private fun updateButtonStatus() { + when (nowStatus) { + REIBUN_STATUS.PREPARE -> { + // 録音ボタン:録音可、再生ボタン:再生可 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button) + btnAnalyzeOtehon.setEnabled(false) + } + REIBUN_STATUS.NORMAL -> { + // 録音ボタン:録音可、再生ボタン:再生可 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button) + btnAnalyzeOtehon.setEnabled(true) + } + REIBUN_STATUS.PLAYING -> { + // 録音ボタン:利用不可、再生ボタン:再生中 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button_press) + btnAnalyzeOtehon.setEnabled(false) + } + REIBUN_STATUS.RECODING -> { + // 録音ボタン:録音中、再生ボタン:再生不可 + val alphaAnimation = AlphaAnimation(1.0f, 0.7f) + alphaAnimation.duration = 1000 + alphaAnimation.fillAfter = true + alphaAnimation.repeatCount = -1 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button_disable) + btnAnalyzeOtehon.setEnabled(false) + } + REIBUN_STATUS.ANALYZING -> { + // 録音ボタン:録音不可、再生ボタン:再生不可 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button_disable) + btnAnalyzeOtehon.setEnabled(false) + } + REIBUN_STATUS.ANALYZE_FINISH -> { + //ボタン等のパーツの状態を戻さずに結果画面に遷移する想定。 + //本画面に戻る時は、再度 onCreate から行われる想定。 + } + REIBUN_STATUS.ANALYZE_ERROR_OCCUR -> { + // 録音ボタン:録音可、再生ボタン:再生不可 + btnAnalyzeOtehon.setBackgroundResource(R.drawable.shape_round_button_disable) + btnAnalyzeOtehon.setEnabled(false) + } + } + } +} diff --git a/app/src/main/java/com/chrhsmt/sisheng/persistence/SDCardManager.kt b/app/src/main/java/com/chrhsmt/sisheng/persistence/SDCardManager.kt new file mode 100644 index 0000000..5f01c34 --- /dev/null +++ b/app/src/main/java/com/chrhsmt/sisheng/persistence/SDCardManager.kt @@ -0,0 +1,87 @@ +package com.chrhsmt.sisheng.persistence + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.util.Log +import android.widget.Toast +import java.io.* +import java.nio.charset.Charset +import kotlin.collections.ArrayList + +class SDCardManager { + + companion object { + val TAG = "SDCardManager" + val REQUEST_PERMISSION = 1000; + } + + fun setUpReadWriteExternalStorage(context: Context) { + val dir = ExternalMedia.getExternalDirectoryPath(context) + if (ExternalMedia.isExternalStorageWritable() && dir != null && dir!!.canWrite()) { +// Toast.makeText(this, "SDカードへの書き込みが可能です", Toast.LENGTH_SHORT).show() + Log.d(TAG, String.format("External media path: %s", dir.absoluteFile)) + } else { + Toast.makeText(context, "SDカードがマウントされていない、もしくは書き込みが不可能な状態です", Toast.LENGTH_SHORT).show() + } + } + + fun readCsv(context: Context, name: String): List { + var lines: List = ArrayList() + val dir = ExternalMedia.getExternalDirectoryPath(context) + if (ExternalMedia.isExternalStorageReadable() && dir != null && dir!!.canRead()) { + val newFile = File(dir, name) + val fr = FileReader(newFile) + lines = fr.readLines() + fr.close() + } else { + Toast.makeText(context, "SDカードがマウントされていない、もしくは書き込みが不可能な状態です", Toast.LENGTH_SHORT).show() + } + return lines + } + + fun write(context: Context, name: String, data: String) { + val dir = ExternalMedia.getExternalDirectoryPath(context) + if (ExternalMedia.isExternalStorageWritable() && dir != null && dir!!.canWrite()) { + val newFile = File(dir, name) + val fos = FileOutputStream(newFile) + fos.write(data.toByteArray(Charset.defaultCharset())) + fos.close() + } else { + Toast.makeText(context, "SDカードがマウントされていない、もしくは書き込みが不可能な状態です", Toast.LENGTH_SHORT).show() + } + } + + fun getFileList(): List { + var ret = ArrayList() + ExternalMedia.saveDir?.takeIf { it -> it.canRead() }?.let { it -> + it.listFiles().forEach { file -> ret.add(file) } + } + return ret + } + + @SuppressLint("WrongConstant") + fun copyAudioFile(file: File, activity: Activity): String { + // ファイル移動 + var dataName = file.name + if (dataName.contains("/")) { + dataName = dataName.replace("/", "_") + } + val path = String.format("/data/data/%s/files/%s", activity.packageName, dataName) + val input = file.inputStream() + val output = activity.openFileOutput(dataName, Context.MODE_ENABLE_WRITE_AHEAD_LOGGING) + val DEFAULT_BUFFER_SIZE = 1024 * 4 + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var n = 0 + while (true) { + n = input.read(buffer) + if (n == -1) break + output.write(buffer, 0, n) + } + output.close() + input.close() + + return path + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chrhsmt/sisheng/point/FreqTransitionPointCalculator.kt b/app/src/main/java/com/chrhsmt/sisheng/point/FreqTransitionPointCalculator.kt index 415ac1e..42e83cd 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/point/FreqTransitionPointCalculator.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/point/FreqTransitionPointCalculator.kt @@ -3,6 +3,8 @@ package com.chrhsmt.sisheng.point import de.qaware.chronix.dtw.TimeWarpInfo /** + * 一つ前の音のデータと比較して遷移を計算する. + * * Created by chihiro on 2017/10/17. */ class FreqTransitionPointCalculator : PointCalculator() { @@ -25,7 +27,8 @@ class FreqTransitionPointCalculator : PointCalculator() { score, info.distance, info.normalizedDistance, - this.getBase(info)) + this.getBase(info), + analyzedFreqList) } private fun calcTransitions(frequencies: MutableList): List { diff --git a/app/src/main/java/com/chrhsmt/sisheng/point/Point.kt b/app/src/main/java/com/chrhsmt/sisheng/point/Point.kt index 0493d7a..3627574 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/point/Point.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/point/Point.kt @@ -9,7 +9,8 @@ data class Point( val score: Int, val distance: Double, val normalizedDistance: Double, - val base: Int) { + val base: Int, + val analyzedFreqList: MutableList?) { fun success() : Boolean { return this.score >= Settings.pointSuccessThreshold diff --git a/app/src/main/java/com/chrhsmt/sisheng/point/PointCalculator.kt b/app/src/main/java/com/chrhsmt/sisheng/point/PointCalculator.kt index c3a659f..70fd1d7 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/point/PointCalculator.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/point/PointCalculator.kt @@ -1,6 +1,5 @@ package com.chrhsmt.sisheng.point -import android.util.Range import com.chrhsmt.sisheng.AudioService import com.chrhsmt.sisheng.Settings import de.qaware.chronix.distance.DistanceFunctionEnum @@ -22,6 +21,52 @@ abstract class PointCalculator { } abstract fun calc(frequencies: MutableList, testFrequencies: MutableList): Point + /** + * お手本音声の最初の音を参考に、録音した音声をキャリブレーションして周波数を調整する. + */ + fun calibrateFrequencies(analyzedFreqList: MutableList, testFreqList: MutableList) { + + if (analyzedFreqList.size == 0 || testFreqList.size == 0) { + return + } + + val testFirst = testFreqList.first { fl: Float -> fl > 0 } + + val firstSentence = analyzedFreqList.dropWhile { fl: Float -> fl <= 0 }.takeWhile { fl: Float -> fl > 0 }.sorted() + val ave = firstSentence.average() + var medium: Float = 0f + if (firstSentence.size % 2 == 1) { + medium = firstSentence.get(firstSentence.size / 2) + } else { + medium = (firstSentence.get(firstSentence.size / 2) + firstSentence.get(firstSentence.size / 2 - 1)) / 2 + } + +// val variance = firstSentence.sumByDouble { fl -> Math.pow((fl - ave), 2.0) } / firstSentence.size +// val sd = Math.sqrt(variance) + + val degree = testFirst - medium + + if (degree > 0) { + // お手本のほうが周波数が高かった場合 + analyzedFreqList.forEachIndexed { index, fl -> + if (fl > 0) { + analyzedFreqList[index] = fl + degree + } + } + } else { + // お手本のほうが周波数が低かった場合 + analyzedFreqList.forEachIndexed { index, fl -> + if (fl > 0) { + analyzedFreqList[index] = fl - (degree * -1) // マイナスなので一度反転 + if (analyzedFreqList[index] < 0) { + analyzedFreqList[index] = 0F + } + } + } + } + + } + /** * 音声周波数の男女差を調整する */ @@ -96,7 +141,7 @@ abstract class PointCalculator { * Removing noises. * @freqList List of frequency */ - fun removeNoises(freqList: MutableList) { + open fun removeNoises(freqList: MutableList) { val noizeMaxLength = Settings.freqNoizeCountThreashold val freqSize = freqList.size @@ -113,7 +158,7 @@ abstract class PointCalculator { } if (noizeIndexList.size > noizeMaxLength) { - // ノイズ判定サイズを超えたものはノイズとみなさないのでクリアー + // ノイズ判定サイズを超えた連続した部分のものはノイズとみなさないのでクリアー noizeIndexList.clear() } diff --git a/app/src/main/java/com/chrhsmt/sisheng/point/SimplePointCalculator.kt b/app/src/main/java/com/chrhsmt/sisheng/point/SimplePointCalculator.kt index 84060c4..ce08d9f 100644 --- a/app/src/main/java/com/chrhsmt/sisheng/point/SimplePointCalculator.kt +++ b/app/src/main/java/com/chrhsmt/sisheng/point/SimplePointCalculator.kt @@ -1,5 +1,7 @@ package com.chrhsmt.sisheng.point +import android.util.Log +import com.chrhsmt.sisheng.Settings import de.qaware.chronix.distance.DistanceFunctionEnum import de.qaware.chronix.distance.DistanceFunctionFactory import de.qaware.chronix.dtw.FastDTW @@ -11,11 +13,40 @@ import de.qaware.chronix.timeseries.MultivariateTimeSeries */ open class SimplePointCalculator : PointCalculator() { + companion object { + enum class CALIBRATION_TYPE(val type: Int) { + SEX(0), + FREQ(1) + } + enum class NOISE_RECUDER(val type: Int) { + V1(0), + V2(1) + } + } + + var calirationType: CALIBRATION_TYPE = CALIBRATION_TYPE.FREQ + var noizeReducer: NOISE_RECUDER = NOISE_RECUDER.V2 + + fun setCalibrationType(type: CALIBRATION_TYPE) { + this.calirationType = type + } + fun setNoiseReducer(type: NOISE_RECUDER) { + this.noizeReducer = type + } + + fun setV1() { + this.calirationType = CALIBRATION_TYPE.SEX + this.noizeReducer = NOISE_RECUDER.V1 + } + + fun setV2() { + this.calirationType = CALIBRATION_TYPE.FREQ + this.noizeReducer = NOISE_RECUDER.V2 + } + override fun calc(frequencies: MutableList, testFrequencies: MutableList): Point { var analyzedFreqList: MutableList = this.copy(frequencies) - // 調整(男女差) - this.adjustFrequencies(analyzedFreqList) // ノイズ除去 this.removeNoises(analyzedFreqList) @@ -23,6 +54,16 @@ open class SimplePointCalculator : PointCalculator() { // 無音除去 analyzedFreqList = this.removeLastSilence(analyzedFreqList) this.minimizeSilence(analyzedFreqList) + + if (this.calirationType == CALIBRATION_TYPE.SEX) { + // 調整(男女差) + this.adjustFrequencies(analyzedFreqList) + } else { + // 調整(周波数) + this.calibrateFrequencies(analyzedFreqList, testFrequencies) + + } + val exampleFrequencies = this.removeLastSilence(testFrequencies) val info: TimeWarpInfo = this.calcDistance(analyzedFreqList, exampleFrequencies) @@ -32,7 +73,8 @@ open class SimplePointCalculator : PointCalculator() { score, info.distance, info.normalizedDistance, - this.getBase(info)) + this.getBase(info), + analyzedFreqList) // var items = this.frequencies.mapIndexed { index, fl -> TimeSeriesItem(index.toDouble(), TimeSeriesPoint(kotlin.DoubleArray(1){ fl.toDouble() })) } // val ts0 = TimeSeriesBase(items) @@ -43,6 +85,62 @@ open class SimplePointCalculator : PointCalculator() { // } } + override fun removeNoises(freqList: MutableList) { + if (this.noizeReducer == NOISE_RECUDER.V1) { + super.removeNoises(freqList) + } else { + super.removeNoises(freqList) + this.simpleMovingAverage(freqList) + } + } + + /** + * だめかも + */ + fun removeNoisesV2(freqList: MutableList) { + val freqSize = freqList.size + val noizeIndexList: MutableList = arrayListOf() + + // 反転したListのインスタンスでループ + freqList.reversed().forEachIndexed { index, fl -> + // 実Index + val realIndex = freqSize - 1 - index + + // 前後比較 + val target = freqList.get(realIndex) + val after = freqList.getOrElse(realIndex + 1) { 0f } + val before = freqList.getOrElse(realIndex - 1) { 0f } + val rebefore = freqList.getOrElse(realIndex - 2) { 0f } + val rerebefore = freqList.getOrElse(realIndex - 3) { 0f } + if ((after + before + rebefore + rerebefore) / 3 < target) { + noizeIndexList.add(realIndex) + } + + if (realIndex == 0 && noizeIndexList.isNotEmpty()) { + noizeIndexList.forEach { i -> + freqList.removeAt(i) + } + } + } + } + + fun simpleMovingAverage(freqList: MutableList) { + var index = 0 + val ret = freqList.windowed(5, 1, false) { list -> + val ave = list.filter { fl -> fl > 0 }.average() + if (ave > 0 && ave * 1.3 < list.first()) { + return@windowed index++ + } else { + index++ + return@windowed null + } + } + ret.filterNotNull().reversed().forEach { index -> + freqList.removeAt(index) + } + Log.d("TAG", "a") + } + open fun getScore(nomalizedDistance: Double): Int { return Math.max(100 - Math.max(nomalizedDistance.toInt() - 10, 0), 0) } diff --git a/app/src/main/res/drawable/first_screen_bg.png b/app/src/main/res/drawable/first_screen_bg.png index cb5f658..7e1c0fa 100644 Binary files a/app/src/main/res/drawable/first_screen_bg.png and b/app/src/main/res/drawable/first_screen_bg.png differ diff --git a/app/src/main/res/layout/activity_analyze.xml b/app/src/main/res/layout/activity_analyze.xml new file mode 100644 index 0000000..d9fb675 --- /dev/null +++ b/app/src/main/res/layout/activity_analyze.xml @@ -0,0 +1,86 @@ + + + + + + + + + +