├── 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 |
5 |
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 | 
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 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
52 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------