├── settings.gradle ├── src └── main │ ├── resources │ ├── baseline_pause_black_48dp.png │ ├── baseline_play_arrow_black_48dp.png │ ├── baseline_skip_previous_black_48dp.png │ ├── 3d.fxml │ ├── simpleProgressDialog.fxml │ └── main.fxml │ └── kotlin │ └── main │ ├── Main.kt │ ├── WindowFactory.kt │ ├── SubSceneManager.kt │ ├── HrtfManager.kt │ ├── SimpleGraph.kt │ └── Controller.kt ├── .idea ├── vcs.xml ├── compiler.xml ├── misc.xml ├── modules.xml ├── gradle.xml └── modules │ ├── HRTFSimulator_main.iml │ └── HRTFSimulator_test.iml └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'HRTFSimulator' 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/baseline_pause_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SIY1121/HRTFSimulator/HEAD/src/main/resources/baseline_pause_black_48dp.png -------------------------------------------------------------------------------- /src/main/resources/baseline_play_arrow_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SIY1121/HRTFSimulator/HEAD/src/main/resources/baseline_play_arrow_black_48dp.png -------------------------------------------------------------------------------- /src/main/resources/baseline_skip_previous_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SIY1121/HRTFSimulator/HEAD/src/main/resources/baseline_skip_previous_black_48dp.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/3d.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HRTF Simulator 2 | 3 | ![HRTF Simulator 2019_08_28 13_52_24](https://user-images.githubusercontent.com/17082836/63826667-2f2b6e00-c99b-11e9-9ecb-c640871dca5c.png) 4 | 5 | HRTF(頭部伝達関数)を利用した立体音響のリアルタイム生成のデモ。 6 | 7 | # 動作環境 8 | - Java8以上(JavaFXが利用できる必要があります) 9 | - [こちら](http://www.sp.m.is.nagoya-u.ac.jp/HRTF/index-j.html)よりHRTFデータベースのダウンロードが必要です 10 | 11 | # 実行 12 | プロジェクトルートで以下のコマンドを実行してください。 13 | ```bash 14 | ./gradlew jfxRun 15 | ``` 16 | 17 | ## HRTFの選択 18 | DLしたHRTFで、`elev0, elev5 ....elev90` というフォルダが 含まれているフォルダ を選択してください。 19 | 20 | ## 音源の選択 21 | 立体音響化したい音声ファイルを選択してください。(FFmpegが対応している形式を読み込めます) 22 | 23 | ## 再生 24 | 再生ボタンで再生されます。 25 | スライダーを動かして音源の位置を変更できます。 26 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/kotlin/main/Main.kt: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import com.aquafx_project.AquaFx 4 | import javafx.application.Application 5 | import javafx.application.Platform 6 | import javafx.fxml.FXMLLoader 7 | import javafx.scene.Scene 8 | import javafx.scene.layout.AnchorPane 9 | import javafx.stage.Stage 10 | import java.io.File 11 | 12 | class Main : Application() { 13 | override fun start(primaryStage: Stage) { 14 | AquaFx.style() 15 | val loader = FXMLLoader(ClassLoader.getSystemResource("main.fxml")) 16 | primaryStage.scene = Scene(loader.load()) 17 | primaryStage.title = "HRTF Simulator" 18 | loader.getController().stage = primaryStage 19 | primaryStage.setOnCloseRequest { 20 | Platform.exit() 21 | } 22 | primaryStage.show() 23 | } 24 | 25 | companion object { 26 | @JvmStatic 27 | fun main(args: Array) { 28 | launch(Main::class.java, *args) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/main/WindowFactory.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import javafx.fxml.FXMLLoader 4 | import javafx.scene.Parent 5 | import javafx.scene.Scene 6 | import javafx.scene.control.Label 7 | import javafx.stage.Modality 8 | import javafx.stage.Screen 9 | import javafx.stage.Stage 10 | 11 | /** 12 | * ソフト内で頻繁に使うダイアログを生成 13 | */ 14 | class WindowFactory { 15 | companion object { 16 | fun buildOnProgressDialog(title: String, msg: String): Stage { 17 | val stage = Stage() 18 | stage.scene = Scene(FXMLLoader.load(ClassLoader.getSystemResource("simpleProgressDialog.fxml"))) 19 | stage.title = title 20 | stage.isResizable = false 21 | stage.initModality(Modality.APPLICATION_MODAL) 22 | (stage.scene.lookup("#label") as Label).text = msg 23 | val primScreenBounds = Screen.getPrimary().visualBounds 24 | stage.x = (primScreenBounds.width - 300) / 2 25 | stage.y = (primScreenBounds.height - 100) / 2 26 | return stage 27 | } 28 | fun createWindow(file: String): Stage { 29 | val stage = Stage() 30 | stage.scene = Scene(FXMLLoader.load(ClassLoader.getSystemResource(file))) 31 | return stage 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/resources/simpleProgressDialog.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/kotlin/main/SubSceneManager.kt: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import javafx.scene.* 4 | import javafx.scene.paint.Color 5 | import javafx.scene.paint.PhongMaterial 6 | import javafx.scene.shape.Box 7 | import javafx.scene.shape.Circle 8 | import javafx.scene.shape.Sphere 9 | import javafx.scene.transform.Rotate 10 | 11 | class SubSceneManager(subScene : SubScene) { 12 | val root = subScene.root as Group 13 | 14 | val guideLine = Circle(100.0).apply { 15 | fill = Color.TRANSPARENT 16 | stroke = Color.RED 17 | rotationAxis = Rotate.X_AXIS 18 | rotate = 180.0 19 | } 20 | 21 | val sphere = Sphere(10.0).apply { 22 | 23 | material = PhongMaterial().apply { 24 | diffuseColor = Color.BLUE 25 | specularColor = Color.SKYBLUE 26 | } 27 | } 28 | 29 | val circle = Circle(100.0).apply { 30 | fill = Color.TRANSPARENT 31 | stroke = Color.YELLOW 32 | rotationAxis = Rotate.X_AXIS 33 | rotate = 90.0 34 | } 35 | 36 | val box = Box(10.0, 10.0, 10.0).apply { 37 | 38 | material = PhongMaterial().apply { 39 | diffuseColor = Color.GREEN 40 | specularColor = Color.YELLOWGREEN 41 | } 42 | } 43 | init{ 44 | subScene.camera = PerspectiveCamera(true) 45 | subScene.camera.translateZ = -400.0 * 30.0 / 360.0 * (Math.PI * 2) - 150 46 | subScene.camera.translateY = -400.0 * 30.0 / 360.0 * (Math.PI * 2) 47 | subScene.camera.rotationAxis = Rotate.X_AXIS 48 | subScene.camera.rotate = -30.0 49 | subScene.camera.farClip = 1000.0 50 | 51 | (subScene.root as Group).children.add(guideLine) 52 | (subScene.root as Group).children.add(sphere) 53 | (subScene.root as Group).children.add(circle) 54 | (subScene.root as Group).children.add(box) 55 | 56 | (subScene.root as Group).children.add(PointLight().apply { 57 | translateX = 100.0 58 | translateY = -200.0 59 | }) 60 | 61 | (subScene.root as Group).children.add(AmbientLight(Color.rgb(80, 80, 80, 0.5))) 62 | subScene.isManaged = false 63 | } 64 | 65 | /** 66 | * 3Dビューを更新 67 | */ 68 | fun setStatus(elev : Int , deg : Int){ 69 | box.translateX = Math.cos((deg + 90.0) / 360.0 * Math.PI * 2) * 100 * Math.cos(elev / 360.0 * Math.PI * 2) 70 | box.translateZ = Math.sin((deg + 90.0) / 360.0 * Math.PI * 2) * 100 * Math.cos(elev / 360.0 * Math.PI * 2) 71 | box.translateY = Math.sin(elev / 360.0 * Math.PI * 2) * -100 72 | 73 | guideLine.rotationAxis = Rotate.Y_AXIS 74 | guideLine.rotate = -deg + 90.0 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/kotlin/main/HrtfManager.kt: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import org.apache.commons.math3.complex.Complex 4 | import org.apache.commons.math3.transform.DftNormalization 5 | import org.apache.commons.math3.transform.FastFourierTransformer 6 | import org.apache.commons.math3.transform.TransformType 7 | import java.io.BufferedReader 8 | import java.io.File 9 | import java.io.FileReader 10 | 11 | class HrtfManager(val dir: File) { 12 | 13 | val L = HashMap>() 14 | val R = HashMap>() 15 | val LRaw = HashMap() 16 | val RRaw = HashMap() 17 | 18 | val fft = FastFourierTransformer(DftNormalization.STANDARD) 19 | 20 | val size: Int 21 | get() = L["0_0"]?.size ?: throw Exception("サイズを返せません") 22 | 23 | init { 24 | val regex = Regex("(L|R)(.*?)d(.*?)e(.*?)a\\.dat") 25 | dir.listFiles().forEach { 26 | if (it.isDirectory) 27 | it.listFiles().forEach { 28 | val res = regex.find(it.name) ?: throw Exception("ファイル名を解析できません") 29 | val ch = res.groupValues[1] 30 | val deg = res.groupValues[4].toInt() 31 | val elev = res.groupValues[3].toInt() 32 | 33 | val reader = BufferedReader(FileReader(it)) 34 | val list = ArrayList() 35 | while (true) { 36 | val v = reader.readLine() ?: break 37 | list.add(v.toFloat()) 38 | } 39 | 40 | val src = FloatArray(list.size) + list 41 | 42 | val irFFT = fft.transform(src.map { it.toDouble() }.toDoubleArray(), TransformType.FORWARD) 43 | 44 | if (ch == "L"){ 45 | L["${elev}_$deg"] = irFFT 46 | LRaw["${elev}_$deg"] = list.toFloatArray() 47 | } 48 | else if (ch == "R"){ 49 | R["${elev}_$deg"] = irFFT 50 | RRaw["${elev}_$deg"] = list.toFloatArray() 51 | } 52 | } 53 | 54 | } 55 | } 56 | 57 | /** 58 | * HRTFを適用する 59 | */ 60 | fun applyHRTF(src: FloatArray, ch: String, deg: Int, elev: Int): Array { 61 | if (ch == "L") { 62 | //FFT変換 63 | var fftL = fft.transform(src.map { it.toDouble() }.toDoubleArray(), TransformType.FORWARD) 64 | 65 | //周波数領域で畳み込む 66 | fftL = fftL.mapIndexed { index, complex -> 67 | complex.multiply(L["${elev}_$deg"]?.get(index)) ?: Complex(0.0) 68 | }.toTypedArray() 69 | 70 | //逆変換 71 | return fft.transform(fftL, TransformType.INVERSE) 72 | } else if (ch == "R") { 73 | //FFT変換 74 | var fftR = fft.transform(src.map { it.toDouble() }.toDoubleArray(), TransformType.FORWARD) 75 | 76 | //周波数領域で畳み込む 77 | fftR = fftR.mapIndexed { index, complex -> 78 | complex.multiply(R["${elev}_$deg"]?.get(index)) ?: Complex(0.0) 79 | }.toTypedArray() 80 | 81 | //逆変換 82 | return fft.transform(fftR, TransformType.INVERSE) 83 | } 84 | return Array(0) { _ -> Complex(0.0) } 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/main/SimpleGraph.kt: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import javafx.beans.binding.Bindings 4 | import javafx.geometry.Bounds 5 | import javafx.geometry.Insets 6 | import javafx.scene.canvas.Canvas 7 | import javafx.scene.canvas.GraphicsContext 8 | import javafx.scene.control.Label 9 | import javafx.scene.layout.Pane 10 | import javafx.scene.paint.Color 11 | import javafx.scene.paint.Paint 12 | import javafx.scene.text.Font 13 | import javafx.scene.text.Text 14 | import javafx.scene.transform.Affine 15 | import javax.xml.crypto.Data 16 | 17 | class SimpleGraph : Pane() { 18 | data class DataPoint(val x: Double, val y: Double) 19 | 20 | var xNegative = false 21 | var yNegative = false 22 | var yTranslateToPositive = false 23 | 24 | private val canvas = Canvas() 25 | private val g: GraphicsContext 26 | 27 | var background: Paint = Color.WHITE 28 | 29 | 30 | var data: List = ArrayList() 31 | set(value) { 32 | field = if (yTranslateToPositive) { 33 | val min = value.minBy { it.y }?.y ?: 0.0 34 | value.map { DataPoint(it.x, it.y - min) } 35 | } else { 36 | value 37 | } 38 | draw() 39 | } 40 | 41 | var title = "Title" 42 | var xAxisName = "x" 43 | var yAxisName = "y" 44 | 45 | init { 46 | g = canvas.graphicsContext2D 47 | 48 | val sampleData = ArrayList() 49 | for (i in 0 until 1000) { 50 | sampleData.add(DataPoint(i.toDouble() - 500, Math.sin(i / 100.0))) 51 | } 52 | data = sampleData 53 | 54 | widthProperty().addListener { _, _, n -> 55 | canvas.width = n.toDouble() 56 | padding = Insets( 57 | 20.0, 20.0, 58 | if (yNegative) canvas.height / 2 else 20.0, 59 | if (xNegative) canvas.width / 2 else 20.0) 60 | draw() 61 | } 62 | heightProperty().addListener { _, _, n -> 63 | canvas.height = n.toDouble() 64 | padding = Insets( 65 | 20.0, 20.0, 66 | if (yNegative) canvas.height / 2 else 20.0, 67 | if (xNegative) canvas.width / 2 else 20.0) 68 | draw() 69 | } 70 | children.add(canvas) 71 | g.font = Font(40.0) 72 | } 73 | 74 | private fun draw() { 75 | g.fill = background 76 | g.stroke = Color.BLACK 77 | 78 | g.fillRect(0.0, 0.0, canvas.width, canvas.height) 79 | g.strokeLine(padding.left, padding.top, padding.left, canvas.height - padding.bottom) 80 | g.strokeLine(padding.left, canvas.height - padding.bottom, canvas.width - padding.right, canvas.height - padding.bottom) 81 | 82 | g.fill = Color.BLACK 83 | g.font = Font(20.0) 84 | g.fillText(title, canvas.width / 2 - estimateTextSize(title).width / 2, estimateTextSize(title).height) 85 | g.fillText(yAxisName, padding.left - estimateTextSize(yAxisName).width / 2, estimateTextSize(yAxisName).height / 2) 86 | g.fillText(xAxisName, canvas.width - padding.right, canvas.height - padding.bottom + estimateTextSize(xAxisName).height / 2) 87 | 88 | val xMax = data.map { Math.abs(it.x) }.max() ?: 1.0 89 | val yMax = data.map { Math.abs(it.y) }.max() ?: 1.0 90 | 91 | data.forEach { 92 | val y = padding.top + (1.0 - (it.y / yMax)) * (canvas.height - padding.top - padding.bottom) 93 | val x = padding.left + (it.x / xMax) * (canvas.width - padding.right - padding.left) 94 | 95 | g.strokeLine(x, y, x, canvas.height - padding.bottom) 96 | } 97 | } 98 | 99 | private fun estimateTextSize(text: String): Bounds { 100 | val t = Text(text) 101 | t.font = g.font 102 | return t.boundsInLocal 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /src/main/resources/main.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 52 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 |
86 | -------------------------------------------------------------------------------- /src/main/kotlin/main/Controller.kt: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import javafx.application.Platform 4 | import javafx.event.ActionEvent 5 | import javafx.fxml.FXML 6 | import javafx.fxml.Initializable 7 | import javafx.scene.SubScene 8 | import javafx.scene.control.Label 9 | import javafx.scene.control.ProgressBar 10 | import javafx.scene.control.Slider 11 | import javafx.scene.image.Image 12 | import javafx.scene.image.ImageView 13 | import javafx.scene.layout.Pane 14 | import javafx.scene.layout.StackPane 15 | import javafx.stage.DirectoryChooser 16 | import javafx.stage.FileChooser 17 | import javafx.stage.Stage 18 | import org.bytedeco.javacv.FFmpegFrameGrabber 19 | import org.bytedeco.javacv.FrameGrabber 20 | import ui.WindowFactory 21 | import java.net.URL 22 | import java.nio.ByteBuffer 23 | import java.nio.ByteOrder 24 | import java.nio.FloatBuffer 25 | import java.util.* 26 | import javax.sound.sampled.AudioFormat 27 | import javax.sound.sampled.AudioSystem 28 | import javax.sound.sampled.DataLine 29 | import javax.sound.sampled.SourceDataLine 30 | import kotlin.collections.ArrayList 31 | 32 | class Controller : Initializable { 33 | 34 | var stage: Stage? = null 35 | 36 | @FXML 37 | lateinit var root: Pane 38 | @FXML 39 | lateinit var slider: Slider 40 | @FXML 41 | lateinit var slider2: Slider 42 | @FXML 43 | lateinit var subScene: SubScene 44 | @FXML 45 | lateinit var subSceneContainer: StackPane 46 | @FXML 47 | lateinit var buttonImage: ImageView 48 | @FXML 49 | lateinit var irSampleCanvasL: SimpleGraph 50 | @FXML 51 | lateinit var irSampleCanvasR: SimpleGraph 52 | @FXML 53 | lateinit var dstSampleCanvasL: SimpleGraph 54 | @FXML 55 | lateinit var dstSampleCanvasR: SimpleGraph 56 | 57 | @FXML 58 | lateinit var currentPositionLabel: Label 59 | @FXML 60 | lateinit var seekBar: ProgressBar 61 | @FXML 62 | lateinit var subSceneManager: SubSceneManager 63 | 64 | lateinit var manager: HrtfManager 65 | 66 | lateinit var srcGrabber: FFmpegFrameGrabber 67 | 68 | var playing = false 69 | 70 | override fun initialize(location: URL?, resources: ResourceBundle?) { 71 | 72 | //シーンマネージャを初期化 73 | subSceneManager = SubSceneManager(subScene) 74 | //シーンのサイズを外枠に合わせる 75 | subScene.widthProperty().bind(subSceneContainer.widthProperty()) 76 | subScene.heightProperty().bind(subSceneContainer.heightProperty()) 77 | 78 | //角度スライダー 79 | slider.valueProperty().addListener { _, _, n -> 80 | subSceneManager.setStatus(slider2.value.toInt(), slider.value.toInt()) 81 | irSampleCanvasL.data = manager.LRaw["${slider2.value.toInt() / 5 * 5}_${slider.value.toInt() / 5 * 5}"]?.mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.toDouble()) } ?: ArrayList() 82 | irSampleCanvasR.data = manager.RRaw["${slider2.value.toInt() / 5 * 5}_${slider.value.toInt() / 5 * 5}"]?.mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.toDouble()) } ?: ArrayList() 83 | 84 | } 85 | //高度スライダー 86 | slider2.valueProperty().addListener { _, _, n -> 87 | subSceneManager.setStatus(slider2.value.toInt(), slider.value.toInt()) 88 | irSampleCanvasL.data = manager.LRaw["${slider2.value.toInt() / 5 * 5}_${slider.value.toInt() / 5 * 5}"]?.mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.toDouble()) } ?: ArrayList() 89 | irSampleCanvasR.data = manager.RRaw["${slider2.value.toInt() / 5 * 5}_${slider.value.toInt() / 5 * 5}"]?.mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.toDouble()) } ?: ArrayList() 90 | 91 | } 92 | 93 | } 94 | 95 | /** 96 | * インパルス応答のデータベースが選択されたときに呼び出される 97 | */ 98 | fun onImpulseSelect() { 99 | val dir = DirectoryChooser().showDialog(root.scene.window) ?: return 100 | val dialog = WindowFactory.buildOnProgressDialog("Processing", "Loading Database...") 101 | dialog.show() 102 | Thread { 103 | manager = HrtfManager(dir) 104 | println(manager.L) 105 | Platform.runLater { dialog.close() } 106 | }.start() 107 | } 108 | 109 | /** 110 | * 畳み込み先ファイルが選択されたときに呼び出される 111 | */ 112 | fun onSrcSelect(actionEvent: ActionEvent) { 113 | val file = FileChooser().showOpenDialog(root.scene.window) ?: return 114 | val dialog = WindowFactory.buildOnProgressDialog("Processing", "Loading Music...") 115 | dialog.show() 116 | Thread{ 117 | srcGrabber = FFmpegFrameGrabber(file) 118 | srcGrabber.audioChannels = 1 119 | srcGrabber.sampleRate = 48000 120 | srcGrabber.sampleMode = FrameGrabber.SampleMode.FLOAT 121 | srcGrabber.start() 122 | Platform.runLater { dialog.close() } 123 | }.start() 124 | 125 | } 126 | 127 | /** 128 | * 再生 129 | */ 130 | fun play(actionEvent: ActionEvent) { 131 | 132 | //ウィンドウを閉じる際に再生を停止 133 | root.scene.window.setOnCloseRequest { 134 | playing = false 135 | } 136 | 137 | if (playing) { 138 | playing = false 139 | buttonImage.image = Image(ClassLoader.getSystemResource("baseline_play_arrow_black_48dp.png").toString()) 140 | return 141 | } 142 | playing = true 143 | buttonImage.image = Image(ClassLoader.getSystemResource("baseline_pause_black_48dp.png").toString()) 144 | 145 | Thread { 146 | val audioFormat = AudioFormat((srcGrabber.sampleRate.toFloat() ?: 0f), 16, 2, true, false) 147 | 148 | val info = DataLine.Info(SourceDataLine::class.java, audioFormat) 149 | val audioLine = AudioSystem.getLine(info) as SourceDataLine 150 | audioLine.open(audioFormat) 151 | audioLine.start() 152 | 153 | //val rec = FFmpegFrameRecorder(File("out.mp3"), 2) 154 | //rec.sampleRate = 44100 155 | //rec.audioBitrate = 192_000 156 | 157 | //rec.start() 158 | var max = 1f 159 | var prevSample = FloatArray(manager.size / 2) 160 | while (playing) { 161 | val sample = readSamples(manager.size / 2) ?: break 162 | 163 | val src = prevSample + sample 164 | val dstL = manager.applyHRTF(src, "L", slider.value.toInt() / 5 * 5, slider2.value.toInt() / 5 * 5) 165 | val dstR = manager.applyHRTF(src, "R", slider.value.toInt() / 5 * 5, slider2.value.toInt() / 5 * 5) 166 | 167 | Platform.runLater { 168 | //グラフ描画 169 | dstSampleCanvasL.data = dstL.slice(0 until dstL.size / 2).mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.real) } 170 | dstSampleCanvasR.data = dstR.slice(0 until dstL.size / 2).mapIndexed { index, value -> SimpleGraph.DataPoint(index.toDouble(), value.real) } 171 | seekBar.progress = srcGrabber.timestamp / srcGrabber.lengthInTime.toDouble() 172 | currentPositionLabel.text = srcGrabber.timestamp.long2TimeText() 173 | } 174 | 175 | 176 | val dst = FloatArray(dstL.size + dstR.size) 177 | for (i in 0 until dstL.size) { 178 | dst[i * 2] = dstL[i].real.toFloat() 179 | dst[i * 2 + 1] = dstR[i].real.toFloat() 180 | } 181 | 182 | //正規化 183 | max = Math.max(max, dst.max() ?: 0f) 184 | println(max) 185 | for (i in 0 until dst.size) 186 | dst[i] /= max 187 | 188 | //shortに変換してバイト配列に変換する 189 | //円状畳み込み結果である前半は切り捨て 190 | val buf = ByteBuffer.allocate(dst.size).order(ByteOrder.LITTLE_ENDIAN) 191 | for (i in 0 until dst.size / 2) { 192 | buf.putShort((dst[i] * Short.MAX_VALUE).toShort()) 193 | } 194 | buf.position(0) 195 | 196 | val arr = buf.array() 197 | audioLine.write(arr, 0, arr.size) 198 | //rec.recordSamples(44100, 2, buf) 199 | 200 | prevSample = sample 201 | } 202 | //rec.stop() 203 | audioLine.stop() 204 | }.start() 205 | } 206 | 207 | var tmpBuffer: FloatBuffer? = null 208 | /** 209 | * 指定された数だけ、畳み込み先のサンプルを返す 210 | */ 211 | private fun readSamples(size: Int): FloatArray? { 212 | val result = FloatArray(size) 213 | var read = 0 214 | while (read < size) { 215 | if (tmpBuffer == null || tmpBuffer?.remaining() == 0) 216 | tmpBuffer = srcGrabber?.grabSamples()?.samples?.get(0) as? FloatBuffer ?: break 217 | 218 | val toRead = Math.min(tmpBuffer?.remaining() ?: 0, size - read) 219 | tmpBuffer?.get(result, read, toRead) 220 | read += toRead 221 | } 222 | return if (read > 0) result else null 223 | } 224 | 225 | /** 226 | *指定された配列のデータを置き換える 227 | */ 228 | fun FloatArray.replaceRange(start: Int, end: Int, replacement: FloatArray) { 229 | if (end - start != replacement.size) throw Exception("置き換えの配列と範囲の大きさが一致しません") 230 | for (i in start until end) 231 | this[i] = replacement[i - start] 232 | } 233 | 234 | /** 235 | * 渡された配列を長さが2の累乗になるようにパディングして返す 236 | */ 237 | fun FloatArray.toPower2(): FloatArray { 238 | var i = 1.0 239 | while (this.size > Math.pow(2.0, i)) { 240 | i++ 241 | } 242 | 243 | return this + FloatArray(Math.pow(2.0, i).toInt() - this.size) 244 | } 245 | 246 | 247 | fun prev(actionEvent: ActionEvent) { 248 | srcGrabber.timestamp = 0 249 | } 250 | 251 | fun Long.long2TimeText(): String { 252 | val a = this / 1000_000 253 | return "${a / 60}:${String.format("%02d", a % 60)}" 254 | } 255 | 256 | fun showProgressDialog() { 257 | val stage = Stage() 258 | } 259 | } -------------------------------------------------------------------------------- /.idea/modules/HRTFSimulator_main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /.idea/modules/HRTFSimulator_test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | --------------------------------------------------------------------------------