diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c061fc3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.idea
+/target
+*.iml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..91b76ba
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+# EDF parser for Kotlin
+Small and simple library to work with EDF files written in Kotlin
+
+### Examples
+You can pass file
+```kotlin
+val file = File("/example.edf")
+val edfFile = EdfParser.parse(file)
+```
+
+Or stream
+```kotlin
+class EdfParserExample
+val stream: InputStream = EdfParserExample::class.java.getResourceAsStream("/example.edf")
+val edfFile = EdfParser.parse(stream)
+```
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f287a35
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+ com.npwork
+ kotlin-edf-parser
+ 1.0-SNAPSHOT
+
+
+ 1.3.61
+ 5.5.2
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ ${kotlin.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+ 1.8
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/kotlin/com/npwork/edfparser/EDFConstants.kt b/src/main/kotlin/com/npwork/edfparser/EDFConstants.kt
new file mode 100644
index 0000000..23c5218
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/EDFConstants.kt
@@ -0,0 +1,39 @@
+package com.npwork.edfparser
+
+import java.nio.charset.Charset
+
+object EDFConstants {
+ val CHARSET = Charset.forName("ASCII")
+
+ const val IDENTIFICATION_CODE_SIZE = 8
+ const val LOCAL_SUBJECT_IDENTIFICATION_SIZE = 80
+ const val LOCAL_REOCRDING_IDENTIFICATION_SIZE = 80
+ const val START_DATE_SIZE = 8
+ const val START_TIME_SIZE = 8
+ const val HEADER_SIZE = 8
+ const val DATA_FORMAT_VERSION_SIZE = 44
+ const val DURATION_DATA_RECORDS_SIZE = 8
+ const val NUMBER_OF_DATA_RECORDS_SIZE = 8
+ const val NUMBER_OF_CHANELS_SIZE = 4
+
+ const val LABEL_OF_CHANNEL_SIZE = 16
+ const val TRANSDUCER_TYPE_SIZE = 80
+ const val PHYSICAL_DIMENSION_OF_CHANNEL_SIZE = 8
+ const val PHYSICAL_MIN_IN_UNITS_SIZE = 8
+ const val PHYSICAL_MAX_IN_UNITS_SIZE = 8
+ const val DIGITAL_MIN_SIZE = 8
+ const val DIGITAL_MAX_SIZE = 8
+ const val PREFILTERING_SIZE = 80
+ const val NUMBER_OF_SAMPLES_SIZE = 8
+ const val RESERVED_SIZE = 32
+
+ /** The size of the EDF-Header-Record containing information about the recording */
+ const val HEADER_SIZE_RECORDING_INFO = (IDENTIFICATION_CODE_SIZE + LOCAL_SUBJECT_IDENTIFICATION_SIZE + LOCAL_REOCRDING_IDENTIFICATION_SIZE
+ + START_DATE_SIZE + START_TIME_SIZE + HEADER_SIZE + DATA_FORMAT_VERSION_SIZE + DURATION_DATA_RECORDS_SIZE
+ + NUMBER_OF_DATA_RECORDS_SIZE + NUMBER_OF_CHANELS_SIZE)
+
+ /** The size per channel of the EDF-Header-Record containing information a channel of the recording */
+ const val HEADER_SIZE_PER_CHANNEL = (LABEL_OF_CHANNEL_SIZE + TRANSDUCER_TYPE_SIZE + PHYSICAL_DIMENSION_OF_CHANNEL_SIZE
+ + PHYSICAL_MIN_IN_UNITS_SIZE + PHYSICAL_MAX_IN_UNITS_SIZE + DIGITAL_MIN_SIZE + DIGITAL_MAX_SIZE
+ + PREFILTERING_SIZE + NUMBER_OF_SAMPLES_SIZE + RESERVED_SIZE)
+}
diff --git a/src/main/kotlin/com/npwork/edfparser/EdfFormatException.kt b/src/main/kotlin/com/npwork/edfparser/EdfFormatException.kt
new file mode 100644
index 0000000..abbea33
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/EdfFormatException.kt
@@ -0,0 +1,8 @@
+package com.npwork.edfparser
+
+sealed class EdfFormatException(message: String? = null) : RuntimeException(message) {
+ class EmptyFile(message: String? = "File is empty") : EdfFormatException(message)
+ class WrongFormat(message: String? = "Wrong format of EDF file. Please check https://www.teuniz.net/edfbrowser/edf%20format%20description.html") : EdfFormatException(message)
+ class WrongHeader(message: String? = "Error during header parsing") : EdfFormatException(message)
+ class WrongSignal(message: String? = "Error during signal parsing") : EdfFormatException(message)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/npwork/edfparser/EdfParser.kt b/src/main/kotlin/com/npwork/edfparser/EdfParser.kt
new file mode 100644
index 0000000..309fa53
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/EdfParser.kt
@@ -0,0 +1,115 @@
+package com.npwork.edfparser
+
+import com.npwork.edfparser.dto.EdfFile
+import com.npwork.edfparser.dto.EdfHeader
+import com.npwork.edfparser.dto.EdfSignal
+import com.npwork.edfparser.extensions.*
+import java.io.File
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.channels.Channels
+import java.nio.channels.ReadableByteChannel
+
+object EdfParser {
+ fun parse(file: File): EdfFile = parse(file.inputStream())
+
+ fun parse(stream: InputStream): EdfFile {
+ if (stream.available() == 0)
+ throw EdfFormatException.EmptyFile()
+
+ val header = try {
+ parseHeader(stream)
+ } catch (e: Exception) {
+ throw EdfFormatException.WrongHeader()
+ }
+
+ val signal = try {
+ parseSignal(stream, header)
+ } catch (e: Exception) {
+ throw EdfFormatException.WrongSignal()
+ }
+
+ return EdfFile(header = header, signal = signal)
+ }
+
+ private fun parseHeader(stream: InputStream): EdfHeader {
+ var numberOfChannels = 0
+ return EdfHeader(
+ idCode = fun(): String {
+ val idCode = stream.readASCII(EDFConstants.IDENTIFICATION_CODE_SIZE)
+ ensureValidIdentificationCode(idCode)
+ return idCode
+ }(),
+ subjectID = stream.readASCII(EDFConstants.LOCAL_SUBJECT_IDENTIFICATION_SIZE),
+ recordingID = stream.readASCII(EDFConstants.LOCAL_REOCRDING_IDENTIFICATION_SIZE),
+ startDate = stream.readASCII(EDFConstants.START_DATE_SIZE),
+ startTime = stream.readASCII(EDFConstants.START_TIME_SIZE),
+ bytesInHeader = stream.readASCII(EDFConstants.HEADER_SIZE).trim().toInt(),
+ formatVersion = stream.readASCII(EDFConstants.DATA_FORMAT_VERSION_SIZE),
+ numberOfRecords = stream.readASCII(EDFConstants.NUMBER_OF_DATA_RECORDS_SIZE).trim().toInt(),
+ durationOfRecords = stream.readASCII(EDFConstants.DURATION_DATA_RECORDS_SIZE).trim().toDouble(),
+ numberOfChannels = fun(): Int {
+ numberOfChannels = stream.readASCII(EDFConstants.NUMBER_OF_CHANELS_SIZE).trim().toInt()
+ return numberOfChannels
+ }(),
+ channelLabels = stream.readASCIIBulk(EDFConstants.LABEL_OF_CHANNEL_SIZE, numberOfChannels),
+ transducerTypes = stream.readASCIIBulk(EDFConstants.TRANSDUCER_TYPE_SIZE, numberOfChannels),
+ dimensions = stream.readASCIIBulk(EDFConstants.PHYSICAL_DIMENSION_OF_CHANNEL_SIZE, numberOfChannels),
+ minInUnits = stream.readASCIIBulkDouble(EDFConstants.PHYSICAL_MIN_IN_UNITS_SIZE, numberOfChannels),
+ maxInUnits = stream.readASCIIBulkDouble(EDFConstants.PHYSICAL_MAX_IN_UNITS_SIZE, numberOfChannels),
+ digitalMin = stream.readASCIIBulkInt(EDFConstants.DIGITAL_MIN_SIZE, numberOfChannels),
+ digitalMax = stream.readASCIIBulkInt(EDFConstants.DIGITAL_MAX_SIZE, numberOfChannels),
+ prefilterings = stream.readASCIIBulk(EDFConstants.PREFILTERING_SIZE, numberOfChannels),
+ numberOfSamples = stream.readASCIIBulkInt(EDFConstants.NUMBER_OF_SAMPLES_SIZE, numberOfChannels),
+ reserveds = (1..numberOfChannels).map { stream.readNBytes(EDFConstants.RESERVED_SIZE) }
+ )
+ }
+
+ private fun ensureValidIdentificationCode(idCode: String) {
+ if (idCode.trim() != "0") {
+ throw EdfFormatException.WrongFormat()
+ }
+ }
+
+ private fun parseSignal(stream: InputStream, header: EdfHeader): EdfSignal {
+ val signal = EdfSignal(
+ unitsInDigit = (0 until header.numberOfChannels)
+ .map {
+ (header.maxInUnits[it] - header.minInUnits[it]) /
+ (header.digitalMax[it] - header.digitalMin[it])
+ }
+ .toTypedArray(),
+
+ digitalValues = (0 until header.numberOfChannels)
+ .map { ShortArray(header.numberOfRecords * header.numberOfSamples[it]) }
+ .toTypedArray(),
+
+ valuesInUnits = (0 until header.numberOfChannels)
+ .map { DoubleArray(header.numberOfRecords * header.numberOfSamples[it]) }
+ .toTypedArray()
+ )
+
+ val samplesPerRecord = header.numberOfSamples.sum()
+
+ val ch: ReadableByteChannel = Channels.newChannel(stream)
+ val bytebuf = ByteBuffer.allocate(samplesPerRecord * 2)
+ bytebuf.order(ByteOrder.LITTLE_ENDIAN)
+
+ for (i in 0 until header.numberOfRecords) {
+ bytebuf.rewind()
+ ch.read(bytebuf)
+ bytebuf.rewind()
+
+ for (j in 0 until header.numberOfChannels) {
+ for (k in 0 until header.numberOfSamples[j]) {
+ val s: Int = header.numberOfSamples[j] * i + k
+ signal.digitalValues[j][s] = bytebuf.short
+ signal.valuesInUnits[j][s] = signal.digitalValues[j][s] * signal.unitsInDigit[j]
+ }
+ }
+ }
+
+ return signal
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/npwork/edfparser/dto/EdfFile.kt b/src/main/kotlin/com/npwork/edfparser/dto/EdfFile.kt
new file mode 100644
index 0000000..85d91f8
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/dto/EdfFile.kt
@@ -0,0 +1,9 @@
+package com.npwork.edfparser.dto
+
+data class EdfFile(
+ val header: EdfHeader,
+ val signal: EdfSignal
+) {
+ val samples: List = (0 until header.numberOfChannels).map { signal.digitalValues[it].size }.toList()
+ val sampleRate: List = samples.map { it / header.durationOfRecords }.toList()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/npwork/edfparser/dto/EdfHeader.kt b/src/main/kotlin/com/npwork/edfparser/dto/EdfHeader.kt
new file mode 100644
index 0000000..17abb00
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/dto/EdfHeader.kt
@@ -0,0 +1,26 @@
+package com.npwork.edfparser.dto
+
+data class EdfHeader(
+ val idCode: String,
+ val subjectID: String,
+ val recordingID: String,
+ val startDate: String,
+ val startTime: String,
+ val bytesInHeader: Int,
+ val formatVersion: String,
+ val numberOfRecords: Int,
+ val durationOfRecords: Double,
+ val numberOfChannels: Int,
+
+ // Channel info
+ val channelLabels: List,
+ val transducerTypes: List,
+ val dimensions: List,
+ val minInUnits: List,
+ val maxInUnits: List,
+ val digitalMin: List,
+ val digitalMax: List,
+ val prefilterings: List,
+ val numberOfSamples: List,
+ val reserveds: List
+)
diff --git a/src/main/kotlin/com/npwork/edfparser/dto/EdfSignal.kt b/src/main/kotlin/com/npwork/edfparser/dto/EdfSignal.kt
new file mode 100644
index 0000000..99c9cb4
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/dto/EdfSignal.kt
@@ -0,0 +1,27 @@
+package com.npwork.edfparser.dto
+
+data class EdfSignal(
+ var unitsInDigit: Array,
+ var digitalValues: Array,
+ var valuesInUnits: Array
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as EdfSignal
+
+ if (!unitsInDigit.contentEquals(other.unitsInDigit)) return false
+ if (!digitalValues.contentDeepEquals(other.digitalValues)) return false
+ if (!valuesInUnits.contentDeepEquals(other.valuesInUnits)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = unitsInDigit.contentHashCode()
+ result = 31 * result + digitalValues.contentDeepHashCode()
+ result = 31 * result + valuesInUnits.contentDeepHashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/npwork/edfparser/extensions/InputStreamExtensions.kt b/src/main/kotlin/com/npwork/edfparser/extensions/InputStreamExtensions.kt
new file mode 100644
index 0000000..31fd920
--- /dev/null
+++ b/src/main/kotlin/com/npwork/edfparser/extensions/InputStreamExtensions.kt
@@ -0,0 +1,16 @@
+package com.npwork.edfparser.extensions
+
+import com.npwork.edfparser.EDFConstants
+import java.io.InputStream
+
+fun InputStream.readNBytes(length: Int): ByteArray {
+ val data = ByteArray(length)
+ this.read(data)
+ return data
+}
+
+fun InputStream.readASCII(length: Int): String = String(readNBytes(length), EDFConstants.CHARSET).trim()
+
+fun InputStream.readASCIIBulk(length: Int, times: Int): List = (1..times).map { this.readASCII(length) }
+fun InputStream.readASCIIBulkDouble(length: Int, times: Int): List = (1..times).map { this.readASCII(length).trim().toDouble() }
+fun InputStream.readASCIIBulkInt(length: Int, times: Int): List = (1..times).map { this.readASCII(length).trim().toInt() }
diff --git a/src/test/kotlin/com/npwork/edfparser/EdfParserTest.kt b/src/test/kotlin/com/npwork/edfparser/EdfParserTest.kt
new file mode 100644
index 0000000..0fe8da0
--- /dev/null
+++ b/src/test/kotlin/com/npwork/edfparser/EdfParserTest.kt
@@ -0,0 +1,65 @@
+package com.npwork.edfparser
+
+import com.npwork.edfparser.dto.EdfFile
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertThrows
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+class EdfParserTest {
+ @Test
+ @DisplayName("EEG 38 channels")
+ fun eegFile_38_channels() {
+ val edfFile = EdfParser.parse(getResource("teuniz_net_test_file_eeg_38_ch.edf"))
+
+ assertEquals(9984, edfFile.header.bytesInHeader)
+ verifyNumberOfChannels(38, edfFile)
+ }
+
+
+ @Test
+ @DisplayName("Short ECG")
+ fun short_ecg() {
+ val edfFile = EdfParser.parse(getResource("short_ecg.edf"))
+ assertEquals(512, edfFile.header.bytesInHeader)
+ assertEquals(listOf(7684), edfFile.samples)
+ assertEquals(listOf(256.0000426444631), edfFile.sampleRate)
+ verifyNumberOfChannels(1, edfFile)
+ }
+
+ @DisplayName("Wrong cases")
+ @Nested
+ inner class WrongCases {
+ @Test
+ @DisplayName("Empty file")
+ fun emptyFile() {
+ assertThrows(EdfFormatException.EmptyFile::class.java) {
+ EdfParser.parse(getResource("empty_file.edf"))
+ }
+ }
+
+ @Test
+ @DisplayName("Header is not complete")
+ fun partialHeader() {
+ assertThrows(EdfFormatException.WrongHeader::class.java) {
+ EdfParser.parse(getResource("partial_header.edf"))
+ }
+ }
+
+ @Test
+ @DisplayName("From file format")
+ fun partialSignal() {
+ assertThrows(EdfFormatException.WrongHeader::class.java) {
+ EdfParser.parse(getResource("wrong_file.edf"))
+ }
+ }
+ }
+
+ private fun verifyNumberOfChannels(expectedNumberOfChannels: Int, edfFile: EdfFile) {
+ assertEquals(expectedNumberOfChannels, edfFile.header.channelLabels.size)
+ assertEquals(expectedNumberOfChannels, edfFile.header.numberOfSamples.size)
+ }
+
+ private fun getResource(fileName: String) = EdfParserTest::class.java.getResourceAsStream("/$fileName")
+}
diff --git a/src/test/resources/empty_file.edf b/src/test/resources/empty_file.edf
new file mode 100644
index 0000000..e69de29
diff --git a/src/test/resources/partial_header.edf b/src/test/resources/partial_header.edf
new file mode 100644
index 0000000..2be4c06
--- /dev/null
+++ b/src/test/resources/partial_header.edf
@@ -0,0 +1,2 @@
+0 18.12.1914.16.00512 1 30.015621 Lead I uV -32768 32767 -32768 32767
+
diff --git a/src/test/resources/short_ecg.edf b/src/test/resources/short_ecg.edf
new file mode 100644
index 0000000..f172c58
Binary files /dev/null and b/src/test/resources/short_ecg.edf differ
diff --git a/src/test/resources/teuniz_net_test_file_eeg_38_ch.edf b/src/test/resources/teuniz_net_test_file_eeg_38_ch.edf
new file mode 100644
index 0000000..e1a9dcc
--- /dev/null
+++ b/src/test/resources/teuniz_net_test_file_eeg_38_ch.edf
@@ -0,0 +1,59649 @@
+0 1234567 M 09-APR-1955 L._Smith Startdate 15-SEP-2005 2 Kesteren Nihon_Kohden_EEG-1100C_V01.00 15.09.0510.18.429984 EDF+C 18181 0.1 38 EEG FP1 EEG FP2 EEG F3 EEG F4 EEG C3 EEG C4 EEG P3 EEG P4 EEG O1 EEG O2 EEG F7 EEG F8 EEG T3 EEG T4 EEG T5 EEG T6 EEG FZ EEG CZ EEG PZ EEG E EEG PG1 EEG PG2 EEG A1 EEG A2 EEG T1 EEG T2 EEG X1 EEG X2 EEG X3 EEG X4 EEG X5 EEG X6 EEG X7 DC01 DC02 DC03 DC04 EDF Annotations uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV uV mV mV mV mV -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -3200 -12002.9-12002.9-12002.9-12002.9-1 3199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.9023199.90212002.5612002.5612002.5612002.561 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 -32768 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 32767 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 25 +0.0 +0REC START IIB CAL +0.1 +0.2 +0.3 +0.4 +0.5 +0.6 +0.7 +0.8 +0.9 +1.0 +0PAT IIB EEG +1.1 +1.2 +1.3 +1.4 }uxVgN,:) TU]QPO ~j]`RS.`I;" ^V pXa % M 2 &